dianara-v1.1/000775 000764 000764 00000000000 12264106407 012536 5ustar00janjan000000 000000 dianara-v1.1/BUGS000644 000764 000764 00000003027 12254154604 013222 0ustar00janjan000000 000000 Known issues: - Sometimes you'll see that a post has some comments, but not see the comments themselves, or not all of them. This is more of an issue with synchronization between different federated Pump servers, not something Dianara-specific. It is being worked on by the pump.io developers. - SSL errors are ignored at the moment. - Some things don't "refresh" immediately as they should. These are things that are just not done yet. They will be in future versions. - Some issues with the "preformatting block" option. - The "insert an image from a web site" option will insert a white icon in place of the image while editing, but the image will be seen in the post, if the link to it is good. - User's system color configuration affects how inserted links are displayed to everyone else, so until a proper fix is added, links will be "pump.io blue", regardless of system config. - Animated GIFs are not animated. - If the profile is not received correctly on startup, posting to "Followers" will fail. - A comment being composed might be lost if something like this happens: > Comment in post A, timeline updates are blocked. > Start commenting in post B, still blocked. > Cancel comment in A, timelines updates get unblocked... keep commenting in B... > Timeline autoupdate hits, you lose your comment. - Some memory leaks, which also cause the shutdown time to be very long. - Deleting person lists quickly, or creating a new one, and deleting without selecting, results in a crash. dianara-v1.1/CHANGELOG000644 000764 000764 00000016600 12264102231 013740 0ustar00janjan000000 000000 v1.1 - The Meanwhile feed now highlights activities related to you. There is a counter for new activities, which are also darker until clicked. - Button to open related posts from the Meanwhile feed. (Has issues, see pump.io issue https://github.com/e14n/pump.io/issues/873) - Ability to manage members of person lists. - Filters to block activities in the Meanwhile feed containing certain words, from certain users, or from certain applications (like OFG). - Button to get more (older) items in the Meanwhile feed. - The contact list gets all contacts now (previously limited to 200). - Own posts are no longer counted as new. - Some keyboard shortcuts have been hardcoded, so they should work under bare WMs, like OpenBox. Some new shortcuts have been added. - Better publisher layout. The option to select different publisher layouts has been removed. - Moved "Formatting" button out of the composer. - Different posts-per-page configurations for the main timeline and the rest. - Option to mark everything as read. - Show post's location, if there is one. - Account configuration will show automatically on the first run of the program. - The interface should be more responsive now while updating timelines. - Option to normalize post text colors temporarily. - Some data is stored diferently now, so a few things will reset on first use. - Lots of other small fixes and enhancements. v1.0 - Post editing. - Ability to create person lists, delete them, and post to specific lists. - Better posts resizing (no more splitters). - Optimize image sizes inside posts and comments. - Avatar upload in profile editor. - Author's avatar in posts is a button with several options. - Show if a post has been updated, and when. - Show "To" and "CC" recipients in posts as links. - Option to paste without formatting (as plain text). - Make proper links automatically when pasting a link-like text. - Option to quote comments. Selecting text in a post before clicking "Comment" also quotes it. - Regular notes can have titles. - Account wizard polishing. - Italian translation, by Metal Biker. - Some more text formatting options. Selecting "Normal" now clears colors, too. - Full screen option. - Partially fixed the ever-increasing memory usage issue. - Minor bugfixes and improvements. v0.9 - Image uploads with title and description. - Comment liking and unliking. - Ability to delete your own comments. - Show unread messages count in tray icon. - Contacts exporting. - New posts are marked as unread until clicked or timeline is updated again. - Don't clear and hide publisher or commenter until posting is confirmed, so you don't lose your post in case of network/server error. - Load images in comments. - Clearly show if "Public" or "Followers" is currently selected when posting. - Added a "Symbols" submenu under the "Formatting" menu in the composer. - Improved "Minor Feed". - Improved "Picture mode" in Publisher. - Lowered QJSON requirement to 0.7.x. - Lots of other minor bugfixes. v0.8 - Ability to select people in the "To/CC" fields when posting. - Option to set "Public" posting as default. - Better text formatting options. - Re-enabled HTML formatting when posting comments. - Profile editor. - Nicer and more informative "Meanwhile" column (tooltips!). - Basic "person lists" support. - Ability to save images from the image viewer (contextual menu). - Status bar can be hidden. - Reload each timeline when appropriate. - Better organization of internal configuration file. This will cause some options to be reset when upgrading from previous versions. - New icon. - Some bugfixes and minor improvements. v0.7 - Messages tab, showing posts specifically directed to you. - Activity tab, listing your own posts. - Favorites tab, listing the posts you've liked. - Show recipients of a post, the "To" and "CC" fields. - New options in formatting menu in the publisher: "preformatted block" and "insert image from web site". - Minor feed, a.k.a. "the meanwhile column". - Show "inserted images", in addition to the image in Picture-type posts. - Timeline reloads after posting. - Comments and likes are reloaded when commenting on or liking a post. - Better information on shared posts, showing the original author and who shared the post with you. - Autorefresh will not interrupt while commenting. - Show where links go when hovering them. - Confirmation when canceling a message if there's content in it. - Accept also https://server/username type of ID when entering an address to follow, in the contact list. - Ignore SSL errors, for now. - More tooltips everywhere! - Slightly better tray icon control. - Complete Catalan and Spanish translations. - Several other small fixes and optimizations. v0.6 - Ability to show all comments. - Show if you've liked a post, and ability to unlike it. - Show list of people who liked a post, in tooltip. - Full contact list: 'following' and 'followers'. - Image uploads. - Ability to select if a post goes to Public, Followers or both. - Clicking on posted images shows them in an internal viewer. - Link to open a post in the web browser. - Don't interrupt the user with timeline autoupdates if the timeline has been manually updated or browsed. - Other bugfixes and minor cleanups. v0.5 - Ability to like posts. - Show comments (only last 4). - Ability to post comments. - Ability to reshare posts. - Ability to delete posts. - Ability to follow people, by entering their address, and stop following. - Each user's avatar and name have their profile in the tooltip (on mouse-over). - Posts timestamp shows precise time and application used, in the tooltip. - Ability to go back and forward in the timeline. - Number of posts per page can be configured. - Main window's left and right panels can be resized. - Left panel can be hidden. - Status bar shows time of events (timeline updated, etc). - Popup notifications when receiving new posts on timeline update. - Several fixes and tooltips added. v0.4 - Pump.io release. - Initial transformation into a pump.io client. - Basic Dynamic Client Registration. - Initial OAuth-based authentication support. - Loading of user's own profile (avatar + real name). - Loading of newest 20 posts in timeline. - Text posting capabilities. - Partial contact listing. v0.3 - Aspect list items are links. - Posts now show uploaded photos too. - Limit maximum image size. - Posts are resizable now. - Plain links (just http://something, without markdown codes) are linkified too. - Thumbnails for embedded content (like Youtube videos) in posts. - #NSFW posts are hidden until clicked. - Fixes for Qt 5 compatibility. v0.2 - Frankenstein release. - Posting messages is possible, but *only* in pods using Pistos fork (which, in turn, don't support loading the timeline). - Aspect list is received (again, only in Pistos-based pods, for now). - Contact list is received (also for Pistos-based pods). - Publisher has a tool menu to add bold, italic, links, etc with Markdown. - Posts now show fuzzy time, like "about 3 hours ago", and Markdown-inserted images. - Initial "Messages" structure. v0.1 - Initial basic release. - Basic GUI structure in place. - System Tray icon. - FreeDesktop.org notifications. - Some configuration options. - Fetch your last 15 public posts, show basic info. - Partial Markdown support: bold, italic, headers, links. dianara-v1.1/Dianara.pro000664 000764 000764 00000006217 12260657136 014633 0ustar00janjan000000 000000 # Dianara - A pump.io client # Copyright 2012-2014 JanKusanagi # # 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 . #------------------------------------------------- # Project created by QtCreator #------------------------------------------------- QT += core gui network dbus greaterThan(QT_MAJOR_VERSION, 4) QT += widgets TARGET = dianara TEMPLATE = app SOURCES += src/main.cpp\ src/mainwindow.cpp \ src/configdialog.cpp \ src/notifications.cpp \ src/post.cpp \ src/timeline.cpp \ src/markdown.cpp \ src/publisher.cpp \ src/composer.cpp \ src/timestamp.cpp \ src/contactlist.cpp \ src/contactcard.cpp \ src/mischelpers.cpp \ src/pumpcontroller.cpp \ src/imageviewer.cpp \ src/minorfeed.cpp \ src/profileeditor.cpp \ src/accountdialog.cpp \ src/audienceselector.cpp \ src/commenterblock.cpp \ src/comment.cpp \ src/minorfeeditem.cpp \ src/listsmanager.cpp \ src/asobject.cpp \ src/asactivity.cpp \ src/filtereditor.cpp \ src/peoplewidget.cpp HEADERS += src/mainwindow.h \ src/configdialog.h \ src/notifications.h \ src/post.h \ src/timeline.h \ src/markdown.h \ src/publisher.h \ src/composer.h \ src/timestamp.h \ src/contactlist.h \ src/contactcard.h \ src/mischelpers.h \ src/pumpcontroller.h \ src/imageviewer.h \ src/minorfeed.h \ src/profileeditor.h \ src/accountdialog.h \ src/audienceselector.h \ src/commenterblock.h \ src/comment.h \ src/minorfeeditem.h \ src/listsmanager.h \ src/asobject.h \ src/asactivity.h \ src/filtereditor.h \ src/peoplewidget.h OTHER_FILES += \ CHANGELOG \ README \ dianara.desktop \ INSTALL \ TODO \ BUGS TRANSLATIONS += translations/dianara_es.ts \ translations/dianara_ca.ts \ translations/dianara_gl.ts \ translations/dianara_eu.ts \ translations/dianara_fr.ts \ translations/dianara_it.ts \ translations/dianara_de.ts \ translations/dianara_pt.ts \ translations/dianara_ru.ts \ translations/dianara_pl.ts RESOURCES += \ dianara.qrc LIBS += -lqjson -lqoauth -lqca # to use libQJSON # (and to use QOauth [+QCA] in Debian) CONFIG += oauth # to use QOAuth ## This is here so the makefile has a 'make install' target target.path = /usr/bin/ INSTALLS += target ## TMP/FIXME added for Debian (Debian's libqoauth-dev bug?) INCLUDEPATH += /usr/include/QtOAuth /usr/include/QtCrypto dianara-v1.1/INSTALL000664 000764 000764 00000003722 12260657135 013600 0ustar00janjan000000 000000 Dianara - A pump.io client Copyright 2012-2014 JanKusanagi ============================================================================== Compiling: From Dianara main directory, where Dianara.pro is located: mkdir build # create a clean directory for the build cd build # go into it qmake .. # ask Qmake to generate a Makefile make # run Make to compile the project (if you have the command 'qmake-qt4' available, use that instead of 'qmake') That should do it. There is an installation target, but you can just run the resulting "dianara" binary. General runtime dependencies (check carefully!): - Qt 4.7.x or 4.8.x (You can build with Qt 5.x, but it will probably crash) - QJSON 0.7.x or 0.8.x - QOAuth 1.0.x - QCA and its openSSL plugin (qca2-plugin-openssl, libqca2-plugin-ossl, or similar) (Dianara will _crash_ if you don't have this) Dependencies for building: You'll need Qmake and the qt-devel (>= 4.7, including QtNetwork and QtDBUS modules, if they are separate), and qjson-devel and qoauth-devel packages. Qmake might be included in the Qt development packages, or it might be a separate package. These are the names of the packages in some GNU/Linux distributions: - Debian (and probably in the derivatives too): libqt4-dev, libqjson-dev and libqoauth-dev. - Mageia (probably in Mandriva too) and openSuse: libqt4-devel, libqjson-devel and libqoauth-devel. You might also need to install qt4-qmake, if your distribution does not include it with the qt-devel package. The language files will be embedded into the binary upon compilation, so there's no need to keep them afterwards. Dianara is built on and for GNU/linux, but it will probably work under other systems, as long as they are supported by Qt, and have ports of the necessary dependencies. Visit http://jancoding.wordpress.com/dianara for more information. Get the latest source from http://gitorious.org/dianara dianara-v1.1/LICENSE000644 000764 000764 00000043254 11701354763 013556 0ustar00janjan000000 000000 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. dianara-v1.1/README000644 000764 000764 00000007150 12260657141 013421 0ustar00janjan000000 000000 Dianara - A pump.io client Copyright 2012-2014 JanKusanagi ============================================================================== 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. Or visit http://www.gnu.org/licenses/ ============================================================================== Dianara is a pump.io application for the desktop. With it, you can access your pump.io account without using a web browser. If you only got the source code, see the INSTALL file for instructions on how to build it. Dianara looks best under Plasma Desktop, preferably using a very complete iconset, like the Oxygen icons, but these are not requirements, just a recommendation. You can make the program look nicer in other environments by using 'qtconfig'. What it does ============================================================================== - Posting text (with some HTML formatting), to Followers, Public, person lists or specific people. - Uploading pictures. - Receiving different timelines, with configurable number of posts per page, and moving forward and backward in pages. - Liking, commenting, sharing, editing and deleting posts. - Liking and deleting comments. - Contact listing, following and unfollowing, either by entering their webfinger address or through buttons in the lists. - Editing your profile, changing your avatar. - Watch the 'minor feed' of activities, a.k.a. "the Meanwhile", and opening related posts from there. Some usage tips ============================================================================== - There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. This is specially useful in the "Meanwhile" column. - When publishing a note or posting a comment, you can send using the keyboard, by pressing Control+Enter. - You can click on the avatar of a post's author to get some options. - If a post is of "image" type, you can click on the image to see it full size. From the image viewer, you can save the image to disk by clicking with the secondary mouse button. - You can hide/show the side panel by pressing F9. Some items in the "Meanwhile" column have a "+" button to open the referenced post. - You can set public posting as default in the Configuration window. - Adding titles to your posts will make the "Meanwhile" feed a lot more useful. - Resizing the window as a column is recommended, even though you can use it maximized, or in full screen mode, of course. See the BUGS and TODO files for a list of known issues. Languages ============================================================================== Dianara is available in English, Catalan, Spanish and Italian languages. If you're interested in translating it to your language, let me know. Visit http://jancoding.wordpress.com/dianara for more information. Get the latest source from http://gitorious.org/dianara dianara-v1.1/TODO000644 000764 000764 00000002513 12264102244 013220 0ustar00janjan000000 000000 General to-do list and ideas for Dianara, in no particular order: ////// 1.2 - Timeline pagination The Right Way™, 'since/before'-based, not offset-based. Same for the MinorFeed. - Ability to search the contact list by partial name/webfinger. - Extend filters to be used in the timelines too. - Upload of other media types. - URL's favorited via hip2.it go to the Favorites timeline. Add checks for that. - Ability to 'unshare' a post. This does not work as one'd expect, however. It does not remove a shared post from your contacts' timelines. - Disable relevant parts of the UI when Dianara is not authorized to use an account. - Log window. - Save drafts. - Limit width of comments. - Fix loops while parsing HTML for tags. - pumplive.com stats "support". - Nickname autocompletion in post Publisher, which would also add the referenced user to the To or CC lists. - Handle server error 500 when following someone you already followed. - Support drag-and-drop in image uploads. - Notification of activities (after those work on the web UI, I guess). - Accept more parameters from CLI / DBUS interface. - Filter out shared posts which are visible more than once (if pump API does not implement it! There's an open issue about this) - Ofirehose support? - Spellcheck. - Contacts import from file? dianara-v1.1/dianara.desktop000644 000764 000764 00000002003 12233212031 015504 0ustar00janjan000000 000000 [Desktop Entry] Type=Application Name=Dianara GenericName=pump.io client GenericName[ca]=Client per pump.io GenericName[de]=pump.io client GenericName[es]=Cliente para pump.io GenericName[eu]=pump.io client GenericName[fr]=pump.io client GenericName[gl]=pump.io client GenericName[it]=Client per pump.io GenericName[pl]=pump.io client GenericName[pt]=pump.io client GenericName[ru]=pump.io client Comment=A pump.io social network client Comment[ca]=Un client per la xarxa social pump.io Comment[de]=A pump.io social network client Comment[es]=Un cliente para la red social pump.io Comment[eu]=A pump.io social network client Comment[fr]=A pump.io social network client Comment[gl]=A pump.io social network client Comment[it]=Un client per il social network pump.io Comment[pl]=A pump.io social network client Comment[pt]=A pump.io social network client Comment[ru]=A pump.io social network client Icon=dianara Exec=dianara Terminal=false StartupNotify=true Categories=Qt;Network; Keywords=pump.io;social network;microblog; dianara-v1.1/dianara.qrc000664 000764 000764 00000000651 12225616377 014657 0ustar00janjan000000 000000 images/no-avatar.png icon/64x64/dianara.png icon/32x32/dianara.png translations/dianara_ca.qm translations/dianara_es.qm translations/dianara_pl.qm images/image-loading.png translations/dianara_it.qm dianara-v1.1/icon/000755 000764 000764 00000000000 12166364170 013470 5ustar00janjan000000 000000 dianara-v1.1/icon/32x32/000775 000764 000764 00000000000 12166364201 014246 5ustar00janjan000000 000000 dianara-v1.1/icon/32x32/dianara.png000664 000764 000764 00000003253 12166366524 016370 0ustar00janjan000000 000000 PNG  IHDR szzrIDATXíˏWa{=H!8 @ ( HHAl"l!?$DĆ "AB`!{j6ufhFRw§8 ]E|.hK+V8Cs\*E0E\'" &h݋{_&9I1Z+'swX ; :]20 38peCUfV{wO!y}n;K|or"6/.XmK0 9#+zL0& b?ӹi={ob/hm4e5cxn}- $\r2I1܏8wJ|^%0;ڄl2tmL4IloFh&aUp'z<":y*kW6B&MfUO ^$hLJt;tk`qؑf03ˆ6ʏKb ,`8賿CgOƖ5Ջv lAP=߻ӬJފΗ&1\<*Zh9%3!E0 [)WnaZ3qEU{BeyOڪ]Ci&xH{lجG=u0pfɘ,, hm4Y{aK-^i=3~YF'|Yh cA? U)!&rނFjO-5~❪?Kc?Ba ~t&33bc8oUZhFj VHsv_Ze绠׹ua+P13>:{+DZz§[m^tTy&?M۸8% d+ 4!'B%j=b. Th~:vubNdLbNPO A>m;ؼR'gMvϖ`*{6n*Y ?쀼l<ظ G}~](cy%8'=&9H2\K̔3̂jx\%Dt8RkP0rϋ{1\"7 ;j4d U.iʋjL$-z j` ]| g- (b\=۫(O 'S9EI٦3<ҌQ:<)y/ %-sMҁD~+QI=ڿuy=IGO4ra:PԁS$P(DI2JPArV{$ 7 5]0!IqzQL%}2VUIENDB`dianara-v1.1/icon/64x64/000755 000764 000764 00000000000 12140335260 014251 5ustar00janjan000000 000000 dianara-v1.1/icon/64x64/dianara.png000664 000764 000764 00000012636 12166366451 016406 0ustar00janjan000000 000000 PNG  IHDR@@iqeIDATx{ieUZ9wz{5tmH6@Y!HB("NNB~E"pbNT <nwu <{+?|}F-WokpG~B_0 ,X @ʣ7A=wgaқC/BVX,|vd+DFPdxʼn^cf\ܼ V_>, V33VLUZ10Y$"Yi r&=ä m潘L&i˗ $c@ @!Ui,ـ󑠡ccL% rJWsJcOحWڙu?M3Edd0E'PrP(XD,8 APP@A y >Vee+$.l^q|qs FC$;)q4Θ j$ yf{VTuu % XeAfc#h8@#^ wGƁ9te e3kk'\@h?'``V$9DY uPm? vmYv/,U!D4 #HMg{\Cz!_xo,V[}_c>D4@V`u4ܼ*@ߡEBk |ڝRR# `'=uݯC$Dl Ret+ 5"}>Ǟaf4o<dUvbΊp8p@r;C'%]!^*bqSW~_J&WV|d(NIABEں]' bg{+_2BKD7%, |f7͕A#`v+AqTȬt<7pꀣFFi}~_~a|~ol?S1 daWg2v.#=AfS]t5)T^#?ʑdrR]$BzfQ!K&D )ǾS/+ҍ!!V?$kje n\{x=2TXVIE !A̳8)d1JtcDž "C'W}}3̶WbϪNIvtִCoၒٌ.ǃS]T(K*r!3N魄3;$ࣻ߰1X0Dt1Y˴Ve 5"I_rv2%:w0@nS]VWR0X0$8)gJvSIH߾e bœn SlWoP5)#vw}V,wa[ivk7BVV, ?ȑN ?xsɅ.n~}Gd9> P2/{SK|ڍѧ+dq-F}_ú蔹>^0\b>xcW*n6?6;(=C>mY XMPUCB ȄT i! I ˭:n!Ppn-?*([kp>ߍd;UezcxNVvt/6æv8ѠZ.u P 8"ģ8,,?ugJ{ ܃OOw3O=ҽ^젠O,z|ꂮqtHJ^a}Bf y 'kyíĈjrNP*e  E^͎yeϏ 'B`AnlA+8G "OX|jeQ9'י]BG6vXQ6B|?3b!,,+mYHq-kU'2M=;+|aq ;LܔYnJhQHu4(TQ>(d!7Bw^MT:13/_sn;sz )=!23!~*Ѷ8`~]:PIX%Ui ͗˅ms^i. 5}qu3 Lsxq|  { ;f C|@1dƐ)͊Pv ηӺxKO͹e>ްIp|-|µ 2A؇ߝ*ܕMhm}LY%*f`ӲFl>of\\~We([$Hǫr.k8݅c3l&KJkcMwdo3%wZ&o_ M奶kO^sZ Wn?,htBFo ܆Бغ`n O%lT,o|-V˩JO;EՕܮsF᛹d6]Us e,?d0Fk|X7I03GCSYd?L0gI 5+r[Y^_iPBEaߊlKtl`j]3ѧ}[5ILuW%4UO%M'o*ZԎn'P"i֭59]LW#4vD{*\i=pX?%4/?b[b.ӇjvT㻝KYdX3A]WI'OPhKgWIz@qFփ%`e^q];'u-,lSތGCr Lǐ`ݨWْoe5/g2}+޲ĦV\L+@nC:f'l6(b`CDgQ?Kw_!Ud=@1׹5Ӏ|F>4  ^|{#%j]58+ 4M.n We}yfTazX&7{Ke*˫ ECk@WQ .i ]ݦu@6FJեQ7/>~bzK9aDq_|&aQt#;u'3_@y.')q+`*~羠{-.nе+2EMkG]rՇ٨,7XpQ&M[Y wU2\0Q樳G8q% Y*}}Wdxz}JH+myr3⍫TK cs*q{tP^*Hl5,N Q閗)q*,8:ˇ;cn#u%ST.iT֤^dV8S=z$Oz]f"aU)䮪;( A,'=~N{kmX<`j'/䇗@M#0>U nfX]쀑"ЇJ0|Sxae& -uW”xԋBO2Y rKU)؄ nɶeLr?/'^>Ga?ܰte31ks_( վ7:|, ?1e5WKZ}U- {H0f(&x(B0Ȳi^(+KMq`1Gς{1L?uZşX ]/3KY!L^mv5t"`;l;_´sv` r| 2}@!b״fN5W5B cZ]QϐNz< /+kacU٣=yGL" oL=:<^uUƝ$qE;D!֋CuIR-_ojЩ_Cc2M?a"#iB,O0p34mF)Ǚ*#Gр8GܵBFB/<XiX)! |pve::rFDžF7Ci]jh}&HG9S4D0Uؾ2窼GO^5훒oΩ-oBY q#޴7/}GW7)~Z_%{VIՋ˅/lm諟O}A]n/aK M3A]  *:iFr-H?4y@'8 s_RĻH kGkYqTTgkic+P:jĖef\m٬)W&If@|*G1Pm"v׍ ܮ'*7&3t iQc7^8/V?m: VҺssM].)PM} ܻ벁1A V@ۡ3M&k]r 8ZpV 'nN}37Y7Ԫ|#Y)&bp@Bb"7 "rNq"kǀ8L.$B{b _ .)KDrk[˔!J:؁K*`o8y@LQ'-tCem\Q8q@fbܥ$B.QՙWu~^РX]Sޠ͉"&7StdYNC.bk,'J)!'Ō I{ M)m.H*0ԑ}H@}GgmI  BI{DsCMrEu!B;.n<1i?Zr~r$'@%* 8d%ѣ@o|}'=/?*wZsay|j@J1 >֫"|> ()s(+Pmg`2,lmB'@&tN- 2HonTvc6Mل,ma Bm%RBCTxв܁{`ҭ'ou a&$uXAl}}}uCFLfVq_gl:uh'vZ <sMXO :Zo<_+f ;&i\_,DsOo-fZ8cnqt nG23@7Y,;nG/n.%@,PMsX-rҮˀ_}Ѐ%л@/~ 6.3KkMpX 0 G{y]`:T56;M]ŕ@TQz'm)˞di j*MhLO3Z_] o6˙ӍfY_ #őbE>0ԲLE[o8֐% * f\~ yS O 8ZPeϦFױG(:;fq;@ShFmP6x)CU>Q:ntN)^B$>A:*t-#H4ޅJ3@'RpTofN)?X7} H~ +6)nT~pA\NzoF<md5,Skt+EaTߒ \?3/ .jBPZw=u,HOGʸs@9ހZ>d֋.fBJژ.+3IX7C :"|=A$K>'WӕGxUǫ#oRRz4+v("/XsgC~c߲-\CգZx,= ׄ߀F^Lnx[1H/}q8kx!/|J еǓ@@CiBO@{ζR}L 2yNb3mmp|LcjR[>"܃+|`Rт rQ0_6[}R[ <~h4pxƽDŽi~@Bήe'@8;{r7`v_o,u 'R eM~w:xnw*فPMg}2C!Z>܀c 1ɝCڃSTD{/th{ <5t4 dx U2qS<"S}cp \]*訪>gZIPBB JGAPP4A&E+"U(,T&^H/R.A$Hf޻֬<'egdm<8M2pV+NH!0:]TyMq#R4Tug1|hRS<<#{֓Ζ[(w2[;9ׯs&fbzK9QU.jt5r!mC=νX+0 7 qQ”F:2β'k Tc" íj OO AjPK<ĻsrG-^b9M]EKu-gO~0\)]k 4@.H=jT& ĺT%:d(PDic@Y ]@{`zǗ"/'ZR!VsU[{TܶR<[AGRFɝɩ ؁*lh?msΟ2 .Ii0JW7Zh<@drB#{V >5t0& `Gb a5hT4\sg&7TCqZ['ʃY&UxLh}y_&uٝD.ҵ߁8|l!22Aa*A|9Θ7 m4]W:Ѥϖi ?Ҋ&2SK՛^^JS|_ʱ]5>,IO*[r[1}W43qaSthi8CByX% ]o. ###[g;,Zׂ* v:(;(:H,:e sb$?@sh\PDK)um[IP wZ%]jMpY)Ou}A0;LgRf^`t5Qzr>R@ /1`+HOy`(L؉鮿 rJOyNG5M|oo^+->3x^/:mvFK+G|=۱3{4]T?Z(<i}b_?ZlN <]xWV\(昣ŽJYpd$bQ7tny@J+Y[U)s9@3\xyA|?k`R?)!\+V,Y90c1^EDHI) Qq"8cʃ{IIOC!{VHZH* oJS g8a0GLwmLnϵnAEqk)ܨYP!'[!OUύ ߂xkzFx=gROx^^YL6YʗYU=EavYs_yC_;TZ _ݺW1u§n黒0ZLI0͘oO#'[%7}BNA8w7F'5-H9yL/׻n]QS^5-*pk7U3Қ.@7P:O; ?p{x,[B'" #CྸjJ~vE{pkCgK9gtH9+édzg|"-8:>daYgQULߩJoj'+П*zzYݺ_?]}3^v4617e4K,xK-ՓL: LQDnî#.D\_R']כX ޫfgUֳzµTP( s [_j?9=-PzZ!yUl og8j Aí#!mQہpyP^B\];|kznXÁ&HG%柾&7-( U˲5>~{޹X俵F>;+\g{˱K_ E~HI 4M)0f"lԏ[g>4"'vcC_WX䏨{(jfB8١p驫@dd 79 ,KKU&^c|&f¦,8GuL3y/EL}< {W>=u*Hۿ=vU՞${ 03ԲDlvR_\G}i`P@M9U|6 6ܸtqʙzhIf/?r+w V IKq֫Vɓ#^?eKDzzt[V#6󅧶fQiKZͶB`<*M~[ǻsk6Ĉo"Wcoowu 쎞~^eP)v55z?f_{bCW!lpo l%H:OK>j,5>V LGyI}N=zxᮑiBވЬIG~Rϸtpw"Vx|f8cG BKȎf_}ଯ6]N7::7jwwױGv5.hwGvz'u;M |{k<:+^Lϲ?+m\ Rm؍+zyuޢ rDb3O7ʵm&P%o]=RE+T9:Z*2t  &˽8 *'m"% ko^¾˷uuztԫqcd$])7'Cגʃ+4gXu`oNdG>2Fܗy}9?goN. fUÕΌ[t蠟ywOz0'T<>sL+w_l;63h5_U#Np7ԛo닷ޅ`a5O&q}|- yeW_/r!nn6O!fڗe x&e8eH]vK))%HSD>GPrAp7/J}tF(8W'A;g2V,akLV>ZVj z 6<4]:H[jr8bs'`L?߱HE˗zzQ˛;a jsR'MTAEԽHfre‰:uy T=ekY5wAγ^{O>5e0f""1$ uM\#LPl(9gpK}Np`a*\ t?̖'\>ace)Yh젶s||S y8EGƂ<%׀81AZK CC_`$f8 j4 41~Z7ҷ| yOn6~ @2%AcT}!+𴒿MùVH8z'bd{Իum׮] 9:##c#E/R?Q,(,mϺd u5/$o& ,PNayBntOyTN2W$K54!mc)H'KQʹ`-Q = &$IUCxʑ)GCyǤ>9`f`]ܞL 9/6C>)I^RH 9\XN7tPԩo5K}/?|A0Mqb:Z}Y14c}dŮ9؊c6ǙK5mE#UD^&DVCLUS<`0I+qk/LaU3ɏt5+Y.Ly-(M%-Lea!zxߙgDqYcXL*ʕGd>֋rBoL6M yU%~$$[/K;_J7QSyD(Q-!{NC\Q?V ;CM(F ř_59b_^*Qc^zXkR*;jr, _QDDYzҲX1GQ<5&ꁨ늍|#}P&͢\ԄiQoGnN܉ԔOً=:o-!byqv)Vu3UqGNs wY*KdAja7_Q:/,锗M<1 fiٝ#Ly\69&FǒԠ:R; !e,b*g䔜2%YJI)S#HRT._ VCmYUt+];N5/^ףs[~NkcnYꗿ<_C6mooo Z7k-ow}ۗ+j/ePZ3m{7Px 5Q C`A9w10 6s*w/z>c"t9pjjz*Ն(2{.=?3 JɄiHZҕRAH 0 J`nB\c?ɻ\gޤ\S—C"hh`~Me{,uFGS)lK25'vZbfqFѡzO(#2Ȥ G-R@E^aem0O.c{f%mFF{J;skKeD12' $bsu} .WB |6sp$Ʉ`Y-_gu(G~r1g__oƜW3@*ٗ[]W(!dI'cܜYE DOVpJ-zR^\=,Ӡ2R@X\^gmaZH)96U3QJ77-soqJs@t>w,BB J5\mRʡDLn.7.\N%yS 6x-ZC>id2ՓIٴ݀jHi."Zkl2xu^|\fROn-$@ vgrKazG`[&nqa Ƕxalۤns} :]N|uRR*8}$d)%Mx26m!5TwQ̥>-R|ճ|tszJM\傇!>sZ3MbrD=s}p{,iv]zF^Cʸ_C~]NAQǟ0,WI'}_R3Ghu<͏0 L&~Pv7 ]G5&*}3@_)(go}V0)%n%? /=oI} H!g;y###$ +y aDQD`nac*_:sd=~mhfE~ˬ7\DWa1PJtجqML{ӣ .JdɤS$aF^ݿArԖU; 5QsA9b>ˡ2'+ضI%؜>2ʙɽZg' 5.qgacbHA׏h=6|?n XеR[Mahڞ)Mxn@>ԉ)c|WZLx?4g9Z?" Bf7rcZL*Roy{݌Z2֧BmŊ^)4!INͯqi>sHUVuڳ<r"0y.|:F\:lnD!fA/BoCáA:)q:!M{Mu;r5(brGίZ+.ߘә5HIUA`C"zD@!5ر۷#] 9_6w)^ @ևwXw{|x}R)xv¸4Vwv>˷suc ُzMCb[&Wgfv 6y6rֻw7y BrWx-QG"QDrl=JYrK10aCSl;\t48{j;sU~qN,q}x&mOqD /XlE۱QZ bB]*F"^,G( )@mhrh]FAHʕ,`v~5_},эy]vU)ym-9ut0Xhz8,Bn7;vrvIOLT8zdT/lwg㻜8C&bzz \>9V7yajo]zT.r@)~q:t#[?@JͦdR(|d;}ܒˆC}4 8qr1L31C k2 Or}=AC270đqLdxB+'e(3dil|()EL>C=Gl2drr'q$B)7`e˷9sB 8wE =?n2Z)2Qɳ"#cj"HCΤmd2t.v󈢘`򎕎 M&I&ɸN(J`&Q0 +Wg8r±\9ǭUBޕPĕ 4:/qø3 LJc&iJ%H䲙|0hgl-I8ؖcoR>}p}Un]Kw9sl|m8`8s ksikmZ]W6"M6MC ݜ(Q1 X LR.dhvxapk0BNo ݁!t.8XX`Q9h YrKY=t*#YКӣd2i$r0) Y]Q̧ Bz=`Hŕ㐰MP}fo9Hcfw(!䡄7Wd7Xz5PDPu3. y/`0 6ACXuq_66!hܧJsTw\v:?{ʃ`am0`S=|J4eћI Z#e<Z`JEӉ 4w9@ilv8-]r&DZC( 6u]*P22C ;T!Ry@V܎˳''{O;<1f(4ڝ+z*c#tbl$?\x-t&NZCJ9j;6cS]0C(*f{o01U)Q*0hhUs wϓJlxB)!~{Bk?i6XCX`}u\.ɑ]xO,dRޫgD;ՍZ<ǡWI |5Nץ٠!GM,ju仯bJchr +gXSgX߬l{#;{;mЖcWCj 5ڭn\E;9MgyqgNNqpcK,^70::Ju4I$lT5| mJMJPjYA[qV/ixo#{v *<k0C^8}v:S! v{ރ"_{E PlVJ!&< ͑+R2 aX^^凯H!ͰI:N 1RSg⧠`)y`+jPJ* w<]5)*q[a"F*E*({|_ۯ=7ԡ*z/+ NQ)Ahho7`&(ysQa;6|D)i4 ~s]%DT5WCW:/5|F73͐dI '"hM*†&Fw_\ .PP This manual page was written by JanKusanagi. dianara-v1.1/packaging/000755 000764 000764 00000000000 12233212031 014443 5ustar00janjan000000 000000 dianara-v1.1/packaging/mga-mdv-rpm/000755 000764 000764 00000000000 12202277507 016606 5ustar00janjan000000 000000 dianara-v1.1/packaging/mga-mdv-rpm/dianara.spec000644 000764 000764 00000002433 12202277507 021063 0ustar00janjan000000 000000 Summary: pump.io social network client Name: dianara Version: 0.9 Release: %mkrel 1 License: GPLv2+ Group: Networking/News URL: http://jancoding.wordpress.com/dianara/ Source0: http://qt-apps.org/CONTENT/content-files/148103-dianara-v%{version}.tar.gz BuildRequires: libqt4-devel BuildRequires: qjson-devel BuildRequires: libqoauth-devel BuildRequires: imagemagick Requires: qt4-common Requires: libqjson Requires: libqoauth Requires: qca2-plugin-openssl %description Dianara is a pump.io client, a desktop application for GNU/linux that allows users to manage their Pump.io social networking accounts without the need to use a web browser. %prep %setup -qn %{name}-v%{version} %build qmake %make %install %define apps %{_datadir}/applications/ %define pixmaps %{_datadir}/pixmaps/ %define locale %{_datadir}/%{name}/locale/ rm -rf %{buildroot} mkdir -p %{buildroot}%{_bindir}/ cp -p %{name} %{buildroot}%{_bindir}/ mkdir -p %{buildroot}%{apps} cp -p %{name}.desktop %{buildroot}%{apps} mkdir -p %{buildroot}%{pixmaps} cp -p icon/64x64/%{name}.png %{buildroot}%{pixmaps}%{name}.png mkdir -p %{buildroot}%{locale} cp -p translations/*.qm %{buildroot}%{locale} %files %doc CHANGELOG LICENSE README INSTALL BUGS TODO %{_bindir}/%{name} %{apps}%{name}.desktop %{pixmaps}%{name}.png %{locale}*.qm dianara-v1.1/packaging/fedora-rpm/000755 000764 000764 00000000000 12242761062 016514 5ustar00janjan000000 000000 dianara-v1.1/packaging/fedora-rpm/dianara.spec000644 000764 000764 00000002735 12242761062 020776 0ustar00janjan000000 000000 Name: dianara Version: 1.0 Release: 3%{?dist} Summary: Pump.io social network client License: GPLv2+ # Group: Networking/News URL: http://jancoding.wordpress.com/dianara/ Source0: http://qt-apps.org/CONTENT/content-files/148103-dianara-v%{version}.tar.gz BuildRequires: qt-devel BuildRequires: qjson-devel BuildRequires: qoauth-devel BuildRequires: ImageMagick Requires: qt Requires: qjson Requires: qoauth Requires: qca-ossl %description Dianara is a Pump.io client, a desktop application for GNU/Linux that allows users to manage their Pump.io social networking accounts without the need to use a web browser. %prep %setup -qn %{name}-v%{version} %build qmake-qt4 make %install %define apps %{_datadir}/applications/ %define pixmaps %{_datadir}/pixmaps/ %define locale %{_datadir}/%{name}/locale/ rm -rf %{buildroot} mkdir -p %{buildroot}%{_bindir}/ cp -p %{name} %{buildroot}%{_bindir}/ mkdir -p %{buildroot}%{apps} cp -p %{name}.desktop %{buildroot}%{apps} mkdir -p %{buildroot}%{pixmaps} cp -p icon/64x64/%{name}.png %{buildroot}%{pixmaps}%{name}.png mkdir -p %{buildroot}%{locale} cp -p translations/*.qm %{buildroot}%{locale} %files %doc CHANGELOG LICENSE README BUGS TODO %{_bindir}/%{name} %{apps}%{name}.desktop %{pixmaps}%{name}.png %{locale}*.qm %changelog * Wed Oct 30 2013 Silvio Amici 1.0-3 - Upgrade from beta2 to release, fixed i686 package problem * Thu Oct 24 2013 Silvio Amici 1.0-2 - Dianara 1.0-2 package creation dianara-v1.1/src/000755 000764 000764 00000000000 12264102321 013312 5ustar00janjan000000 000000 dianara-v1.1/src/main.cpp000664 000764 000764 00000005405 12264102307 014754 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include #include #include #include "mainwindow.h" #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) void customMessageHandlerQt4(QtMsgType type, const char *msg) { // do nothing Q_UNUSED(type) Q_UNUSED(msg) // FIXME, memory leak? return; } #else void customMessageHandlerQt5(QtMsgType type, const QMessageLogContext &context, const QString &msg) { Q_UNUSED(type) Q_UNUSED(context) Q_UNUSED(msg) // Do nothing return; } #endif int main(int argc, char *argv[]) { QApplication dianaraApp(argc, argv); dianaraApp.setApplicationName("Dianara"); dianaraApp.setApplicationVersion("1.1"); dianaraApp.setOrganizationName("JanCoding"); dianaraApp.setOrganizationDomain("jancoding.wordpress.com"); std::cout << QString("%1 v%2 - JanKusanagi 2012-2014\n") .arg(dianaraApp.applicationName()) .arg(dianaraApp.applicationVersion()).toStdString(); std::cout.flush(); // Register custom message handler, to hide debug messages unless specified if (qApp->arguments().contains("--debug", Qt::CaseInsensitive)) { qDebug() << "Debug messages enabled"; } else { qDebug() << "To see debug messages while running, use --debug"; #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) qInstallMsgHandler(customMessageHandlerQt4); #else qInstallMessageHandler(customMessageHandlerQt5); #endif } // Load translation files QTranslator translator; QString languageFile; // Generate resource name from LANG variable languageFile = QString(":/translations/dianara_%1") .arg(qgetenv("LANG").constData()); qDebug() << "Trying to load translation file:" << languageFile; translator.load(languageFile); dianaraApp.installTranslator(&translator); MainWindow dianaraWindow; dianaraWindow.show(); return dianaraApp.exec(); } dianara-v1.1/src/mainwindow.cpp000664 000764 000764 00000151477 12264102314 016215 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { this->setWindowTitle("Dianara"); this->setWindowIcon(QIcon(":/icon/64x64/dianara.png")); this->setMinimumSize(400, 400); QSettings settings; firstRun = true; prepareDataDirectory(); // This sets this->dataDirectory reallyQuitProgram = false; trayIconAvailable = false; QString currentIconset = QIcon::themeName(); qDebug() << "System iconset:" << currentIconset; qDebug() << "Icon theme search paths:" << QIcon::themeSearchPaths(); if (currentIconset.isEmpty() || currentIconset == "hicolor") { qDebug() << ">> No system iconset (or hicolor) configured; trying to use Oxygen"; QIcon::setThemeName("oxygen"); // VERY TMPFIX } // Network control pumpController = new PumpController(); ////// GUI // User's profile editor, in its own window profileEditor = new ProfileEditor(pumpController, this); // Splitter to divide the window horizontally mainSplitter = new QSplitter(Qt::Horizontal, this); mainSplitter->setChildrenCollapsible(false); mainSplitter->setContentsMargins(0, 0, 0, 0); mainSplitter->setHandleWidth(4); // Left side leftSideWidget = new QWidget(); leftLayout = new QVBoxLayout(); leftLayout->setContentsMargins(1, 1, 0, 1); avatarIconButton = new QPushButton(); avatarIconButton->setIcon(QIcon(QPixmap(":/images/no-avatar.png") .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation))); avatarIconButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); avatarIconButton->setFlat(true); avatarIconButton->setIconSize(QSize(64, 64)); connect(avatarIconButton, SIGNAL(clicked()), profileEditor, SLOT(show())); leftLayout->addWidget(avatarIconButton, 0, Qt::AlignLeft); userInfoLayout = new QVBoxLayout(); userInfoLayout->setContentsMargins(10, 0, 10, 0); fullNameLabel = new QLabel("[---------------]"); fullNameLabel->setWordWrap(true); userInfoLayout->addWidget(fullNameLabel); QFont userDetailsFont; userDetailsFont.setBold(true); userDetailsFont.setItalic(true); userDetailsFont.setPointSize(userDetailsFont.pointSize() - 2); userIdLabel = new QLabel(); userIdLabel->setWordWrap(true); userIdLabel->setFont(userDetailsFont); userInfoLayout->addWidget(userIdLabel); userDetailsFont.setBold(false); userDetailsFont.setItalic(false); userHometownLabel = new QLabel(); userHometownLabel->setWordWrap(true); userHometownLabel->setFont(userDetailsFont); userInfoLayout->addWidget(userHometownLabel); leftLayout->addLayout(userInfoLayout); leftPanel = new QToolBox(); // For now, only one "section"... meanwhileFeed = new MinorFeed(pumpController); connect(meanwhileFeed, SIGNAL(newItemsCountChanged(int)), this, SLOT(setMinorFeedTitle(int))); leftPanel->addItem(meanwhileFeed, QIcon::fromTheme("clock"), "*meanwhile*"); this->setMinorFeedTitle(0); // Set initial title leftLayout->addWidget(leftPanel); this->leftSideWidget->setLayout(leftLayout); // Right side rightSideWidget = new QWidget(); rightLayout = new QVBoxLayout(); rightLayout->setContentsMargins(0, 1, 1, 1); publisher = new Publisher(pumpController); /// START SETTING UP TIMELINES // Main timeline // mainTimeline = new TimeLine(TimeLine::TimelineTypeMain, pumpController, this); mainTimelineScrollArea = new QScrollArea(); mainTimelineScrollArea->setWidget(mainTimeline); // Make timeline scrollable mainTimelineScrollArea->setWidgetResizable(true); mainTimelineScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(mainTimeline, SIGNAL(scrollToTop()), this, SLOT(scrollMainTimelineToTop())); connect(mainTimeline, SIGNAL(timelineRendered(int,int)), this, SLOT(setTimelineTabTitle(int,int))); connect(mainTimeline, SIGNAL(unreadPostsCountChanged(int,int)), this, SLOT(setTimelineTabTitle(int,int))); connect(mainTimeline, SIGNAL(timelineRendered(int,int)), this, SLOT(notifyTimelineUpdate(int,int))); // for post editing connect(mainTimeline, SIGNAL(postEditRequested(QString,QString,QString)), publisher, SLOT(setEditingMode(QString,QString,QString))); // To ensure comment composer is visible connect(mainTimeline, SIGNAL(commentingOnPost(QWidget*)), this, SLOT(scrollMainTimelineToWidget(QWidget*))); // Direct timeline // directTimeline = new TimeLine(TimeLine::TimelineTypeDirect, pumpController, this); directTimelineScrollArea = new QScrollArea(); directTimelineScrollArea->setWidget(directTimeline); directTimelineScrollArea->setWidgetResizable(true); directTimelineScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(directTimeline, SIGNAL(scrollToTop()), this, SLOT(scrollDirectTimelineToTop())); connect(directTimeline, SIGNAL(timelineRendered(int,int)), this, SLOT(setTimelineTabTitle(int,int))); connect(directTimeline, SIGNAL(unreadPostsCountChanged(int,int)), this, SLOT(setTimelineTabTitle(int,int))); connect(directTimeline, SIGNAL(timelineRendered(int,int)), this, SLOT(notifyTimelineUpdate(int,int))); // direct timeline doesn't need a connection for post editing, // since you're not gonna send a message to yourself =) // To ensure comment composer is visible connect(directTimeline, SIGNAL(commentingOnPost(QWidget*)), this, SLOT(scrollDirectTimelineToWidget(QWidget*))); // Activity timeline // activityTimeline = new TimeLine(TimeLine::TimelineTypeActivity, pumpController, this); activityTimelineScrollArea = new QScrollArea(); activityTimelineScrollArea->setWidget(activityTimeline); // Make it scrollable activityTimelineScrollArea->setWidgetResizable(true); activityTimelineScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(activityTimeline, SIGNAL(scrollToTop()), this, SLOT(scrollActivityTimelineToTop())); connect(activityTimeline, SIGNAL(timelineRendered(int,int)), this, SLOT(setTimelineTabTitle(int,int))); connect(activityTimeline, SIGNAL(unreadPostsCountChanged(int,int)), this, SLOT(setTimelineTabTitle(int,int))); connect(activityTimeline, SIGNAL(timelineRendered(int,int)), this, SLOT(notifyTimelineUpdate(int,int))); // for post editing connect(activityTimeline, SIGNAL(postEditRequested(QString,QString,QString)), publisher, SLOT(setEditingMode(QString,QString,QString))); // To ensure comment composer is visible connect(activityTimeline, SIGNAL(commentingOnPost(QWidget*)), this, SLOT(scrollActivityTimelineToWidget(QWidget*))); // Favorites timeline // favoritesTimeline = new TimeLine(TimeLine::TimelineTypeFavorites, pumpController, this); favoritesTimelineScrollArea = new QScrollArea(); favoritesTimelineScrollArea->setWidget(favoritesTimeline); favoritesTimelineScrollArea->setWidgetResizable(true); favoritesTimelineScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(favoritesTimeline, SIGNAL(scrollToTop()), this, SLOT(scrollFavoritesTimelineToTop())); connect(favoritesTimeline, SIGNAL(timelineRendered(int,int)), this, SLOT(setTimelineTabTitle(int,int))); connect(favoritesTimeline, SIGNAL(unreadPostsCountChanged(int,int)), this, SLOT(setTimelineTabTitle(int,int))); connect(favoritesTimeline, SIGNAL(timelineRendered(int,int)), this, SLOT(notifyTimelineUpdate(int,int))); // for post editing connect(favoritesTimeline, SIGNAL(postEditRequested(QString,QString,QString)), publisher, SLOT(setEditingMode(QString,QString,QString))); // To ensure comment composer is visible connect(favoritesTimeline, SIGNAL(commentingOnPost(QWidget*)), this, SLOT(scrollFavoritesTimelineToWidget(QWidget*))); /// END SETTING UP TIMELINES // The contact list has its own tabs with its own scroll areas contactList = new ContactList(pumpController, this); tabWidget = new QTabWidget(); tabWidget->addTab(mainTimelineScrollArea, QIcon::fromTheme("view-list-details"), "MAIN TIMELINE TAB"); tabWidget->setTabToolTip(0, tr("The main timeline")); this->setTimelineTabTitle(TimeLine::TimelineTypeMain, 0); tabWidget->addTab(directTimelineScrollArea, QIcon::fromTheme("mail-message"), "MESSAGES TAB"); tabWidget->setTabToolTip(1, tr("Messages sent explicitly to you")); this->setTimelineTabTitle(TimeLine::TimelineTypeDirect, 0); tabWidget->addTab(activityTimelineScrollArea, QIcon::fromTheme("user-home"), "ACTIVITY TAB"); tabWidget->setTabToolTip(2, tr("Your own posts")); this->setTimelineTabTitle(TimeLine::TimelineTypeActivity, 0); tabWidget->addTab(favoritesTimelineScrollArea, QIcon::fromTheme("folder-favorites"), "FAVORITES TAB"); tabWidget->setTabToolTip(3, tr("Your favorited posts")); this->setTimelineTabTitle(TimeLine::TimelineTypeFavorites, 0); tabWidget->addTab(contactList, QIcon::fromTheme("system-users"), tr("&Contacts")); tabWidget->setTabToolTip(4, tr("The people you follow, and the ones who follow you")); connect(tabWidget, SIGNAL(currentChanged(int)), this, SLOT(setTitleAndTrayInfo(int))); rightLayout->addWidget(publisher, 1); // stretch 1/10 rightLayout->addWidget(tabWidget, 9); // stretch 9/10 this->rightSideWidget->setLayout(rightLayout); mainSplitter->addWidget(leftSideWidget); mainSplitter->addWidget(rightSideWidget); this->setCentralWidget(mainSplitter); ////////////////// Load configuration from disk loadSettings(); // FreeDesktop.org notifications handler fdNotifier = new FDNotifications(); fdNotifier->setNotificationType(showNotifications); // valid since loadSettings() // was just called connect(fdNotifier, SIGNAL(showFallbackNotification(QString)), this, SLOT(showTrayFallbackMessage(QString))); updateTimer = new QTimer(this); updateTimer->setInterval(this->updateInterval * 1000 * 60); // min > msec connect(updateTimer, SIGNAL(timeout()), this, SLOT(updateMainDirectMinorTimelines())); updateTimer->start(); //// External widgets which live in their own windows // Account wizard accountDialog = new AccountDialog(pumpController, this); connect(accountDialog, SIGNAL(userIDChanged(QString)), this, SLOT(updateUserID(QString))); // If this is the first run, show the account Dialog if (firstRun) { accountDialog->show(); } // Configuration dialog configDialog = new ConfigDialog(this->dataDirectory, this->updateInterval, this->postsPerPageMain, this->postsPerPageOther, this->tabsPosition, this->tabsMovable, this->publicPosts, this->showNotifications, this); connect(configDialog, SIGNAL(configurationChanged(int,int,int,int,bool,bool,int)), this, SLOT(updateSettings(int,int,int,int,bool,bool,int))); // Filter editor filterEditor = new FilterEditor(pumpController, this); /// //////////////////////////////// Connections for PumpController ////////// /// connect(pumpController, SIGNAL(profileReceived(QString,QString,QString,QString,QString)), this, SLOT(updateProfileData(QString,QString,QString,QString,QString))); connect(pumpController, SIGNAL(avatarPictureReceived(QByteArray,QUrl)), this, SLOT(storeAvatar(QByteArray,QUrl))); connect(pumpController, SIGNAL(imageReceived(QByteArray,QUrl)), this, SLOT(storeImage(QByteArray,QUrl))); // After receiving timeline contents, update corresponding timeline connect(pumpController, SIGNAL(mainTimelineReceived(QVariantList,int,QString,QString)), mainTimeline, SLOT(setTimeLineContents(QVariantList,int,QString,QString))); connect(pumpController, SIGNAL(directTimelineReceived(QVariantList,int,QString,QString)), directTimeline, SLOT(setTimeLineContents(QVariantList,int,QString,QString))); connect(pumpController, SIGNAL(activityTimelineReceived(QVariantList,int,QString,QString)), activityTimeline, SLOT(setTimeLineContents(QVariantList,int,QString,QString))); connect(pumpController, SIGNAL(favoritesTimelineReceived(QVariantList,int,QString,QString)), favoritesTimeline, SLOT(setTimeLineContents(QVariantList,int,QString,QString))); // After sucessful posting, request updated timeline, with your post included connect(pumpController, SIGNAL(postPublished()), this, SLOT(updateMainActivityMinorTimelines())); // After successful liking, update likes count connect(pumpController, SIGNAL(likesReceived(QVariantList,QString)), mainTimeline, SLOT(setLikesInPost(QVariantList,QString))); connect(pumpController, SIGNAL(likesReceived(QVariantList,QString)), directTimeline, SLOT(setLikesInPost(QVariantList,QString))); connect(pumpController, SIGNAL(likesReceived(QVariantList,QString)), activityTimeline, SLOT(setLikesInPost(QVariantList,QString))); // We don't update likes count in favorites timeline, // since it still doesn't know about this post // Instead, reload favorites timeline connect(pumpController, SIGNAL(likesReceived(QVariantList,QString)), favoritesTimeline, SLOT(goToFirstPage())); // After commenting successfully, refresh list of comments in that post connect(pumpController, SIGNAL(commentsReceived(QVariantList,QString)), mainTimeline, SLOT(setCommentsInPost(QVariantList,QString))); connect(pumpController, SIGNAL(commentsReceived(QVariantList,QString)), directTimeline, SLOT(setCommentsInPost(QVariantList,QString))); connect(pumpController, SIGNAL(commentsReceived(QVariantList,QString)), activityTimeline, SLOT(setCommentsInPost(QVariantList,QString))); connect(pumpController, SIGNAL(commentsReceived(QVariantList,QString)), favoritesTimeline, SLOT(setCommentsInPost(QVariantList,QString))); // After successful sharing.... // TODO*** // After receiving the minor feed ("Meanwhile"), update it connect(pumpController, SIGNAL(minorFeedReceived(QVariantList)), meanwhileFeed, SLOT(setFeedContents(QVariantList))); // After receiving a contact list, update it connect(pumpController, SIGNAL(contactListReceived(QString,QVariantList,int)), contactList, SLOT(setContactListContents(QString,QVariantList,int))); // After receiving the list of lists, update it connect(pumpController, SIGNAL(listsListReceived(QVariantList)), contactList, SLOT(setListsListContents(QVariantList))); // Show notifications for events sent from pumpController connect(pumpController, SIGNAL(showNotification(QString)), fdNotifier, SLOT(showMessage(QString))); // Update statusBar message from pumpController's infos connect(pumpController, SIGNAL(currentJobChanged(QString)), this, SLOT(setStatusBarMessage(QString))); // Add menus createMenus(); // Add the system tray icon createTrayIcon(); // tmp statusBar stuff this->statusBar()->showMessage(tr("Initializing...")); settings.beginGroup("MainWindow"); // Now set the "view side panel" checkable menu to its saved state // That will also trigger the action to hide/show it viewSidePanel->setChecked(settings.value("viewSidePanel", true).toBool()); // Set the "view status bar" checkable menu to its saved state, which // will also trigger the action to hide/show it viewStatusBar->setChecked(settings.value("viewStatusBar", true).toBool()); settings.endGroup(); // If User ID is defined, set PumpController in motion if (!userID.isEmpty()) { pumpController->setUserCredentials(this->userID); // getUserProfile() will be called from setUserCredentials() } else // Otherwise, just say so in the statusbar { this->setStatusBarMessage(tr("Your account is not configured yet.")); } this->adjustTimelineSizes(); qApp->processEvents(); qDebug() << "MainWindow created"; } MainWindow::~MainWindow() { qDebug() << "MainWindow destroyed"; } void MainWindow::closeEvent(QCloseEvent *event) { qDebug() << "MainWindow::closeEvent()"; if (reallyQuitProgram) { event->accept(); // really close, if called from Quit menu qDebug() << "Quit called from menu, shutting down program"; } else { this->hide(); // Hide window, app accessible via tray icon qDebug() << "Tried to close main window, so hiding to tray"; event->ignore(); // ignore the closeEvent } } void MainWindow::resizeEvent(QResizeEvent *event) { qDebug() << "MainWindow::resizeEvent()" << event->size(); this->adjustTimelineSizes(); event->accept(); } /* * Prepare the data directory. Create if necessary * */ void MainWindow::prepareDataDirectory() { #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) dataDirectory = QDesktopServices::storageLocation(QDesktopServices::DataLocation); #else dataDirectory = QStandardPaths::standardLocations(QStandardPaths::DataLocation).first(); #endif qDebug() << "Data directory:" << this->dataDirectory; QDir dataDir; if (!dataDir.exists(dataDirectory)) { qDebug() << "Creating data directory"; if (dataDir.mkpath(dataDirectory)) { qDebug() << "Data directory created"; } else { qDebug() << "Error creating data directory!"; } } if (!dataDir.exists(dataDirectory + "/images")) { qDebug() << "Creating images directory"; if (dataDir.mkpath(dataDirectory + "/images")) { qDebug() << "Images directory created"; } else { qDebug() << "Error creating images directory!"; } } if (!dataDir.exists(dataDirectory + "/avatars")) { qDebug() << "Creating avatars directory"; if (dataDir.mkpath(dataDirectory + "/avatars")) { qDebug() << "Avatars directory created"; } else { qDebug() << "Error creating avatars directory!"; } } } /* * Populate the menus * */ void MainWindow::createMenus() { sessionMenu = new QMenu(tr("&Session")); sessionUpdateMainTimeline = new QAction(QIcon::fromTheme("view-refresh"), tr("&Update Main Timeline"), this); sessionUpdateMainTimeline->setShortcut(QKeySequence(Qt::Key_F5)); connect(sessionUpdateMainTimeline, SIGNAL(triggered()), mainTimeline, SLOT(goToFirstPage())); sessionMenu->addAction(sessionUpdateMainTimeline); sessionUpdateDirectTimeline = new QAction(QIcon::fromTheme("view-refresh"), tr("Update &Messages Timeline"), this); sessionUpdateDirectTimeline->setShortcut(QKeySequence(Qt::Key_F6)); connect(sessionUpdateDirectTimeline, SIGNAL(triggered()), directTimeline, SLOT(goToFirstPage())); sessionMenu->addAction(sessionUpdateDirectTimeline); sessionUpdateActivityTimeline = new QAction(QIcon::fromTheme("view-refresh"), tr("Update &Activity Timeline"), this); sessionUpdateActivityTimeline->setShortcut(QKeySequence(Qt::Key_F7)); connect(sessionUpdateActivityTimeline, SIGNAL(triggered()), activityTimeline, SLOT(goToFirstPage())); sessionMenu->addAction(sessionUpdateActivityTimeline); sessionUpdateFavoritesTimeline = new QAction(QIcon::fromTheme("view-refresh"), tr("Update Favorites &Timeline"), this); sessionUpdateFavoritesTimeline->setShortcut(QKeySequence(Qt::Key_F8)); connect(sessionUpdateFavoritesTimeline, SIGNAL(triggered()), favoritesTimeline, SLOT(goToFirstPage())); sessionMenu->addAction(sessionUpdateFavoritesTimeline); sessionUpdateMinorFeed = new QAction(QIcon::fromTheme("view-refresh"), tr("Update Minor &Feed"), this); sessionUpdateMinorFeed->setShortcut(QKeySequence(Qt::Key_F10)); connect(sessionUpdateMinorFeed, SIGNAL(triggered()), meanwhileFeed, SLOT(updateFeed())); sessionMenu->addAction(sessionUpdateMinorFeed); sessionUpdateAllTimelines = new QAction(QIcon::fromTheme("view-refresh"), tr("Update All Timelines"), this); sessionUpdateAllTimelines->setShortcut(QKeySequence("Ctrl+F5")); connect(sessionUpdateAllTimelines, SIGNAL(triggered()), this, SLOT(updateAllTimelines())); sessionMenu->addAction(sessionUpdateAllTimelines); sessionMenu->addSeparator(); sessionMarkAllAsRead = new QAction(QIcon::fromTheme("mail-mark-read"), tr("Mark All as Read"), this); sessionMarkAllAsRead->setShortcut(QKeySequence("Ctrl+R")); connect(sessionMarkAllAsRead, SIGNAL(triggered()), this, SLOT(markAllAsRead())); sessionMenu->addAction(sessionMarkAllAsRead); sessionMenu->addSeparator(); sessionPostNote = new QAction(QIcon::fromTheme("document-edit"), tr("&Post a Note"), this); sessionPostNote->setShortcut(QKeySequence("Ctrl+N")); connect(sessionPostNote, SIGNAL(triggered()), publisher, SLOT(setFullMode())); connect(sessionPostNote, SIGNAL(triggered()), this, SLOT(show())); // show window, in case it's hidden and // Post a Note is used from tray menu sessionMenu->addAction(sessionPostNote); sessionMenu->addSeparator(); sessionQuit = new QAction(QIcon::fromTheme("application-exit"), tr("&Quit"), this); sessionQuit->setShortcut(QKeySequence("Ctrl+Q")); connect(sessionQuit, SIGNAL(triggered()), this, SLOT(quitProgram())); sessionMenu->addAction(sessionQuit); this->menuBar()->addMenu(sessionMenu); viewMenu = new QMenu(tr("&View")); viewSidePanel = new QAction(QIcon::fromTheme("view-sidetree"), tr("Side &Panel"), this); connect(viewSidePanel, SIGNAL(toggled(bool)), this, SLOT(toggleSidePanel(bool))); viewSidePanel->setCheckable(true); viewSidePanel->setChecked(true); viewSidePanel->setShortcut(Qt::Key_F9); viewMenu->addAction(viewSidePanel); viewStatusBar = new QAction(QIcon::fromTheme("configure-toolbars"), tr("Status &Bar"), this); connect(viewStatusBar, SIGNAL(toggled(bool)), this, SLOT(toggleStatusBar(bool))); viewStatusBar->setCheckable(true); viewStatusBar->setChecked(true); viewMenu->addAction(viewStatusBar); viewFullscreenAction = new QAction(QIcon::fromTheme("view-fullscreen"), tr("Full &Screen"), this); connect(viewFullscreenAction, SIGNAL(toggled(bool)), this, SLOT(toggleFullscreen(bool))); viewFullscreenAction->setCheckable(true); viewFullscreenAction->setChecked(false); viewFullscreenAction->setShortcut(Qt::Key_F11); viewMenu->addAction(viewFullscreenAction); this->menuBar()->addMenu(viewMenu); settingsMenu = new QMenu(tr("S&ettings")); settingsEditProfile = new QAction(QIcon::fromTheme("user-properties"), tr("Edit &Profile"), this); connect(settingsEditProfile, SIGNAL(triggered()), profileEditor, SLOT(show())); settingsMenu->addAction(settingsEditProfile); settingsAccount = new QAction(QIcon::fromTheme("dialog-password"), tr("&Account"), this); connect(settingsAccount, SIGNAL(triggered()), accountDialog, SLOT(show())); settingsMenu->addAction(settingsAccount); settingsMenu->addSeparator(); settingsFilters = new QAction(QIcon::fromTheme("view-filter"), tr("&Filters"), this); connect(settingsFilters, SIGNAL(triggered()), filterEditor, SLOT(show())); settingsMenu->addAction(settingsFilters); settingsConfigure = new QAction(QIcon::fromTheme("configure"), tr("&Configure Dianara"), this); connect(settingsConfigure, SIGNAL(triggered()), configDialog, SLOT(show())); settingsMenu->addAction(settingsConfigure); this->menuBar()->addMenu(settingsMenu); this->menuBar()->addSeparator(); helpMenu = new QMenu(tr("&Help")); helpVisitWebsite = new QAction(QIcon::fromTheme("internet-web-browser"), tr("Visit &Website"), this); connect(helpVisitWebsite, SIGNAL(triggered()), this, SLOT(visitWebSite())); helpMenu->addAction(helpVisitWebsite); helpVisitPumpFAQ = new QAction(QIcon::fromTheme("internet-web-browser"), tr("&Frequently Asked Questions about Pump.io"), this); connect(helpVisitPumpFAQ, SIGNAL(triggered()), this, SLOT(visitFAQ())); helpMenu->addAction(helpVisitPumpFAQ); helpMenu->addSeparator(); helpAbout = new QAction(QIcon::fromTheme("system-help"), tr("About &Dianara"), this); connect(helpAbout, SIGNAL(triggered()), this, SLOT(aboutDianara())); helpMenu->addAction(helpAbout); this->menuBar()->addMenu(helpMenu); // Context menu for the tray icon trayContextMenu = new QMenu("Tray Context Menu"); trayContextMenu->setSeparatorsCollapsible(false); trayContextMenu->addSeparator()->setText("Dianara"); trayContextMenu->addAction(QIcon(":/icon/64x64/dianara.png"), tr("&Show Window"), this, SLOT(show())); trayContextMenu->addSeparator(); trayContextMenu->addAction(sessionUpdateMainTimeline); trayContextMenu->addAction(sessionMarkAllAsRead); trayContextMenu->addAction(sessionPostNote); trayContextMenu->addSeparator(); trayContextMenu->addAction(settingsConfigure); trayContextMenu->addAction(helpAbout); trayContextMenu->addSeparator(); trayContextMenu->addAction(sessionQuit); // FIXME: if mainwindow is hidden, program quits // after closing Configure or About window (now partially fixed) qDebug() << "Menus created"; } /* * Create an icon in the system tray, define its contextual menu, etc. * */ void MainWindow::createTrayIcon() { trayIcon = new QSystemTrayIcon(this); if (trayIcon->isSystemTrayAvailable()) { trayIconAvailable = true; this->setTrayIconPixmap(); // Set icon for "no unread messages" initially this->setTitleAndTrayInfo(this->tabWidget->currentIndex()); // Catch clicks on icon connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(trayControl(QSystemTrayIcon::ActivationReason))); // clicking in a popup notification (balloon-type) will show the window connect(trayIcon, SIGNAL(messageClicked()), this, SLOT(show())); // Set contextual menu for the icon trayIcon->setContextMenu(this->trayContextMenu); trayIcon->show(); qDebug() << "Tray icon created"; } else { trayIconAvailable = false; qDebug() << "System tray not available"; } } /* * Set the tray icon's pixmap, with number of unread messages, or nothing * */ void MainWindow::setTrayIconPixmap(int count) { QPixmap iconPixmap = QIcon::fromTheme("dianara", QIcon(":/icon/32x32/dianara.png")).pixmap(32, 32); // Paint the number of unread messages on top of the pixmap, if != 0 if (count > 0) { QFont font; font.setPointSize(font.pointSize() + 1); font.setBold(true); QPainter painter(&iconPixmap); painter.setPen(QColor(Qt::white)); painter.setFont(font); painter.drawText(0, 0, 30, 30, // Not 32, 32, to have some margin Qt::AlignRight | Qt::AlignBottom, QString("%1").arg(count)); } this->trayIcon->setIcon(iconPixmap); } /* * Adjust timelines maximum widths according to their scrollareas sizes, * which in turn depend on the window size * * Called from the resizeEvent(), among other places * */ void MainWindow::adjustTimelineSizes() { int scrollbarWidth = mainTimelineScrollArea->verticalScrollBar()->width() + 4; // FIXME: adding +4 pixels should not be hardcoded int timelineWidth; int timelineHeight; // Get the right timelinewidth based on currently active tab's timeline width switch (this->tabWidget->currentIndex()) { case 0: // main timelineWidth = mainTimelineScrollArea->width() - scrollbarWidth; timelineHeight = mainTimelineScrollArea->height(); mainTimeline->resizePosts(); break; case 1: // direct timelineWidth = directTimelineScrollArea->width() - scrollbarWidth; timelineHeight = directTimelineScrollArea->height(); directTimeline->resizePosts(); break; case 2: // activity timelineWidth = activityTimelineScrollArea->width() - scrollbarWidth; timelineHeight = activityTimelineScrollArea->height(); activityTimeline->resizePosts(); break; case 3: // favorites timelineWidth = favoritesTimelineScrollArea->width() - scrollbarWidth; timelineHeight = favoritesTimelineScrollArea->height(); favoritesTimeline->resizePosts(); break; default: // Contacts tab, ATM timelineWidth = contactList->width() - scrollbarWidth; timelineHeight = contactList->height(); } // Then set the maximum width for all of them based on that mainTimeline->setMaximumWidth(timelineWidth); directTimeline->setMaximumWidth(timelineWidth); activityTimeline->setMaximumWidth(timelineWidth); favoritesTimeline->setMaximumWidth(timelineWidth); // Some basic 1:2 proportions TMP FIXME if (timelineHeight > (timelineWidth * 2)) { timelineHeight = timelineWidth * 2; } mainTimeline->setMinMaxHeightForPosts(timelineHeight); directTimeline->setMinMaxHeightForPosts(timelineHeight); activityTimeline->setMinMaxHeightForPosts(timelineHeight); favoritesTimeline->setMinMaxHeightForPosts(timelineHeight); } /* * Load general program settings and state: size, position... * */ void MainWindow::loadSettings() { QSettings settings; firstRun = settings.value("firstRun", true).toBool(); if (firstRun) { qDebug() << "This is the first run"; } userID = settings.value("userID", "").toString(); this->setTitleAndTrayInfo(tabWidget->currentIndex()); // Main window state settings.beginGroup("MainWindow"); this->resize(settings.value("windowSize", QSize(680, 560)).toSize()); if (!firstRun) { this->move(settings.value("windowPosition").toPoint()); } this->mainSplitter->restoreState(settings.value("mainSplitterState", mainSplitter->saveState()).toByteArray()); // Set childrenCollapsible to false AFTER loading state, to make sure state does not mess with it mainSplitter->setChildrenCollapsible(false); settings.endGroup(); // General program configuration settings.beginGroup("Configuration"); this->updateInterval = settings.value("updateInterval", 5).toInt(); this->postsPerPageMain = settings.value("postsPerPageMain", 20).toInt(); this->pumpController->setPostsPerPageMain(this->postsPerPageMain); this->postsPerPageOther = settings.value("postsPerPageOther", 5).toInt(); this->pumpController->setPostsPerPageOther(this->postsPerPageOther); this->tabsPosition = settings.value("tabsPosition", QTabWidget::North).toInt(); tabWidget->setTabPosition((QTabWidget::TabPosition)tabsPosition); this->tabsMovable = settings.value("tabsMovable", true).toBool(); tabWidget->setMovable(tabsMovable); this->publicPosts = settings.value("publicPosts", false).toBool(); this->publisher->setDefaultPublicPosting(this->publicPosts); this->showNotifications = settings.value("showNotifications", 0).toInt(); settings.endGroup(); qDebug() << "Settings loaded"; } /* * Save general program settings and state: size, position... * */ void MainWindow::saveSettings() { QSettings settings; settings.setValue("firstRun", false); // General main window status settings.beginGroup("MainWindow"); settings.setValue("windowSize", this->size()); settings.setValue("windowPosition", this->pos()); settings.setValue("mainSplitterState", this->mainSplitter->saveState()); settings.setValue("viewSidePanel", this->viewSidePanel->isChecked()); settings.setValue("viewStatusBar", this->viewStatusBar->isChecked()); settings.endGroup(); // From config dialog settings.beginGroup("Configuration"); settings.setValue("updateInterval", this->updateInterval); settings.setValue("postsPerPageMain", this->postsPerPageMain); settings.setValue("postsPerPageOther", this->postsPerPageOther); settings.setValue("tabsPosition", this->tabsPosition); settings.setValue("tabsMovable", this->tabsMovable); settings.setValue("publicPosts", this->publicPosts); settings.setValue("showNotifications", this->showNotifications); settings.endGroup(); qDebug() << "Settings saved"; } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////////// SLOTS //////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// /* * Update UserID string from signal emitted in AccountDialog * */ void MainWindow::updateUserID(QString newUserID) { this->userID = newUserID; // update window title and tray icon tooltip this->setTitleAndTrayInfo(tabWidget->currentIndex()); this->pumpController->setUserCredentials(userID); // Remove current user's name, id and avatar this->fullNameLabel->setText("--"); this->userIdLabel->setText("_@_"); this->userHometownLabel->setText("--"); avatarIconButton->setIcon(QIcon(QPixmap(":/images/no-avatar.png") .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation))); qDebug() << "UserID updated from AccountDialog:" << userID; } /* * Update settings changed from ConfigDialog() * */ void MainWindow::updateSettings(int newUpdateInterval, int newPostsPerPageMain, int newPostsPerPageOther, int newTabsPosition,bool newTabsMovable, bool newPublicPosts, int newShowNotifications) { this->updateInterval = newUpdateInterval; this->updateTimer->setInterval(updateInterval * 1000 * 60); this->postsPerPageMain = newPostsPerPageMain; this->pumpController->setPostsPerPageMain(this->postsPerPageMain); this->postsPerPageOther = newPostsPerPageOther; this->pumpController->setPostsPerPageOther(this->postsPerPageOther); this->tabsPosition = newTabsPosition; this->tabWidget->setTabPosition((QTabWidget::TabPosition)tabsPosition); this->tabsMovable = newTabsMovable; this->tabWidget->setMovable(tabsMovable); this->publicPosts = newPublicPosts; this->publisher->setDefaultPublicPosting(this->publicPosts); this->showNotifications = newShowNotifications; fdNotifier->setNotificationType(showNotifications); // FIXME: Safe? qDebug() << "updateInterval updated:" << updateInterval << updateInterval*60000; qDebug() << "postsPerPage Main/Other:" << postsPerPageMain << postsPerPageOther; qDebug() << "tabsPosition updated:" << tabsPosition << tabWidget->tabPosition(); qDebug() << "tabsMovable updated:" << tabsMovable; qDebug() << "public posts updated:" << publicPosts; qDebug() << "Notifications updated:" << showNotifications; } /* * Control interaction with the system tray icon * */ void MainWindow::trayControl(QSystemTrayIcon::ActivationReason reason) { qDebug() << "Tray icon activation reason:" << reason; if (reason != QSystemTrayIcon::Context) // Simple "main button" click in icon { /* qDebug() << "trayControl()"; qDebug() << "isHidden?" << this->isHidden(); qDebug() << "isVisible?" << this->isVisible(); qDebug() << "isMinimized?" << this->isMinimized(); qDebug() << "hasFocus?" << this->hasFocus(); */ // Hide or show the main window if (this->isMinimized()) { // hide and show, because raise() wouldn't work this->hide(); this->showNormal(); qDebug() << "RAISING!"; } else if (this->isHidden()) { this->show(); qDebug() << "SHOWING"; } else { this->hide(); qDebug() << "HIDING"; } } } /* * If FreeDesktop.org notifications are not available, * fall back to Qt's balloon ones * */ void MainWindow::showTrayFallbackMessage(QString message) { this->trayIcon->showMessage(tr("Dianara Notification"), message, QSystemTrayIcon::Information, 4000); // 4 secs } void MainWindow::updateProfileData(QString avatarUrl, QString fullName, QString hometown, QString bio, QString eMail) { if (!bio.isEmpty()) { bio.prepend(""); // make it rich text, so it gets wordwrap bio.replace("\n", "
"); // HTML newlines } this->fullNameLabel->setText(fullName); this->fullNameLabel->setToolTip(bio); this->userIdLabel->setText(this->userID); this->userIdLabel->setToolTip(bio); this->userHometownLabel->setText(hometown); this->userHometownLabel->setToolTip(bio); qDebug() << "Updated profile data from server:" << fullName << " @" << hometown; this->avatarURL = avatarUrl; qDebug() << "Own avatar URL:" << avatarURL; // Get local file name, which is stored in base64 hash form QString avatarFilename = MiscHelpers::getCachedAvatarFilename(avatarURL); if (QFile::exists(avatarFilename)) { // Load avatar if already cached this->avatarIconButton->setIcon(QIcon(QPixmap(avatarFilename) .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation))); qDebug() << "Using cached avatar for user"; } else { pumpController->getAvatar(avatarURL); } this->avatarIconButton->setToolTip(bio); // Fill/update this info in the profile editor too this->profileEditor->setProfileData(avatarURL, fullName, hometown, bio, eMail); } /* * Update all timelines * */ void MainWindow::updateAllTimelines() { mainTimeline->goToFirstPage(); // received timeline will come in a SIGNAL() directTimeline->goToFirstPage(); activityTimeline->goToFirstPage(); favoritesTimeline->goToFirstPage(); meanwhileFeed->updateFeed(); qDebug() << "Updated all timelines by menu"; } /* * Update some of the timelines: * * Main, Direct messages, and Minor feed * */ void MainWindow::updateMainDirectMinorTimelines() { mainTimeline->goToFirstPage(); directTimeline->goToFirstPage(); meanwhileFeed->updateFeed(); qDebug() << "Updated some timelines by menu or after:" << this->updateInterval << "min"; } void MainWindow::updateMainActivityMinorTimelines() { mainTimeline->goToFirstPage(); activityTimeline->goToFirstPage(); meanwhileFeed->updateFeed(); qDebug() << "Updated some timelines by menu or after posting"; } void MainWindow::scrollMainTimelineToTop() { this->mainTimelineScrollArea->verticalScrollBar()->setValue(0); this->mainTimelineScrollArea->horizontalScrollBar()->setValue(0); this->adjustTimelineSizes(); } void MainWindow::scrollDirectTimelineToTop() { this->directTimelineScrollArea->verticalScrollBar()->setValue(0); this->directTimelineScrollArea->horizontalScrollBar()->setValue(0); this->adjustTimelineSizes(); } void MainWindow::scrollActivityTimelineToTop() { this->activityTimelineScrollArea->verticalScrollBar()->setValue(0); this->activityTimelineScrollArea->horizontalScrollBar()->setValue(0); this->adjustTimelineSizes(); } void MainWindow::scrollFavoritesTimelineToTop() { this->favoritesTimelineScrollArea->verticalScrollBar()->setValue(0); this->favoritesTimelineScrollArea->horizontalScrollBar()->setValue(0); this->adjustTimelineSizes(); } /* * Scroll timelines to make sure the commenter block of the post * currently being commented is shown * */ void MainWindow::scrollMainTimelineToWidget(QWidget *widget) { this->mainTimelineScrollArea->ensureWidgetVisible(widget, 1, 1); } void MainWindow::scrollDirectTimelineToWidget(QWidget *widget) { this->directTimelineScrollArea->ensureWidgetVisible(widget, 1, 1); } void MainWindow::scrollActivityTimelineToWidget(QWidget *widget) { this->activityTimelineScrollArea->ensureWidgetVisible(widget, 1, 1); } void MainWindow::scrollFavoritesTimelineToWidget(QWidget *widget) { this->favoritesTimelineScrollArea->ensureWidgetVisible(widget, 1, 1); } void MainWindow::notifyTimelineUpdate(int timelineType, int newPostCount) { if (newPostCount > 0) { QString newPostsString; if (newPostCount == 1) { newPostsString = tr("There is 1 new post."); } else { newPostsString = tr("There are %1 new posts.").arg(newPostCount); } this->setStatusBarMessage(tr("Timeline updated.") + " " + newPostsString); // If some type of notifications are enabled, and only for main timeline if (this->showNotifications != FDNotifications::NoNotifications && timelineType == TimeLine::TimelineTypeMain) { this->fdNotifier->showMessage(tr("Timeline updated at %1.").arg(QTime::currentTime().toString()) + "\n" + newPostsString); } } else { this->setStatusBarMessage(tr("Timeline updated. No new posts.")); } // In either case, restart update timer, so every manual update of timeline this->updateTimer->stop(); // will postpone the auto-update this->updateTimer->start(); } /* * Update timelines titles with number of new posts * */ void MainWindow::setTimelineTabTitle(int timelineType, int newPostCount) { int updatedTab = 0; QString messageCountString; if (newPostCount > 0) { messageCountString = QString(" (%1)").arg(newPostCount); } switch (timelineType) { case TimeLine::TimelineTypeMain: this->tabWidget->setTabText(0, tr("&Timeline") + messageCountString); updatedTab = 0; break; case TimeLine::TimelineTypeDirect: this->tabWidget->setTabText(1, tr("&Messages") + messageCountString); updatedTab = 1; break; case TimeLine::TimelineTypeActivity: this->tabWidget->setTabText(2, tr("&Activity") + messageCountString); updatedTab = 2; break; case TimeLine::TimelineTypeFavorites: this->tabWidget->setTabText(3, tr("Fav&orites") + messageCountString); updatedTab = 3; break; } // If the updated tab is the current one, set also window title, etc. if (updatedTab == tabWidget->currentIndex()) { this->setTitleAndTrayInfo(updatedTab); } // If it's the main timeline, set the tray icon pixmap with the newmsg count if (updatedTab == 0) { if (trayIconAvailable) { this->setTrayIconPixmap(newPostCount); } } } /* * Set mainWindow's title based on current tab, and user ID * * Use that same title as tray icon's tooltip * */ void MainWindow::setTitleAndTrayInfo(int currentTab) { QString currentTabTitle; currentTabTitle = this->tabWidget->tabText(currentTab); currentTabTitle.remove("&"); // Remove accelators QString title = currentTabTitle + " - Dianara"; /* Not sure if I like it if (currentTabTitle.endsWith(")")) // kinda TMP { title.prepend("* "); } */ this->setWindowTitle(title); if (trayIconAvailable) { title = "Dianara: " + currentTabTitle + "

[" + (userID.isEmpty() ? tr("Your Pump.io account is not configured") : userID) + "]"; this->trayIcon->setToolTip(title); } } /* * Set the title for the minor feed (Meanwhile), with new item count * */ void MainWindow::setMinorFeedTitle(int newItemsCount) { QString title = tr("Meanwhile..."); if (newItemsCount > 0) { title.append(QString(" (%1)").arg(newItemsCount)); } this->leftPanel->setItemText(0, title); } /* * Store avatars on disk * */ void MainWindow::storeAvatar(QByteArray avatarData, QUrl avatarUrl) { QString fileName = MiscHelpers::getCachedAvatarFilename(avatarUrl.toString()); qDebug() << "Saving avatar to disk: " << fileName; QFile avatarFile(fileName); avatarFile.open(QFile::WriteOnly); avatarFile.write(avatarData); avatarFile.close(); this->pumpController->notifyAvatarStored(avatarUrl.toString(), avatarFile.fileName()); if (avatarUrl == this->avatarURL) { this->avatarIconButton->setIcon(QIcon(QPixmap(avatarFile.fileName()) .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation))); } qDebug() << "avatarData size:" << avatarData.size(); } /* * Store images on disk * */ void MainWindow::storeImage(QByteArray imageData, QUrl imageUrl) { QString fileName = MiscHelpers::getCachedImageFilename(imageUrl.toString()); qDebug() << "Saving image to disk: " << fileName; QFile imageFile(fileName); imageFile.open(QFile::WriteOnly); imageFile.write(imageData); imageFile.close(); this->pumpController->notifyImageStored(imageUrl.toString()); qDebug() << "imageData size:" << imageData.size(); } /* * Update status bar message, from pumpController's (and others) signals * */ void MainWindow::setStatusBarMessage(QString message) { this->statusBar()->showMessage("[" + QTime::currentTime().toString() + "] " + message, 0); } /* * Mark all posts in all timelines as read * and clear their counters * */ void MainWindow::markAllAsRead() { mainTimeline->markPostsAsRead(); directTimeline->markPostsAsRead(); activityTimeline->markPostsAsRead(); favoritesTimeline->markPostsAsRead(); meanwhileFeed->markAllAsRead(); this->setMinorFeedTitle(0); } /* * Hide or show the side panel * */ void MainWindow::toggleSidePanel(bool shown) { // Unless the publisher (really, the composer) has focus already, // give focus to timeline before, to avoid giving focus to the publisher if (!this->publisher->hasFocus()) { // FIXME: check if publisher's composer has focus! this->mainTimeline->setFocus(); } qDebug() << "Showing side panel:" << shown; this->leftSideWidget->setVisible(shown); if (!shown) { qApp->processEvents(); this->adjustTimelineSizes(); } } /* * Hide or show the status bar * */ void MainWindow::toggleStatusBar(bool shown) { qDebug() << "Showing side panel:" << shown; this->statusBar()->setVisible(shown); } /* * Toggle between fullscreen mode and normal window mode * */ void MainWindow::toggleFullscreen(bool enabled) { if (enabled) { this->showFullScreen(); } else { this->showNormal(); } } /* * Open website in browser * */ void MainWindow::visitWebSite() { qDebug() << "Opening website in browser"; QDesktopServices::openUrl(QUrl("http://jancoding.wordpress.com/dianara")); } /* * Open pump.io's FAQ in web browser * (currently at GitHub) * */ void MainWindow::visitFAQ() { qDebug() << "Opening Pump.io FAQ in browser"; QDesktopServices::openUrl(QUrl("https://github.com/e14n/pump.io/wiki/FAQ")); } /* * About... message * */ void MainWindow::aboutDianara() { QMessageBox::about(this, tr("About Dianara"), "Dianara v1.1
" "Copyright 2012-2014 JanKusanagi

" "" "http://jancoding.wordpress.com/dianara

" + tr("Dianara is a pump.io social networking client.") + "

" + tr("With Dianara you can see your timelines, create new posts, " "upload pictures, interact with posts, manage " "your contacts and follow new people.") + "

" + tr("Thanks to all the testers, translators and packagers, " "who help make Dianara better!") + "

" + tr("English translation by JanKusanagi.", "TRANSLATORS: Change this with your language and name ;)") + "

" + tr("Dianara is licensed under the GNU GPL license, and " "uses some Oxygen icons: http://www.oxygen-icons.org/ " "(LGPL licensed)")); if (this->isHidden()) // FIXME: ugly workaround to avoid closing the program { // after closing About dialog, if mainWindow hidden this->show(); this->hide(); // This hack might be causing problems under LXDE qDebug() << "MainWindow was hidden, showing and hiding again"; } } /* * Close the program. Needed to quit correctly from context menu * */ void MainWindow::quitProgram() { // Add more needed shutdown stuff here if (this->isHidden()) { this->show(); } saveSettings(); reallyQuitProgram = true; qApp->closeAllWindows(); qDebug() << "All windows closed, bye!"; } dianara-v1.1/src/mainwindow.h000664 000764 000764 00000014321 12262302745 015654 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) #include #endif #include #include #include #include #include "accountdialog.h" #include "configdialog.h" #include "pumpcontroller.h" #include "notifications.h" #include "publisher.h" #include "timeline.h" #include "post.h" #include "contactlist.h" #include "minorfeed.h" #include "profileeditor.h" #include "filtereditor.h" class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); ~MainWindow(); void prepareDataDirectory(); void createMenus(); void createTrayIcon(); void setTrayIconPixmap(int count = 0); void adjustTimelineSizes(); void loadSettings(); void saveSettings(); public slots: void updateUserID(QString newUserID); void updateSettings(int newUpdateInterval, int newPostsPerPageMain, int newPostsPerPageOther, int newTabsPosition, bool newTabsMovable, bool newPublicPosts, int newShowNotifications); void trayControl(QSystemTrayIcon::ActivationReason reason); void showTrayFallbackMessage(QString message); void updateProfileData(QString avatarUrl, QString fullName, QString hometown, QString bio, QString eMail); void updateAllTimelines(); void updateMainDirectMinorTimelines(); void updateMainActivityMinorTimelines(); void scrollMainTimelineToTop(); void scrollDirectTimelineToTop(); void scrollActivityTimelineToTop(); void scrollFavoritesTimelineToTop(); void scrollMainTimelineToWidget(QWidget *widget); void scrollDirectTimelineToWidget(QWidget *widget); void scrollActivityTimelineToWidget(QWidget *widget); void scrollFavoritesTimelineToWidget(QWidget *widget); void notifyTimelineUpdate(int timelineType, int newPostCount); void setTimelineTabTitle(int timelineType, int newPostCount); void setTitleAndTrayInfo(int currentTab); void setMinorFeedTitle(int newItemsCount); void storeAvatar(QByteArray avatarData, QUrl avatarUrl); void storeImage(QByteArray imageData, QUrl imageUrl); void setStatusBarMessage(QString message); void markAllAsRead(); void toggleSidePanel(bool shown); void toggleStatusBar(bool shown); void toggleFullscreen(bool enabled); void visitWebSite(); void visitFAQ(); void aboutDianara(); void quitProgram(); protected: virtual void closeEvent(QCloseEvent *event); virtual void resizeEvent(QResizeEvent *event); private: ////////////////////////////////////// Menus QMenu *sessionMenu; QMenu *viewMenu; QMenu *settingsMenu; QMenu *helpMenu; QMenu *trayContextMenu; QAction *sessionUpdateMainTimeline; QAction *sessionUpdateDirectTimeline; QAction *sessionUpdateActivityTimeline; QAction *sessionUpdateFavoritesTimeline; QAction *sessionUpdateMinorFeed; QAction *sessionUpdateAllTimelines; QAction *sessionMarkAllAsRead; QAction *sessionPostNote; QAction *sessionQuit; QAction *viewSidePanel; QAction *viewStatusBar; QAction *viewFullscreenAction; QAction *settingsEditProfile; QAction *settingsAccount; QAction *settingsFilters; QAction *settingsConfigure; QAction *helpVisitWebsite; QAction *helpVisitPumpFAQ; QAction *helpAbout; ////////////////////////////////////// End menus QSplitter *mainSplitter; QWidget *leftSideWidget; QVBoxLayout *leftLayout; QVBoxLayout *userInfoLayout; QToolBox *leftPanel; QWidget *rightSideWidget; QVBoxLayout *rightLayout; QTabWidget *tabWidget; int tabsPosition; bool tabsMovable; QPushButton *avatarIconButton; QString avatarURL; QLabel *fullNameLabel; QLabel *userIdLabel; QLabel *userHometownLabel; QSystemTrayIcon *trayIcon; bool trayIconAvailable; AccountDialog *accountDialog; ProfileEditor *profileEditor; ConfigDialog *configDialog; FilterEditor *filterEditor; PumpController *pumpController; FDNotifications *fdNotifier; MinorFeed *meanwhileFeed; int postsPerPageMain; TimeLine *mainTimeline; QScrollArea *mainTimelineScrollArea; int postsPerPageOther; TimeLine *directTimeline; QScrollArea *directTimelineScrollArea; TimeLine *activityTimeline; QScrollArea *activityTimelineScrollArea; TimeLine *favoritesTimeline; QScrollArea *favoritesTimelineScrollArea; ContactList *contactList; Publisher *publisher; bool firstRun; QString dataDirectory; // will have /images and /avatars bool reallyQuitProgram; int updateInterval; QTimer *updateTimer; bool publicPosts; int showNotifications; // user account-related data QString userID; }; #endif // MAINWINDOW_H dianara-v1.1/src/configdialog.h000664 000764 000764 00000004655 12260657136 016143 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef CONFIGDIALOG_H #define CONFIGDIALOG_H #include #include #include #include #include #include #include #include #include #include #include class ConfigDialog : public QWidget { Q_OBJECT public: ConfigDialog(QString dataDirectory, int updateInterval, int postsPerPageMain, int postsPerPageOther, int tabsPosition, bool tabsMovable, bool publicPosts, int showNotifications, QWidget *parent); ~ConfigDialog(); signals: void configurationChanged(int newUpdateInterval, int newPostsPerPageMain, int newPostsPerPageOther, int newTabsPosition, bool newTabsMovable, bool newPublicPosts, int newShowNotifications); public slots: void saveConfiguration(); private: QVBoxLayout *mainLayout; QFormLayout *optionsLayout; QSpinBox *updateIntervalSpinbox; QSpinBox *postsPerPageMainSpinbox; QSpinBox *postsPerPageOtherSpinbox; QComboBox *tabsPositionCombobox; QCheckBox *tabsMovableCheckbox; QCheckBox *publicPostsCheckbox; QComboBox *showNotificationsCombobox; QLabel *dataDirectoryLabel; QHBoxLayout *buttonsLayout; QPushButton *saveConfigButton; QPushButton *cancelButton; }; #endif // CONFIGDIALOG_H dianara-v1.1/src/configdialog.cpp000664 000764 000764 00000015061 12260657140 016462 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "configdialog.h" ConfigDialog::ConfigDialog(QString dataDirectory, int updateInterval, int postsPerPageMain, int postsPerPageOther, int tabsPosition, bool tabsMovable, bool publicPosts, int showNotifications, QWidget *parent) : QWidget(parent) { this->setWindowTitle("Dianara - " + tr("Program Configuration")); this->setWindowIcon(QIcon::fromTheme("configure")); this->setWindowFlags(Qt::Window); this->setWindowModality(Qt::ApplicationModal); this->setMinimumSize(480, 320); // Upper part optionsLayout = new QFormLayout(); updateIntervalSpinbox = new QSpinBox(); updateIntervalSpinbox->setRange(2, 60); // 2-60 min updateIntervalSpinbox->setSuffix(" "+ tr("minutes")); updateIntervalSpinbox->setValue(updateInterval); optionsLayout->addRow(tr("Timeline &update interval"), updateIntervalSpinbox); postsPerPageMainSpinbox = new QSpinBox(); postsPerPageMainSpinbox->setRange(5, 100); // 5-100 ppp postsPerPageMainSpinbox->setSuffix(" "+ tr("posts", "Goes after a number, as: 25 posts")); postsPerPageMainSpinbox->setValue(postsPerPageMain); optionsLayout->addRow(tr("&Posts per page, main timeline"), postsPerPageMainSpinbox); postsPerPageOtherSpinbox = new QSpinBox(); postsPerPageOtherSpinbox->setRange(5, 100); // 5-100 ppp postsPerPageOtherSpinbox->setSuffix(" "+ tr("posts", "This goes after a number, like: 10 posts")); postsPerPageOtherSpinbox->setValue(postsPerPageOther); optionsLayout->addRow(tr("Posts per page, &other timelines"), postsPerPageOtherSpinbox); tabsPositionCombobox = new QComboBox(); tabsPositionCombobox->addItem(QIcon::fromTheme("arrow-up"), tr("Top")); tabsPositionCombobox->addItem(QIcon::fromTheme("arrow-down"), tr("Bottom")); tabsPositionCombobox->addItem(QIcon::fromTheme("arrow-left"), tr("Left side")); tabsPositionCombobox->addItem(QIcon::fromTheme("arrow-right"), tr("Right side")); tabsPositionCombobox->setCurrentIndex(tabsPosition); optionsLayout->addRow(tr("&Tabs position"), tabsPositionCombobox); tabsMovableCheckbox = new QCheckBox(); tabsMovableCheckbox->setChecked(tabsMovable); optionsLayout->addRow(tr("&Movable tabs"), tabsMovableCheckbox); publicPostsCheckbox = new QCheckBox(); publicPostsCheckbox->setChecked(publicPosts); optionsLayout->addRow(tr("Public posts as &default"), publicPostsCheckbox); showNotificationsCombobox = new QComboBox(); showNotificationsCombobox->addItem(QIcon::fromTheme("preferences-desktop-notification"), tr("As system notifications")); showNotificationsCombobox->addItem(QIcon::fromTheme("view-conversation-balloon"), tr("Using own notifications")); showNotificationsCombobox->addItem(QIcon::fromTheme("user-busy"), // dialog-cancel tr("Don't show notifications")); showNotificationsCombobox->setCurrentIndex(showNotifications); optionsLayout->addRow(tr("Show ¬ifications"), showNotificationsCombobox); // Label to show where the data directory is dataDirectoryLabel = new QLabel(tr("Dianara stores data in this folder:") + QString(" %2") .arg(dataDirectory).arg(dataDirectory)); dataDirectoryLabel->setWordWrap(true); dataDirectoryLabel->setOpenExternalLinks(true); QFont dataDirectoryFont; dataDirectoryFont.setPointSize(dataDirectoryFont.pointSize() - 1); dataDirectoryLabel->setFont(dataDirectoryFont); //// Bottom part saveConfigButton = new QPushButton(QIcon::fromTheme("document-save"), tr("&Save Configuration")); connect(saveConfigButton, SIGNAL(clicked()), this, SLOT(saveConfiguration())); cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel"), tr("&Cancel")); connect(cancelButton, SIGNAL(clicked()), this, SLOT(hide())); this->buttonsLayout = new QHBoxLayout(); buttonsLayout->setAlignment(Qt::AlignRight); buttonsLayout->addWidget(saveConfigButton); buttonsLayout->addWidget(cancelButton); // Set up main layout mainLayout = new QVBoxLayout(); mainLayout->addLayout(optionsLayout, 2); mainLayout->addSpacing(16); mainLayout->addStretch(1); mainLayout->addWidget(dataDirectoryLabel); mainLayout->addSpacing(16); mainLayout->addStretch(1); mainLayout->addLayout(buttonsLayout); this->setLayout(mainLayout); qDebug() << "Config dialog created"; } ConfigDialog::~ConfigDialog() { qDebug() << "Config dialog destroyed"; } ///////////////////////////////// SLOTS ////////////////////////////////////// void ConfigDialog::saveConfiguration() { emit configurationChanged(updateIntervalSpinbox->value(), postsPerPageMainSpinbox->value(), postsPerPageOtherSpinbox->value(), tabsPositionCombobox->currentIndex(), tabsMovableCheckbox->isChecked(), publicPostsCheckbox->isChecked(), showNotificationsCombobox->currentIndex()); qDebug() << "ConfigDialog: config saved"; this->hide(); // this->close() would end the program if mainWindow was hidden } dianara-v1.1/src/pumpcontroller.h000664 000764 000764 00000021426 12260657140 016571 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef PUMPCONTROLLER_H #define PUMPCONTROLLER_H #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) #include #endif #include #include #include #include // TMP #include #include #include #include #include /// For JSON parsing #include #include #include #include // For OAuth authentication #include class PumpController : public QObject { Q_OBJECT public: explicit PumpController(QObject *parent = 0); ~PumpController(); void setPostsPerPageMain(int ppp); void setPostsPerPageOther(int ppp); void setUpdatesToTimelineBlocked(bool blocked); void setFilters(QVariantList filterList); QVariantList getCurrentFilters(); void setNewUserId(QString userId); void setUserCredentials(QString userId); QString currentUserId(); QString currentUsername(); QString currentFollowersUrl(); int currentFollowersCount(); int currentFollowingCount(); void getUserProfile(QString userId); void updateUserProfile(QString avatarUrl, QString fullName, QString hometown, QString bio); void getAvatar(QString avatarURL); void getImage(QString imageURL); void notifyAvatarStored(QString avatarUrl, QString avatarFilename); void notifyImageStored(QString imageUrl); void getContactList(QString listType, int offset=0); bool userInFollowing(QString contactId); void updateInternalFollowingIdList(QStringList idList); void getListsList(); void createPersonList(QString name, QString description); void deletePersonList(QString id); void getPersonList(QString url); void addPersonToList(QString listId, QString personId); void removePersonFromList(QString listId, QString personId); void getMainTimeline(int timelineOffset); void getDirectTimeline(int timelineOffset); void getActivityTimeline(int timelineOffset); void getFavoritesTimeline(int timelineOffset); void getPostLikes(QString postLikesURL); void getPostComments(QString postCommentsURL); void getPostShares(QString postSharesURL); void getMinorFeed(int offset = 0); QNetworkRequest prepareRequest(QString url, QOAuth::HttpMethod method, int requestType, QOAuth::ParamMap paramMap = QOAuth::ParamMap(), QString contentTypeString="application/json"); void uploadFile(QString filename, QString contentType, int uploadType = UploadFileRequest); QList processAudience(QMap audienceMap); enum requestTypes { NoRequest, ClientRegistrationRequest, TokenRequest, UserProfileRequest, UpdateProfileRequest, FollowingListRequest, FollowersListRequest, ListsListRequest, CreatePersonListRequest, DeletePersonListRequest, PersonListRequest, AddMemberToListRequest, RemoveMemberFromListRequest, MainTimelineRequest, DirectTimelineRequest, ActivityTimelineRequest, FavoritesTimelineRequest, PostLikesRequest, PostCommentsRequest, PostSharesRequest, MinorFeedRequest, PublishPostRequest, LikePostRequest, CommentPostRequest, SharePostRequest, UnsharePostRequest, DeletePostRequest, UpdatePostRequest, FollowContactRequest, UnfollowContactRequest, AvatarRequest, ImageRequest, UploadFileRequest, UploadPictureRequest, UploadAvatarRequest, PublishAvatarRequest }; signals: void openingAuthorizeURL(QUrl url); void authorizationStatusChanged(bool authorized); void profileReceived(QString avatarURL, QString fullName, QString hometown, QString bio, QString email); void contactListReceived(QString listType, QVariantList contactList, int totalReceivedCount); void listsListReceived(QVariantList listsList); void personListReceived(QVariantList personList, QString listUrl); void personAddedToList(QString id, QString name); void personRemovedFromList(QString id); void mainTimelineReceived(QVariantList postList, int postsPerPage, QString previousLink, QString nextLink); void directTimelineReceived(QVariantList postList, int postsPerPage, QString previousLink, QString nextLink); void activityTimelineReceived(QVariantList postList, int postsPerPage, QString previousLink, QString nextLink); void favoritesTimelineReceived(QVariantList postList, int postsPerPage, QString previousLink, QString nextLink); void likesReceived(QVariantList likesList, QString originatingPostURL); void commentsReceived(QVariantList commentsList, QString originatingPostURL); void minorFeedReceived(QVariantList activitiesList); void avatarPictureReceived(QByteArray pictureData, QUrl pictureURL); void imageReceived(QByteArray pictureData, QUrl pictureURL); void avatarStored(QString avatarUrl, QString avatarFilename); void imageStored(QString imageUrl); void postPublished(); void postPublishingFailed(); void likeSet(); void commentPosted(); void commentPostingFailed(); void avatarUploaded(QString url); void showNotification(QString message); void currentJobChanged(QString message); public slots: void requestFinished(QNetworkReply *reply); void sslErrorsHandler(QNetworkReply *reply, QList errorList); void getToken(); void authorizeApplication(QString verifierCode); void getInitialData(); void postNote(QMap audienceMap, QString postText, QString postTitle); void postImage(QMap audienceMap, QString postText, QString currentImageTitle, QString imageFilename, QString contentType); void postImageStepTwo(QString id); void postImageStepThree(QString id); void postAvatarStepTwo(QString id); void updatePost(QString id, QString content, QString title); void likePost(QString postID, QString postType, bool like); void addComment(QString comment, QString postID, QString postType); void sharePost(QString postID, QString postType); void unsharePost(QString postId, QString postType); void deletePost(QString postID, QString postType); void followContact(QString address); void unfollowContact(QString address); void tmpfixmeNotifyQNAMdestroyed(); // FIXME TMP KILL IT! private: QNetworkAccessManager nam; QByteArray userAgentString; // QOAuth-related QOAuth::Interface *qoauth; bool isApplicationAuthorized; QString clientID; QString clientSecret; QByteArray token; QByteArray tokenSecret; QString userId; // Full webfinger address, user@host.tld QString userName; QString serverURL; QString userFollowersURL; int userFollowersCount; int userFollowingCount; QStringList followingIdList; int totalReceivedFollowers; int totalReceivedFollowing; QTimer *initialDataTimer; int initialDataStep; bool haveProfile; bool haveFollowing; bool haveFollowers; bool havePersonLists; int postsPerPageMain; int postsPerPageOther; bool updatesToTimelineBlocked; QVariantList currentFilters; // For multi-step operations in posts QString currentImageTitle; QString currentImageDescription; QMap currentAudienceMap; }; #endif // PUMPCONTROLLER_H dianara-v1.1/src/post.cpp000664 000764 000764 00000125302 12264102165 015016 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "post.h" Post::Post(PumpController *pumpController, ASActivity *activity, bool isStandalone, QWidget *parent) : QFrame(parent) { this->pController = pumpController; this->standalone = isStandalone; activity->setParent(this); // reparent the passed activity this->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); leftColumnLayout = new QVBoxLayout(); this->postId = activity->object()->getId(); this->postType = activity->object()->getType(); this->postUrl = activity->object()->getUrl(); this->postAuthorId = activity->getAuthorId(); this->postAuthorName = activity->getAuthorName(); if (postAuthorName.isEmpty()) // In case name is empty, use webfinger ID { postAuthorName = this->postAuthorId; } this->postAuthorUrl = activity->getAuthorUrl(); // Save post title for openClickedURL(), etc this->postTitle = activity->object()->getTitle(); QString generator = activity->getGenerator(); // If standalone post, set window title if (standalone) { QString postTypeString; if (postType == "note") { postTypeString = tr("Note"); } else if (postType == "image") { postTypeString = tr("Image"); } else if (postType == "comment") { postTypeString = tr("Comment"); } else { postTypeString = tr("Other", "As in: other type of post"); } QString windowTitle = tr("Post", "Noun, not verb") + ": " + postTypeString + " - Dianara"; this->setWindowTitle(windowTitle); } postIsUnread = false; QFont detailsFont; detailsFont.setPointSize(detailsFont.pointSize() - 2); // FIXME: check size first /////////////////////////////////////////////////// Left column, post Meta info // Is the post a reshare? Indicate who shared it postIsSharedLabel = new QLabel(); if (activity->isShared()) { this->postSharedById = activity->getSharedById(); QString sharedByName = MiscHelpers::fixLongName(activity->getSharedByName()); postIsSharedLabel->setText(QString::fromUtf8("\342\231\272 ") // Recycling symbol + tr("Via %1").arg(sharedByName)); postIsSharedLabel->setWordWrap(true); postIsSharedLabel->setAlignment(Qt::AlignCenter); postIsSharedLabel->setFont(detailsFont); postIsSharedLabel->setForegroundRole(QPalette::Text); postIsSharedLabel->setBackgroundRole(QPalette::Base); postIsSharedLabel->setAutoFillBackground(true); postIsSharedLabel->setFrameStyle(QFrame::Panel | QFrame::Raised); QString sharedByAvatarFilename = MiscHelpers::getCachedAvatarFilename(activity->getSharedByAvatar()); QString sharedByTooltipString = ""; sharedByTooltipString.append("  " + activity->getSharedByName() + "
"); sharedByTooltipString.append("  " + this->postSharedById + ""); QString exactShareTime = Timestamp::localTimeDate(activity->getUpdatedAt()); QString fuzzyShareTime = Timestamp::fuzzyTime(activity->getUpdatedAt()); sharedByTooltipString.append("


" + tr("Shared on %1").arg(exactShareTime) + QString("
(%1)").arg(fuzzyShareTime)); if (!generator.isEmpty()) { sharedByTooltipString.append(" " + tr("using %1").arg(generator)); generator.clear(); // So it's not used in the post timestamp tooltip! } postIsSharedLabel->setToolTip(sharedByTooltipString); leftColumnLayout->addWidget(postIsSharedLabel); } leftColumnLayout->addSpacing(2); QString authorTooltipInfo = "" + this->postAuthorName + "
"; authorTooltipInfo.append("" + postAuthorId + ""); authorTooltipInfo.append("
"); if (!activity->getAuthorHometown().isEmpty()) { authorTooltipInfo.append("
"); authorTooltipInfo.append("" + tr("Hometown") + ": " + activity->getAuthorHometown()); authorTooltipInfo.append("
"); } if (!activity->getAuthorBio().isEmpty()) { authorTooltipInfo.append("
"); authorTooltipInfo.append(activity->getAuthorBio()); } authorTooltipInfo.replace("\n", "
"); // HTML newlines if (this->postAuthorId == pController->currentUserId()) { postIsOwn = true; qDebug() << "post is our own!"; this->setFrameStyle(QFrame::Panel | QFrame::Sunken); } else { postIsOwn = false; this->setFrameStyle(QFrame::Box | QFrame::Raised); } // Author avatar postAuthorAvatarButton = new QPushButton(); postAuthorAvatarButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); postAuthorAvatarButton->setFlat(true); postAuthorAvatarButton->setIconSize(QSize(48,48)); postAuthorAvatarButton->setToolTip(authorTooltipInfo); // Get "following" status before creating avatar menu this->postAuthorFollowed = this->pController->userInFollowing(this->postAuthorId); this->createAvatarMenu(); postAuthorAvatarButton->setMenu(avatarMenu); this->postAuthorAvatarUrl = activity->getAuthorAvatar(); // Get local file name for avatar, which is stored in base64 hash form QString avatarFilename = MiscHelpers::getCachedAvatarFilename(postAuthorAvatarUrl); if (QFile::exists(avatarFilename)) { // Load avatar if already cached postAuthorAvatarButton->setIcon(QIcon(QPixmap(avatarFilename).scaled(48, 48, Qt::KeepAspectRatio, Qt::SmoothTransformation))); } else { // Placeholder image postAuthorAvatarButton->setIcon(QIcon::fromTheme("user-identity", QIcon(":/images/no-avatar.png"))); qDebug() << "Using placeholder, downloading real avatar now"; // Download avatar for next time connect(pController, SIGNAL(avatarStored(QString,QString)), this, SLOT(redrawAvatar(QString,QString))); pController->getAvatar(postAuthorAvatarUrl); } leftColumnLayout->addWidget(postAuthorAvatarButton, 0, Qt::AlignLeft); leftColumnLayout->addSpacing(2); QFont authorFont; authorFont.setBold(true); authorFont.setUnderline(true); authorFont.setPointSize(authorFont.pointSize()-1); if (postIsOwn) // Another visual hint when the post is our own { authorFont.setItalic(true); } // Author name QString authorName = MiscHelpers::fixLongName(this->postAuthorName); postAuthorNameLabel = new QLabel(authorName); postAuthorNameLabel->setTextFormat(Qt::PlainText); postAuthorNameLabel->setWordWrap(true); postAuthorNameLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); postAuthorNameLabel->setFont(authorFont); postAuthorNameLabel->setToolTip(authorTooltipInfo); leftColumnLayout->addWidget(postAuthorNameLabel); leftColumnLayout->addSpacing(2); // Post creation time QString createdAt = activity->object()->getCreatedAt(); // and update time QString updatedAt = activity->object()->getUpdatedAt(); if (updatedAt == createdAt) { updatedAt.clear(); } // Format is "Combined date and time in UTC", like "2012-02-07T01:32:02Z" // as specified in ISO 8601 http://en.wikipedia.org/wiki/ISO_8601 QString exactPostTime = Timestamp::localTimeDate(createdAt); createdAt = Timestamp::fuzzyTime(createdAt); QString dateGeneratorTooltip = tr("Posted on %1", "1=Date").arg(exactPostTime); if (!generator.isEmpty()) { dateGeneratorTooltip.append("\n" + tr("using %1", "1=Program used for posting").arg(generator)); } if (!updatedAt.isEmpty()) { // Using \n instead of
because I don't want this tooltip to be HTML (wrapped) createdAt.append("\n" + tr("Edited: %1") .arg(Timestamp::fuzzyTime(updatedAt))); dateGeneratorTooltip.append("\n\n" + tr("Edited on %1") .arg(Timestamp::localTimeDate(updatedAt))); } postCreatedAtLabel = new QLabel(createdAt); postCreatedAtLabel->setToolTip(dateGeneratorTooltip); postCreatedAtLabel->setTextFormat(Qt::PlainText); postCreatedAtLabel->setWordWrap(true); postCreatedAtLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); postCreatedAtLabel->setFont(detailsFont); leftColumnLayout->addWidget(postCreatedAtLabel); leftColumnLayout->addSpacing(4); // Location information, if any QString location = activity->object()->getLocation(); postLocationLabel = new QLabel(tr("In") + ": " + location + ""); postLocationLabel->setWordWrap(true); postLocationLabel->setFont(detailsFont); if (!location.isEmpty()) { leftColumnLayout->addWidget(postLocationLabel); leftColumnLayout->addSpacing(2); } postToLabel = new QLabel(tr("To") + ": " + activity->getToString()); postToLabel->setWordWrap(true); postToLabel->setFont(detailsFont); postToLabel->setOpenExternalLinks(true); if (!activity->getToString().isEmpty()) { leftColumnLayout->addWidget(postToLabel); } postCCLabel = new QLabel(tr("CC") + ": " + activity->getCCString()); postCCLabel->setWordWrap(true); postCCLabel->setFont(detailsFont); postCCLabel->setOpenExternalLinks(true); if (!activity->getCCString().isEmpty()) { leftColumnLayout->addWidget(postCCLabel); } leftColumnLayout->addSpacing(4); this->postLikesUrl = activity->object()->getLikesUrl(); this->postCommentsUrl = activity->object()->getCommentsUrl(); this->postSharesUrl = activity->object()->getSharesUrl(); // Set this QLabels with parent=this, since we're gonna hide them right away // They'll be reparented to the layout, but not having a parent before that // would cause visual glitches postLikesCountLabel = new QLabel(this); postLikesCountLabel->setWordWrap(true); postLikesCountLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); postLikesCountLabel->setFont(detailsFont); leftColumnLayout->addWidget(postLikesCountLabel); postLikesCountLabel->hide(); int likesCount = activity->object()->getLikesCount().toInt(); this->setLikesLabel(likesCount); postCommentsCountLabel = new QLabel(this); postCommentsCountLabel->setWordWrap(true); postCommentsCountLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); postCommentsCountLabel->setFont(detailsFont); leftColumnLayout->addWidget(postCommentsCountLabel); postCommentsCountLabel->hide(); int commentsCount = activity->object()->getCommentsCount().toInt(); this->setCommentsLabel(commentsCount); postResharesCountLabel = new QLabel(this); postResharesCountLabel->setWordWrap(true); postResharesCountLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); postResharesCountLabel->setFont(detailsFont); leftColumnLayout->addWidget(postResharesCountLabel); postResharesCountLabel->hide(); int sharesCount = activity->object()->getSharesCount().toInt(); this->setSharesLabel(sharesCount); // Try to use all remaining space, aligning // all previous widgets nicely at the top // leftColumnLayout->setAlignment(Qt::AlignTop) caused glitches leftColumnLayout->addStretch(1); ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////// Right column, content rightColumnLayout = new QVBoxLayout(); rightColumnLayout->setAlignment(Qt::AlignTop); ///////////////////////////////////// Title this->postTitleLabel = new QLabel(); postTitleLabel->setWordWrap(true); QFont postTitleFont; postTitleFont.setBold(true); postTitleFont.setPointSize(postTitleFont.pointSize() + 1); postTitleLabel->setFont(postTitleFont); postTitleLabel->setText(postTitle); ///////////////////////////////////// Post text postText = new QTextBrowser(); postText->setAlignment(Qt::AlignLeft | Qt::AlignTop); postText->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); postText->setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); postText->setOpenLinks(false); // don't open links, manage in openClickedURL() postText->setReadOnly(true); // it's default with QTextBrowser, but still... connect(postText, SIGNAL(anchorClicked(QUrl)), this, SLOT(openClickedURL(QUrl))); connect(postText, SIGNAL(highlighted(QUrl)), this, SLOT(showHighlightedURL(QUrl))); // Add a label in the bottom right corner, to show where links go highlightedUrlLabel = new QLabel(); highlightedUrlLabel->setAlignment(Qt::AlignLeft); highlightedUrlLabel->setFont(detailsFont); highlightedUrlLabel->setForegroundRole(QPalette::ToolTipText); // use Tooltip colors highlightedUrlLabel->setBackgroundRole(QPalette::ToolTipBase); highlightedUrlLabel->setAutoFillBackground(true); highlightedUrlLabel->setWordWrap(true); highlightedUrlLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); highlightedUrlLabel->hide(); // Hidden initially postTextLayout = new QVBoxLayout(); postTextLayout->setAlignment(Qt::AlignRight | Qt::AlignBottom); postTextLayout->addWidget(highlightedUrlLabel); postText->setLayout(postTextLayout); QFont buttonsFont; buttonsFont.setPointSize(buttonsFont.pointSize() - 1); // Like, comment, share buttons likeButton = new QPushButton(QIcon::fromTheme("emblem-favorite"), "like"); likeButton->setCheckable(true); this->fixLikeButton(activity->object()->isLiked()); likeButton->setFlat(true); likeButton->setFont(buttonsFont); connect(likeButton, SIGNAL(clicked(bool)), this, SLOT(likePost(bool))); commentButton = new QPushButton(QIcon::fromTheme("mail-reply-sender"), tr("Comment")); commentButton->setToolTip(tr("Comment on this post.") + "
" + tr("If you select some text, it will be quoted.")); commentButton->setFlat(true); commentButton->setFont(buttonsFont); connect(commentButton, SIGNAL(clicked()), this, SLOT(commentOnPost())); // Note: Gwenview includes 'document-share' icon shareButton = new QPushButton(QIcon::fromTheme("mail-forward"), "*share*"); shareButton->setFlat(true); shareButton->setFont(buttonsFont); if (postSharedById.isEmpty() || (pController->currentUserId() != this->postSharedById)) { shareButton->setText(tr("Share")); shareButton->setToolTip(tr("Share this post")); connect(shareButton, SIGNAL(clicked()), this, SLOT(sharePost())); } else // Shared by us! { shareButton->setText(tr("Unshare")); shareButton->setToolTip(tr("Unshare this post")); connect(shareButton, SIGNAL(clicked()), this, SLOT(unsharePost())); shareButton->setDisabled(true); // FIXME: disabled for 1.1, since doesn't really work } // Add like, comment, share and, if post is our own, edit and delete buttons buttonsLayout = new QHBoxLayout(); buttonsLayout->setAlignment(Qt::AlignHCenter | Qt::AlignTop); buttonsLayout->setContentsMargins(0, 0, 0, 0); buttonsLayout->setMargin(0); buttonsLayout->setSpacing(0); buttonsLayout->addWidget(likeButton, 0, Qt::AlignLeft); if (postType != "comment") // Don't add these buttons if it's a comment { // (can happen in the Favorites timeline) buttonsLayout->addWidget(commentButton, 0, Qt::AlignLeft); buttonsLayout->addWidget(shareButton, 0, Qt::AlignLeft); } buttonsLayout->addStretch(1); // so the (optional) Edit and Delete buttons get separated if (postIsOwn) { editButton = new QPushButton(QIcon::fromTheme("document-edit"), tr("Edit")); editButton->setToolTip(tr("Edit this post")); editButton->setFlat(true); editButton->setFont(buttonsFont); connect(editButton, SIGNAL(clicked()), this, SLOT(editPost())); buttonsLayout->addWidget(editButton, 0, Qt::AlignRight); deleteButton = new QPushButton(QIcon::fromTheme("edit-delete"), tr("Delete")); deleteButton->setToolTip(tr("Delete this post")); deleteButton->setFlat(true); deleteButton->setFont(buttonsFont); connect(deleteButton, SIGNAL(clicked()), this, SLOT(deletePost())); buttonsLayout->addWidget(deleteButton, 0, Qt::AlignRight); } /******************************************************************/ // Get URL of post image, if it's "image" type of post if (this->postType == "image") { postImageUrl = activity->object()->getImageUrl(); this->enqueueImageForDownload(postImageUrl); } this->postOriginalText = activity->object()->getContent(); // Save it for later... QStringList postTextImageList = MiscHelpers::htmlWithReplacedImages(postOriginalText, this->postText->width()); postTextImageList.removeFirst(); // First is the HTML itself // If the image list is not empty, get them (unless they're cached already) if (!postTextImageList.isEmpty()) { qDebug() << "Post has" << postTextImageList.size() << "images included..."; foreach (QString imageUrl, postTextImageList) { this->enqueueImageForDownload(imageUrl); } } // Post resizing takes place in resizeEvent() // which will also call setPostContents() /////////////////////////////////////////////////////// Comments block this->commenter = new CommenterBlock(this->pController); connect(commenter, SIGNAL(commentSent(QString)), this, SLOT(sendComment(QString))); connect(commenter, SIGNAL(allCommentsRequested()), this, SLOT(getAllComments())); if (!this->postTitle.isEmpty()) { rightColumnLayout->addWidget(postTitleLabel, 0); } rightColumnLayout->addWidget(postText, 3); rightColumnLayout->addLayout(buttonsLayout, 0); rightColumnLayout->addWidget(commenter, 0); rightColumnLayout->addStretch(0); // Set the initial likes, comments and shares (4 most recent) this->setLikes(activity->object()->getLastLikesList(), likesCount); this->commenter->setComments(activity->object()->getLastCommentsList()); this->setShares(activity->object()->getLastSharesList(), sharesCount); mainLayout = new QHBoxLayout(); mainLayout->addLayout(leftColumnLayout, 2); // stretch 2/11 mainLayout->addLayout(rightColumnLayout, 9); // stretch 9/11 mainLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop); this->setLayout(mainLayout); this->minMaxHeight = 400; // Initialize, but it'll be set before it's used anyway // Set read status this->setPostUnreadStatus(); qDebug() << "Post created" << this->postUrl; } Post::~Post() { qDebug() << "Post destroyed" << this->postUrl; } /* * Create the menu shown when clicking the post author's avatar * */ void Post::createAvatarMenu() { this->avatarMenu = new QMenu(); avatarMenu->setSeparatorsCollapsible(false); this->avatarMenuIdAction = new QAction(QIcon::fromTheme("user-identity"), postAuthorId, this); avatarMenuIdAction->setSeparator(true); // Make it nicer and not clickable avatarMenu->addAction(avatarMenuIdAction); avatarMenu->addAction(QIcon::fromTheme("internet-web-browser"), tr("Open %1's profile in web browser") .arg(this->postAuthorName), this, SLOT(openAuthorProfileInBrowser())); this->avatarMenuFollowAction = new QAction("*follow/unfollow*", this); if (!postIsOwn) // Only add "follow/unfollow" option, if the post is not ours { avatarMenu->addAction(avatarMenuFollowAction); this->setFollowUnfollow(); } avatarMenu->addSeparator(); avatarMenu->addAction(QIcon::fromTheme("internet-web-browser"), tr("Open post in web browser"), this, SLOT(openPostInBrowser())); avatarMenu->addAction(QIcon::fromTheme("edit-copy"), tr("Copy post link to clipboard"), this, SLOT(copyPostUrlToClipboard())); avatarMenu->addSeparator(); avatarMenu->addAction(QIcon::fromTheme("format-text-color"), tr("Normalize text colors"), this, SLOT(normalizeTextFormat())); } /* * Set the icon and text of the follow/unfollow option of the avatar menu * according to whether we're following that user or not * */ void Post::setFollowUnfollow() { if (this->postAuthorFollowed) { this->avatarMenuFollowAction->setIcon(QIcon::fromTheme("list-remove-user")); this->avatarMenuFollowAction->setText(tr("Stop following")); connect(avatarMenuFollowAction, SIGNAL(triggered()), this, SLOT(unfollowUser())); disconnect(avatarMenuFollowAction, SIGNAL(triggered()), this, SLOT(followUser())); qDebug() << "post author followed, connecting to UNFOLLOW()" << this->postAuthorId; } else { this->avatarMenuFollowAction->setIcon(QIcon::fromTheme("list-add-user")); this->avatarMenuFollowAction->setText(tr("Follow")); connect(avatarMenuFollowAction, SIGNAL(triggered()), this, SLOT(followUser())); disconnect(avatarMenuFollowAction, SIGNAL(triggered()), this, SLOT(unfollowUser())); qDebug() << "post author not followed, connecting to FOLLOW()" << this->postAuthorId; } } void Post::setMinMaxHeight(int newMinMaxHeight) { // Substract some pixels to account for the row of buttons, etc. this->minMaxHeight = newMinMaxHeight - 80; } /* * Set the post contents, and trigger a vertical resize * */ void Post::setPostContents() { //qDebug() << "Post::setPostContents()" << this->postID; QString postImage; int imageWidth; if (!this->postImageUrl.isEmpty()) { QString imageCachedFilename = MiscHelpers::getCachedImageFilename(postImageUrl); imageWidth = MiscHelpers::getImageWidth(imageCachedFilename); // if the image is wider than the post space, make it smaller if (imageWidth > this->postWidth) { imageWidth = this->postWidth; } if (QFile::exists(imageCachedFilename)) { postImage = "
" "" "" "" "

"; } else // use placeholder image while it loads... { postImage = "
" "
" "" "
" "

"; } } QStringList postTextImageList = MiscHelpers::htmlWithReplacedImages(postOriginalText, this->postWidth); QString postTextContents = postTextImageList.takeAt(0); // Add the text content of the post postText->setHtml(postImage + postTextContents); this->setPostHeight(); } /* * Set the height of the post, based on the contents * */ void Post::setPostHeight() { int height = postText->document()->size().toSize().height() + 12; // +12px error margin if (height < 60) { height = 60; } if (height > this->minMaxHeight) // Don't allow a post to be too tall { height = this->minMaxHeight; // Scrollbars might appear } // Don't force a specific height if the post is standalone if (!standalone) { postText->setMinimumHeight(height); postText->setMaximumHeight(height); } } /* * Add the URL of an image to the queue of pending-download * * connect() signal/slot if necessary * */ void Post::enqueueImageForDownload(QString url) { if (QFile::exists(MiscHelpers::getCachedImageFilename(url)) || pendingImagesList.contains(url)) { qDebug() << "Post::enqueueImageForDownload(), " "Using cached post image, or requested image is pending download..."; } else { qDebug() << "Post::enqueueImageForDownload(), " "post image not cached, downloading" << url; // FIXME? multi-connect? connect(pController, SIGNAL(imageStored(QString)), this, SLOT(redrawImages(QString))); this->pendingImagesList.append(url); pController->getImage(url); } } /* * Return the likes/favorites URL for this post * */ QString Post::likesURL() { return this->postLikesUrl; } /* * Update the tooltip in "%NUMBER likes" with the names * of the people who liked the post * */ void Post::setLikes(QVariantList likesList, int likesCount) { if (likesList.size() > 0) { QString peopleString; foreach (QVariant likesMap, likesList) { peopleString = likesMap.toMap().value("displayName").toString() + ", " + peopleString; } // Remove last comma and add "like this" peopleString.remove(-2, 2); // Last 2 characters QString likesString; if (likesList.size() == 1) { likesString = tr("%1 likes this", "One person").arg(peopleString); } else { likesString = tr("%1 like this", "More than one person").arg(peopleString); } likesString.append(""); // Turn the label into rich text this->postLikesCountLabel->setToolTip(likesString); } // TMP/FIXME this can set the number to lower than initially set // if called with the "initial" up-to-4 likes list that comes with the post if (likesCount != -1) { this->setLikesLabel(likesCount); // use the passed likesCount parameter } else { this->setLikesLabel(likesList.size()); // show size of actual names list } } /* * Update the "NUMBER likes" label in left side * */ void Post::setLikesLabel(int likesCount) { if (likesCount != 0) { if (likesCount == 1) { postLikesCountLabel->setText(QString::fromUtf8("\342\231\245 ") // heart symbol + tr("1 like")); } else { postLikesCountLabel->setText(QString::fromUtf8("\342\231\245 ") // heart symbol + tr("%1 likes").arg(likesCount)); } postLikesCountLabel->show(); } else { postLikesCountLabel->clear(); postLikesCountLabel->hide(); } } /* * Return the comments URL for this post * */ QString Post::commentsURL() { return this->postCommentsUrl; } /* * Ask the Commenter to set new comments * */ void Post::setComments(QVariantList commentsList) { this->commenter->setComments(commentsList); // update number of comments in left side counter this->setCommentsLabel(commentsList.size()); } /* * Update the "NUMBER comments" label in left side * */ void Post::setCommentsLabel(int commentsCount) { if (commentsCount != 0) { if (commentsCount == 1) { postCommentsCountLabel->setText(QString::fromUtf8("\342\234\215 ") // writing hand + tr("1 comment")); } else { postCommentsCountLabel->setText(QString::fromUtf8("\342\234\215 ") // writing hand + tr("%1 comments").arg(commentsCount)); } postCommentsCountLabel->show(); } else { postCommentsCountLabel->clear(); postCommentsCountLabel->hide(); } } /* * Return the shares URL for this post, * list of people who reshared it * */ QString Post::sharesURL() { return this->postSharesUrl; } /* * Update the tooltip for "%NUMBER shares" with names * */ void Post::setShares(QVariantList sharesList, int sharesCount) { if (sharesList.size() > 0) { QString peopleString; foreach (QVariant sharesMap, sharesList) { peopleString = sharesMap.toMap().value("displayName").toString() + ", " + peopleString; } // Remove last comma and add "shared this" peopleString.remove(-2, 2); // Last 2 characters QString sharesString; if (sharesList.size() == 1) { sharesString = tr("%1 shared this", "%1 = One person name").arg(peopleString); } else { sharesString = tr("%1 shared this", "%1 = Names for more than one person").arg(peopleString); } sharesString.append(""); // So that the label is rich text this->postResharesCountLabel->setToolTip(sharesString); } // TMP/FIXME this can set the number to lower than initially set // if called with the "initial" up-to-4 shares list that comes with the post if (sharesCount != -1) { this->setSharesLabel(sharesCount); // use the passed sharesCount parameter } else { this->setSharesLabel(sharesList.size()); // show size of actual names list } } void Post::setSharesLabel(int resharesCount) { if (resharesCount != 0) { if (resharesCount == 1) { postResharesCountLabel->setText(QString::fromUtf8("\342\231\273 ") // recycle symbol + tr("Shared once")); } else { postResharesCountLabel->setText(QString::fromUtf8("\342\231\273 ") // recycle symbol + tr("Shared %1 times").arg(resharesCount)); } postResharesCountLabel->show(); } else { postResharesCountLabel->clear(); postResharesCountLabel->hide(); } } /* * Set or unset the visual hint indicating if the post is unread * */ void Post::setPostUnreadStatus() { if (this->postIsUnread) { postAuthorNameLabel->setAutoFillBackground(true); postAuthorNameLabel->setForegroundRole(QPalette::HighlightedText); postAuthorNameLabel->setBackgroundRole(QPalette::Highlight); postCreatedAtLabel->setAutoFillBackground(true); postCreatedAtLabel->setForegroundRole(QPalette::HighlightedText); postCreatedAtLabel->setBackgroundRole(QPalette::Highlight); this->setAutoFillBackground(true); this->setBackgroundRole(QPalette::Mid); } else { postAuthorNameLabel->setAutoFillBackground(false); postAuthorNameLabel->setForegroundRole(QPalette::Foreground); postAuthorNameLabel->setBackgroundRole(QPalette::Background); postCreatedAtLabel->setAutoFillBackground(false); postCreatedAtLabel->setForegroundRole(QPalette::Foreground); postCreatedAtLabel->setBackgroundRole(QPalette::Background); this->setAutoFillBackground(false); this->setBackgroundRole(QPalette::Window); } } void Post::setPostAsNew() { this->postIsUnread = true; setPostUnreadStatus(); } void Post::setPostAsRead() { if (postIsUnread) { this->postIsUnread = false; emit postRead(); // Inform the Timeline() setPostUnreadStatus(); } } void Post::resizeEvent(QResizeEvent *event) { this->postWidth = postText->width() - 40; // -40 px to account for a possible scrollbar /FIXME if (postWidth < 50) { postWidth = 50; // minimum width! } setPostContents(); event->accept(); } /* * On mouse click in any part of the post, set it as read * */ void Post::mousePressEvent(QMouseEvent *event) { setPostAsRead(); event->accept(); } /* * Ensure we hide the highlighted URL label when the mouse leaves the post * */ void Post::leaveEvent(QEvent *event) { this->highlightedUrlLabel->clear(); this->highlightedUrlLabel->hide(); event->accept(); } /* * closeEvent, needed when posts are opened in separate window * */ void Post::closeEvent(QCloseEvent *event) { qDebug() << "Post::closeEvent()"; this->deleteLater(); event->accept(); } void Post::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { event->accept(); if (standalone) { this->close(); } } else { event->ignore(); } } //////////////////////////////////////////////////////////////////////////// ////////////////////////////////// SLOTS /////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /* * Like (favorite) a post * */ void Post::likePost(bool like) { qDebug() << "Post::likePost()" << (like ? "like" : "unlike"); this->pController->likePost(this->postId, this->postType, like); this->fixLikeButton(like ? "true" : "false"); connect(pController, SIGNAL(likeSet()), this, SLOT(getAllLikes())); } /* * Set the right labels and tooltips to the like button, depending on its state * */ void Post::fixLikeButton(QString state) { if (state == "true") { likeButton->setToolTip(tr("You like this")); likeButton->setText(tr("Unlike")); likeButton->setChecked(true); } else { likeButton->setToolTip(tr("Like this post")); likeButton->setText(tr("Like")); likeButton->setChecked(false); } } void Post::getAllLikes() { disconnect(pController, SIGNAL(likeSet()), this, SLOT(getAllLikes())); this->pController->getPostLikes(this->postLikesUrl); } /* * Make the commenter widget visible, so user can type the comment * */ void Post::commentOnPost() { qDebug() << "Commenting on this post"; QString initialText; QString selectedText = this->postText->textCursor().selectedText(); if (!selectedText.isEmpty()) { initialText = MiscHelpers::quotedText(this->postAuthorName, selectedText); } this->commenter->setFullMode(initialText); emit commentingOnPost(this->commenter); } /* * The actual sending of the comment to the Pump controller * */ void Post::sendComment(QString comment) { qDebug() << "About to publish this comment:" << comment; this->pController->addComment(MiscHelpers::cleanupHtml(comment), this->postId, this->postType); } void Post::getAllComments() { this->pController->getPostComments(this->postCommentsUrl); } /* * Set all comments received from signal, when post is a separate window, * and not handled by Timeline() * */ void Post::setAllComments(QVariantList commentsList, QString originatingPostUrl) { QString originatingPostCleanUrl = originatingPostUrl.split("?").at(0); if (this->commentsURL() == originatingPostCleanUrl) { this->setComments(commentsList); } } /* * Re-share a post * */ void Post::sharePost() { int confirmation = QMessageBox::question(this, tr("Share post?"), tr("Do you want to share %1's post?").arg(this->postAuthorNameLabel->text()), tr("&Yes, share it"), tr("&No"), "", 1, 1); if (confirmation == 0) { qDebug() << "Sharing this post:" << this->postId; this->pController->sharePost(this->postId, this->postType); } else { qDebug() << "Confirmation canceled, not sharing"; } } void Post::unsharePost() { int confirmation = QMessageBox::question(this, tr("Unshare post?"), tr("Do you want to unshare %1's post?").arg(this->postAuthorNameLabel->text()), tr("&Yes, unshare it"), tr("&No"), "", 1, 1); if (confirmation == 0) { qDebug() << "Unsharing this post:" << this->postId; this->pController->unsharePost(this->postId, this->postType); this->setDisabled(true); // Disable the widget, to let user know it's been unshared } else { qDebug() << "Confirmation canceled, will not unshare"; } } /* * Set the Publisher in editing mode with this post's contents * */ void Post::editPost() { emit postEditRequested(this->postId, this->postOriginalText, this->postTitle); } /* * Delete a post * */ void Post::deletePost() { int confirmation = QMessageBox::question(this, tr("WARNING: Delete post?"), tr("Are you sure you want to delete this post?"), tr("&Yes, delete it"), tr("&No"), "", 1, 1); if (confirmation == 0) { qDebug() << "Deleting post"; this->pController->deletePost(this->postId, this->postType); this->setDisabled(true); // disable... maybe hide? } else { qDebug() << "Confirmation cancelled, not deleting the post"; } } void Post::openClickedURL(QUrl url) { qDebug() << "Anchor URL clicked:" << url.toString(); if (url.scheme() == "image") // Use our own viewer, or maybe a configured external viewer { qDebug() << "Opening this image in our own viewer..."; ImageViewer *viewer = new ImageViewer(url.toString(), this->postTitle); viewer->show(); } else { qDebug() << "Opening this link in browser"; QDesktopServices::openUrl(url); } } void Post::showHighlightedURL(QUrl url) { QString urlString = url.toString(); if (!urlString.isEmpty()) { if (urlString.startsWith("image://")) // Own image:// URL { this->highlightedUrlLabel->setText(tr("Click the image to see it in full size")); } else // Normal URL { this->highlightedUrlLabel->setText(tr("Link to: %1").arg(urlString)); qDebug() << "Highlighted url:" << urlString; } this->highlightedUrlLabel->show(); } else { this->highlightedUrlLabel->clear(); this->highlightedUrlLabel->hide(); } } void Post::openAuthorProfileInBrowser() { QDesktopServices::openUrl(this->postAuthorUrl); } void Post::openPostInBrowser() { QDesktopServices::openUrl(this->postUrl); } void Post::copyPostUrlToClipboard() { QApplication::clipboard()->setText(this->postUrl); } void Post::followUser() { this->pController->followContact(this->postAuthorId); postAuthorFollowed = true; // FIXME: should wait until confirmed by server... this->setFollowUnfollow(); } void Post::unfollowUser() { int confirmation = QMessageBox::question(this, tr("Stop following?"), tr("Are you sure you want to stop following %1?") .arg(this->postAuthorId), tr("&Yes, stop following"), tr("&No"), "", 1, 1); if (confirmation == 0) { this->pController->unfollowContact(this->postAuthorId); postAuthorFollowed = false; // FIXME: should wait until confirmed by server... this->setFollowUnfollow(); } } /* * Normalize text colors; Used from menu when a post * has white text with white background, or similar * */ void Post::normalizeTextFormat() { postText->selectAll(); postText->setTextColor(qApp->palette().windowText().color()); postText->setTextBackgroundColor(QColor(Qt::transparent)); // Select 'none' postText->moveCursor(QTextCursor::Start); postText->textCursor().select(QTextCursor::WordUnderCursor); } /* * Trigger a resizeEvent, which will call * setPostContents() and setPostHeight() * */ void Post::triggerResize() { this->resize(this->width() -1, this->height() -1); } /* * Redraw post author's avatar after receiving it * */ void Post::redrawAvatar(QString avatarUrl, QString avatarFilename) { if (avatarUrl == this->postAuthorAvatarUrl) { postAuthorAvatarButton->setIcon(QIcon(QPixmap(avatarFilename).scaled(48, 48, Qt::KeepAspectRatio, Qt::SmoothTransformation))); disconnect(pController, SIGNAL(avatarStored(QString,QString)), this, SLOT(redrawAvatar(QString,QString))); } } /* * Redraw post contents after receiving downloaded images * */ void Post::redrawImages(QString imageUrl) { if (pendingImagesList.contains(imageUrl)) { this->pendingImagesList.removeAll(imageUrl); if (pendingImagesList.isEmpty()) // If there are no more, disconnect { disconnect(pController, SIGNAL(imageStored(QString)), this, SLOT(redrawImages(QString))); } // Trigger resizeEvent(), with setPostContents(), etc. triggerResize(); } } dianara-v1.1/src/images/000755 000764 000764 00000000000 12157073165 014575 5ustar00janjan000000 000000 dianara-v1.1/src/profileeditor.h000664 000764 000764 00000004317 12260657141 016354 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef PROFILEEDITOR_H #define PROFILEEDITOR_H #include #include #include #include #include #include #include #include #include #include #include #include "pumpcontroller.h" #include "mischelpers.h" class ProfileEditor : public QWidget { Q_OBJECT public: explicit ProfileEditor(PumpController *pumpController, QWidget *parent = 0); ~ProfileEditor(); void setProfileData(QString avatarUrl, QString fullName, QString hometown, QString bio, QString eMail); signals: public slots: void findAvatarFile(); void saveProfile(); void sendProfileData(QString newImageUrl = QString()); private: PumpController *pController; QFormLayout *mainLayout; QHBoxLayout *avatarLayout; QHBoxLayout *bottomLayout; QLabel *avatarLabel; QPushButton *changeAvatarButton; bool avatarChanged; QLabel *webfingerLabel; QLabel *emailLabel; QLineEdit *fullNameLineEdit; QLineEdit *hometownLineEdit; QTextEdit *bioTextEdit; QPushButton *saveButton; QPushButton *cancelButton; QAction *cancelAction; QString currentAvatarURL; QString newAvatarFilename; QString newAvatarContentType; }; #endif // PROFILEEDITOR_H dianara-v1.1/src/publisher.h000664 000764 000764 00000007201 12260657135 015500 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef PUBLISHER_H #define PUBLISHER_H #include #include #include #include #include #include #include #include #include #include #include "composer.h" #include "pumpcontroller.h" #include "mischelpers.h" #include "audienceselector.h" class Publisher : public QWidget { Q_OBJECT public: explicit Publisher(PumpController *pumpController, QWidget *parent = 0); ~Publisher(); void setDefaultPublicPosting(bool defaultPublicPosts); void setEmptyPictureData(); void updatePublicFollowersLabels(); QMap getAudienceMap(); signals: public slots: void setMinimumMode(); void setFullMode(); void setPictureMode(); void setEditingMode(QString postID, QString postText, QString postTitle); void onPublishingOk(); void onPublishingFailed(); void setToPublic(bool activated); void setToFollowers(bool activated); void setCCPublic(bool activated); void setCCFollowers(bool activated); void updateToCcFields(QString selectorType, QStringList contactsList); void updateListsMenus(QVariantList listsList); void updateToListsFields(QAction *listAction); void updateCcListsFields(QAction *listAction); void sendPost(); void findPictureFile(); private: QGridLayout *mainLayout; QHBoxLayout *titleLayout; QLabel *titleLabel; QLineEdit *titleLineEdit; QLabel *pictureInfoLabel; QPushButton *selectPictureButton; QLabel *pictureLabel; QPushButton *toolsButton; Composer *composerBox; QPushButton *toSelectorButton; QAction *toPublicAction; QAction *toFollowersAction; QMenu *toSelectorMenu; QMenu *toSelectorListsMenu; QPushButton *ccSelectorButton; QAction *ccPublicAction; QAction *ccFollowersAction; QMenu *ccSelectorMenu; QMenu *ccSelectorListsMenu; AudienceSelector *audienceSelectorTo; AudienceSelector *audienceSelectorCC; QLabel *toPublicFollowersLabel; QLabel *toAudienceLabel; QLabel *ccPublicFollowersLabel; QLabel *ccAudienceLabel; QStringList toAddressStringList; QStringList ccAddressStringList; QStringList toListsNameStringList; QStringList toListsIdStringList; QStringList ccListsNameStringList; QStringList ccListsIdStringList; bool defaultPublicPosting; QPushButton *pictureButton; QLabel *statusInfoLabel; QPushButton *postButton; QPushButton *cancelButton; QString pictureFilename; QString pictureContentType; QString lastUsedDirectory; bool editingMode; QString editingPostId; PumpController *pController; }; #endif // PUBLISHER_H dianara-v1.1/src/notifications.h000664 000764 000764 00000003046 12260657137 016361 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef NOTIFICATIONS_H #define NOTIFICATIONS_H #include #include #include #include #include #include class FDNotifications : public QObject { Q_OBJECT public: FDNotifications(); ~FDNotifications(); bool areNotificationsAvailable(); void setNotificationType(int type); enum notificationTypes { SystemNotifications, FallbackNotifications, NoNotifications }; signals: void showFallbackNotification(QString message); public slots: void showMessage(QString message); private: bool notificationsAvailable; int notificationType; QDBusConnection *bus; }; #endif // NOTIFICATIONS_H dianara-v1.1/src/notifications.cpp000664 000764 000764 00000010724 12260657140 016707 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "notifications.h" FDNotifications::FDNotifications() { qDebug() << "Creating FreeDesktop Notifier"; this->notificationsAvailable = false; // Init as false, detect later this->bus = new QDBusConnection(QDBusConnection::sessionBus()); if (bus->isConnected()) { QDBusReply busReply = bus->interface()->registeredServiceNames(); qDebug() << "Listing DBus services"; if (busReply.isValid()) { foreach (QString serviceName, busReply.value()) { if (serviceName == "org.freedesktop.Notifications") { qDebug() << "org.freedesktop.Notifications service found!"; this->notificationsAvailable = true; } } } } else { qDebug() << "DBus unavailable!"; notificationsAvailable = false; } qDebug() << "FreeDesktop Notifier created"; } FDNotifications::~FDNotifications() { qDebug() << "FreeDesktop Notifier destroyed"; } bool FDNotifications::areNotificationsAvailable() { qDebug() << "Are notifications available?" << notificationsAvailable; return notificationsAvailable; } void FDNotifications::setNotificationType(int type) { this->notificationType = type; qDebug() << "Notification type set to" << type << "-- FD.o/Qt/none"; } //////////////////////////// SLOTS ////////////////////////////////// void FDNotifications::showMessage(QString message) { // if notifications are disabled if (notificationType == FDNotifications::NoNotifications) { return; } // If FD.org notifications are not available, or Qt's Balloon ones are selected if (!notificationsAvailable || notificationType == FDNotifications::FallbackNotifications) { qDebug() << "FreeDesktop Notifications are NOT available, or balloon notifications selected"; // Clean up possible HTML, since Qt's balloons don't support it message.remove(""); message.remove(""); message.remove(""); message.remove(""); message.remove(""); message.remove(""); // FIXME: remove also emit showFallbackNotification(message); return; } message.replace("\n", "
"); // use HTML newlines QDBusMessage dBusMessage = QDBusMessage::createMethodCall( "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "", "Notify"); // --- DBus Notify call ---------------------------------------------- // method uint org.freedesktop.Notifications.Notify(QString app_name, // uint replaces_id, QString app_icon, QString summary, QString body, // QStringList actions, QVariantMap hints, int timeout) QList arguments; arguments << QString("Dianara"); // app_name arguments << uint(0); // replaces_id if (QIcon::hasThemeIcon("dianara")) { arguments << QString("dianara"); // app_icon, if "dianara" icon is in theme } else { arguments << QString("dialog-information"); // app_icon otherwise } arguments << QString(tr("Dianara Notification")); arguments << message; arguments << QStringList(); // actions arguments << QVariantMap(); // hints arguments << 5000; // timeout, milliseconds dBusMessage.setArguments(arguments); qDebug() << "sending DBUS call to org.freedesktop.Notifications Notify"; this->bus->asyncCall(dBusMessage); } dianara-v1.1/src/post.h000664 000764 000764 00000011601 12263112162 014454 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef POST_H #define POST_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "timestamp.h" #include "mischelpers.h" #include "pumpcontroller.h" #include "commenterblock.h" #include "imageviewer.h" #include "asactivity.h" class Post : public QFrame { Q_OBJECT public: Post(PumpController *pumpController, ASActivity *activity, bool isStandalone = false, QWidget *parent = 0); ~Post(); void createAvatarMenu(); void setFollowUnfollow(); void setMinMaxHeight(int newMinMaxHeight); void setPostContents(); void setPostHeight(); void enqueueImageForDownload(QString url); QString likesURL(); void setLikes(QVariantList likesList, int likesCount=-1); void setLikesLabel(int likesCount); QString commentsURL(); void setComments(QVariantList commentsList); void setCommentsLabel(int commentsCount); QString sharesURL(); void setShares(QVariantList sharesList, int sharesCount=-1); void setSharesLabel(int resharesCount); void setPostUnreadStatus(); void setPostAsNew(); void setPostAsRead(); signals: void postRead(); void postEditRequested(QString originalPostID, QString contents, QString title); void commentingOnPost(QWidget *commenterWidget); public slots: void likePost(bool like); void fixLikeButton(QString state); void getAllLikes(); void commentOnPost(); void sendComment(QString comment); void getAllComments(); void setAllComments(QVariantList commentsList, QString originatingPostUrl); void sharePost(); void unsharePost(); void editPost(); void deletePost(); void openClickedURL(QUrl url); void showHighlightedURL(QUrl url); void openAuthorProfileInBrowser(); void openPostInBrowser(); void copyPostUrlToClipboard(); void followUser(); void unfollowUser(); void normalizeTextFormat(); void triggerResize(); void redrawAvatar(QString avatarUrl, QString avatarFilename); void redrawImages(QString imageUrl); protected: virtual void resizeEvent(QResizeEvent *event); virtual void mousePressEvent(QMouseEvent *event); virtual void leaveEvent(QEvent *event); virtual void closeEvent(QCloseEvent *event); virtual void keyPressEvent(QKeyEvent *event); private: QHBoxLayout *mainLayout; QVBoxLayout *leftColumnLayout; QVBoxLayout *rightColumnLayout; QString postId; QString postType; QString postUrl; QString postAuthorId; QString postAuthorName; QString postAuthorUrl; QString postAuthorAvatarUrl; bool postAuthorFollowed; QString postSharedById; bool postIsOwn; bool postIsUnread; QPushButton *postAuthorAvatarButton; QMenu *avatarMenu; QAction *avatarMenuIdAction; QAction *avatarMenuFollowAction; QLabel *postAuthorNameLabel; QLabel *postCreatedAtLabel; QLabel *postLocationLabel; QLabel *postToLabel; QLabel *postCCLabel; QLabel *postIsSharedLabel; QLabel *postLikesCountLabel; QLabel *postCommentsCountLabel; QLabel *postResharesCountLabel; QLabel *postTitleLabel; QVBoxLayout *postTextLayout; QTextBrowser *postText; QLabel *highlightedUrlLabel; QHBoxLayout *buttonsLayout; QPushButton *likeButton; QPushButton *commentButton; QPushButton *shareButton; QPushButton *editButton; QPushButton *deleteButton; QString postTitle; QString postImageUrl; QString postOriginalText; int postWidth; int minMaxHeight; QString postLikesUrl; QString postCommentsUrl; QString postSharesUrl; QStringList pendingImagesList; bool standalone; PumpController *pController; CommenterBlock *commenter; }; #endif // POST_H dianara-v1.1/src/timeline.h000664 000764 000764 00000006063 12260657136 015317 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef TIMELINE_H #define TIMELINE_H #include #include #include #include #include #include #include #include "pumpcontroller.h" #include "asobject.h" #include "asactivity.h" #include "post.h" class TimeLine : public QWidget { Q_OBJECT public: TimeLine(int timelineType, PumpController *pumpController, QWidget *parent = 0); ~TimeLine(); void clearTimeLineContents(); void requestTimelinePage(); void updateCurrentPageNumber(); void setMinMaxHeightForPosts(int newMinMaxHeightForPosts); void resizePosts(); void markPostsAsRead(); enum TimelineTypes { TimelineTypeMain, TimelineTypeDirect, TimelineTypeActivity, TimelineTypeFavorites }; signals: void getPreviousPage(); void getNextPage(); void scrollToTop(); void timelineRendered(int timelineType, int newPostCount); void unreadPostsCountChanged(int timelineType, int newPostCount); void postEditRequested(QString originalPostID, QString contents, QString title); void commentingOnPost(QWidget *commenterWidget); public slots: void setTimeLineContents(QVariantList postList, int postsPerPage, QString previousLink, QString nextLink); void setLikesInPost(QVariantList likesList, QString originatingPostURL); void setCommentsInPost(QVariantList commentsList, QString originatingPostURL); void goToFirstPage(); void goToPreviousPage(); void goToNextPage(); void decreaseUnreadPostsCount(); protected: private: QVBoxLayout *mainLayout; QVBoxLayout *postsLayout; QHBoxLayout *bottomLayout; PumpController *pController; QPushButton *firstPageButton; QLabel *currentPageLabel; QPushButton *previousPageButton; QPushButton *nextPageButton; QString previousPageLink; QString nextPageLink; int timelineType; int timelineOffset; int postsPerPage; int unreadPostsCount; bool postIsNew; int minMaxHeightForPosts; QString previousNewestPostId; QList postsInTimeline; }; #endif // TIMELINE_H dianara-v1.1/src/timeline.cpp000664 000764 000764 00000043677 12260657136 015666 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "timeline.h" TimeLine::TimeLine(int timelineType, PumpController *pumpController, QWidget *parent) : QWidget(parent) { this->timelineType = timelineType; this->pController = pumpController; // Simulated data for demo posts QVariantMap demoLocationData; demoLocationData.insert("displayName", "Demoville"); QVariantMap demoAuthorData; demoAuthorData.insert("displayName", "Demo User"); demoAuthorData.insert("id", "demo@somepump.example"); demoAuthorData.insert("location", demoLocationData); demoAuthorData.insert("summary", "I am not a real user"); QVariantMap demoGeneratorData; demoGeneratorData.insert("displayName", "Dianara"); QVariantMap demoObjectData; demoObjectData.insert("published", "2013-05-01T00:00:00Z"); QSettings settings; // FIXME: kinda tmp, until posts have "unread" status, etc. settings.beginGroup("TimelineStates"); // Demo post content depends on timeline type switch (this->timelineType) { case TimelineTypeMain: demoObjectData.insert("displayName", tr("Welcome to Dianara")); demoObjectData.insert("content", tr("Dianara is a pump.io client.") + "
" + tr("If you don't have a Pump account yet, you can get one " "at the following address:") + "
" "
http://pump.io/tryit.html" "

" + tr("First, configure your account from the " "Settings - Account menu.") + "
" + tr("After the process is done, your profile " "and timelines should update automatically.") + "

" + tr("Take a moment to look around the menus and " "the Configuration window.") + "

" + tr("You can also set your profile data and picture from " "the Settings - Edit Profile menu.") + "

" + tr("There are tooltips everywhere, so if you hover " "a button or a text field with your mouse, you'll " "probably see some extra information.") + "

" + "" + tr("Dianara's blog") + "

" "" + tr("Frequently asked questions about pump.io") + "" + "

"); this->previousNewestPostId = settings.value("previousNewestPostIdMain", "").toString(); break; case TimelineTypeDirect: demoObjectData.insert("displayName", tr("Direct Messages Timeline")); demoObjectData.insert("content", tr("Here, you'll see posts specifically directed to you.") + "

"); this->previousNewestPostId = settings.value("previousNewestPostIdDirect", "").toString(); break; case TimelineTypeActivity: demoObjectData.insert("displayName", tr("Activity Timeline")); demoObjectData.insert("content", tr("You'll see your own posts here.") + "

"); this->previousNewestPostId = settings.value("previousNewestPostIdActivity", "").toString(); break; case TimelineTypeFavorites: demoObjectData.insert("displayName", tr("Favorites Timeline")); demoObjectData.insert("content", tr("Posts and comments you've liked.") + "

"); this->previousNewestPostId = settings.value("previousNewestPostIdFavorites", "").toString(); break; default: demoObjectData.insert("content", "

Empty timeline

"); } settings.endGroup(); QVariantMap demoPostData; demoPostData.insert("actor", demoAuthorData); demoPostData.insert("generator", demoGeneratorData); demoPostData.insert("object", demoObjectData); this->unreadPostsCount = 0; this->timelineOffset = 0; this->minMaxHeightForPosts = 400; firstPageButton = new QPushButton(QIcon::fromTheme("go-first"), tr("F&irst Page")); connect(firstPageButton, SIGNAL(clicked()), this, SLOT(goToFirstPage())); currentPageLabel = new QLabel(); currentPageLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); currentPageLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); previousPageButton = new QPushButton(QIcon::fromTheme("go-previous"), tr("&Previous Page")); connect(previousPageButton, SIGNAL(clicked()), this, SLOT(goToPreviousPage())); nextPageButton = new QPushButton(QIcon::fromTheme("go-next"), tr("&Next Page")); connect(nextPageButton, SIGNAL(clicked()), this, SLOT(goToNextPage())); ///// Layout mainLayout = new QVBoxLayout(); postsLayout = new QVBoxLayout(); postsLayout->setAlignment(Qt::AlignTop); mainLayout->addLayout(postsLayout, 1); mainLayout->addSpacing(2); // 2 pixel separation bottomLayout = new QHBoxLayout(); bottomLayout->addWidget(firstPageButton); bottomLayout->addSpacing(8); bottomLayout->addWidget(currentPageLabel); bottomLayout->addSpacing(8); bottomLayout->addWidget(previousPageButton); bottomLayout->addWidget(nextPageButton); mainLayout->addLayout(bottomLayout, 0); this->setLayout(mainLayout); // Add the default "demo" post ASActivity *demoActivity = new ASActivity(demoPostData, this); Post *demoPost = new Post(pController, demoActivity, false, this); demoPost->setMinMaxHeight(this->minMaxHeightForPosts); postsInTimeline.append(demoPost); postsLayout->addWidget(demoPost); qDebug() << "TimeLine created"; } /* * Destructor stores timeline states in the settings * */ TimeLine::~TimeLine() { QSettings settings; settings.beginGroup("TimelineStates"); switch (timelineType) { case TimelineTypeMain: settings.setValue("previousNewestPostIdMain", this->previousNewestPostId); break; case TimelineTypeDirect: settings.setValue("previousNewestPostIdDirect", this->previousNewestPostId); break; case TimelineTypeActivity: settings.setValue("previousNewestPostIdActivity", this->previousNewestPostId); break; case TimelineTypeFavorites: settings.setValue("previousNewestPostIdFavorites", this->previousNewestPostId); break; } settings.endGroup(); qDebug() << "TimeLine destroyed"; } /* * Remove all widgets (Post *) from the timeline * */ void TimeLine::clearTimeLineContents() { foreach (Post *oldPost, postsInTimeline) { this->mainLayout->removeWidget(oldPost); delete oldPost; } this->postsInTimeline.clear(); qApp->processEvents(); // So GUI gets updated } void TimeLine::requestTimelinePage() { switch (this->timelineType) { case TimelineTypeMain: this->pController->getMainTimeline(this->timelineOffset); break; case TimelineTypeDirect: this->pController->getDirectTimeline(this->timelineOffset); break; case TimelineTypeActivity: this->pController->getActivityTimeline(this->timelineOffset); break; case TimelineTypeFavorites: this->pController->getFavoritesTimeline(this->timelineOffset); break; } } /* * Update the label at the bottom of the page, indicating current "page" * */ void TimeLine::updateCurrentPageNumber() { QString currentPageString; currentPageString = QString("%1").arg((this->timelineOffset / this->postsPerPage) + 1); this->currentPageLabel->setText(currentPageString); } void TimeLine::setMinMaxHeightForPosts(int newMinMaxHeightForPosts) { this->minMaxHeightForPosts = newMinMaxHeightForPosts; } /* * Resize all posts in timeline * */ void TimeLine::resizePosts() { foreach (Post *post, postsInTimeline) { post->setMinMaxHeight(this->minMaxHeightForPosts); // Force a Post() resizeEvent, which will call // setPostContents() and setPostHeight() post->resize(post->width() - 1, post->height() - 1); } } void TimeLine::markPostsAsRead() { foreach (Post *post, postsInTimeline) { post->setPostAsRead(); } } /*********************************************************/ /*********************************************************/ /************************ SLOTS **************************/ /*********************************************************/ /*********************************************************/ void TimeLine::setTimeLineContents(QVariantList postList, int postsPerPage, QString previousLink, QString nextLink) { qDebug() << "TimeLine::setTimeLineContents()"; // Remove all previous posts in timeline qDebug() << "Removing previous posts from timeline"; this->clearTimeLineContents(); emit scrollToTop(); // ask mainWindow to scroll the QScrollArea containing the timeline to the top // Hide the widget while it reloads; helps performance a lot this->hide(); this->postsPerPage = postsPerPage; int newPostCount = 0; bool allNewPostsCounted = false; QString newestPostId; // Here we'll store the postID for the first (newest) post in the timeline // With it, we can know how many new posts (if any) we receive next time // Fill timeline with new contents foreach (QVariant singlePost, postList) { if (singlePost.type() == QVariant::Map) { this->previousPageLink = previousLink; // Useless at the moment this->nextPageLink = nextLink; qDebug() << "Prev/Next links:"; qDebug() << previousPageLink; qDebug() << nextPageLink; this->postIsNew = false; QVariantMap activityMap; // Since "favorites" is a collection of objects, not activities, // we need to put "Favorites" posts into fake activities if (this->timelineType != TimelineTypeFavorites) { // Data is already an activity activityMap = singlePost.toMap(); } else { // Put object into the empty/fake VariantMap for the activity activityMap.insert("object", singlePost.toMap()); activityMap.insert("actor", singlePost.toMap().value("author").toMap()); activityMap.insert("id", singlePost.toMap().value("id").toString()); } ASActivity *activity = new ASActivity(activityMap, this); QString postDeletedTime = activity->object()->getDeletedTime(); if (newestPostId.isEmpty()) // only first time, for newest post { if (this->timelineOffset == 0) { newestPostId = activity->getId(); } else { newestPostId = this->previousNewestPostId; allNewPostsCounted = true; } } if (!allNewPostsCounted) { if (activity->getId() == this->previousNewestPostId) { allNewPostsCounted = true; } else { // If post is NOT deleted, and not ours, add it to the count if (postDeletedTime.isEmpty() && activity->getAuthorId() != pController->currentUserId()) { ++newPostCount; // Mark current post as new postIsNew = true; } } } if (postDeletedTime.isEmpty()) // if post was NOT deleted { Post *newPost = new Post(pController, activity, false, // NOT standalone this); if (postIsNew) { newPost->setPostAsNew(); connect(newPost, SIGNAL(postRead()), this, SLOT(decreaseUnreadPostsCount())); } this->postsInTimeline.append(newPost); this->postsLayout->addWidget(newPost); connect(newPost, SIGNAL(postEditRequested(QString,QString,QString)), this, SIGNAL(postEditRequested(QString,QString,QString))); connect(newPost, SIGNAL(commentingOnPost(QWidget*)), this, SIGNAL(commentingOnPost(QWidget*))); } else { // If there's a "deleted" key, ignore this post qDebug() << "This post was deleted on" << postDeletedTime << " / Not adding."; delete activity; } } else // singlePost.type() is not a QVariant::Map { qDebug() << "Expected a Map, got something else"; qDebug() << postList; } qApp->processEvents(); // Avoid GUI freeze } // end foreach this->previousNewestPostId = newestPostId; this->unreadPostsCount = newPostCount; qDebug() << "New posts:" << newPostCount << "; Newest post ID:" << previousNewestPostId; this->updateCurrentPageNumber(); this->resizePosts(); emit timelineRendered(this->timelineType, unreadPostsCount); // Show timeline again, since everything is added and drawn this->show(); qDebug() << "setTimeLineContents() /END"; } /* * Add the full list of likes to a post * */ void TimeLine::setLikesInPost(QVariantList likesList, QString originatingPostURL) { qDebug() << "TimeLine::setLikesInPost()"; QString originatingPostCleanUrl = originatingPostURL.split("?").at(0); qDebug() << "Originating post URL:" << originatingPostCleanUrl; // Look for the originating Post() object qDebug() << "Looking for the originating Post() object"; foreach (Post *post, postsInTimeline) { qDebug() << "Checking if" << post->likesURL() << "==" << originatingPostCleanUrl; if (post->likesURL() == originatingPostCleanUrl) { qDebug() << "Found originating Post; setting likes on it..."; post->setLikes(likesList); break; } } } /* * Add the full list of comments to a post * */ void TimeLine::setCommentsInPost(QVariantList commentsList, QString originatingPostURL) { qDebug() << "TimeLine::setCommentsInPost()"; QString originatingPostCleanURL = originatingPostURL.split("?").at(0); qDebug() << "Originating post URL:" << originatingPostCleanURL; // Look for the originating Post() object qDebug() << "Looking for the originating Post() object"; foreach (Post *post, postsInTimeline) { qDebug() << "Checking if" << post->commentsURL() << "==" << originatingPostCleanURL; if (post->commentsURL() == originatingPostCleanURL) { qDebug() << "Found originating Post; setting comments on it..."; post->setComments(commentsList); // break; /* Don't break, so comments get set in copies of the post too, like if JohnDoe posted something and JaneDoe shared it soon after, so both the original post and its shared copy are visible in the timeline. */ } } } void TimeLine::goToFirstPage() { qDebug() << "TimeLine::goToFirstPage()"; this->timelineOffset = 0; this->requestTimelinePage(); } void TimeLine::goToPreviousPage() { qDebug() << "TimeLine::goToPreviousPage()"; this->timelineOffset -= this->postsPerPage; if (timelineOffset < 0) { timelineOffset = 0; } this->requestTimelinePage(); } void TimeLine::goToNextPage() { qDebug() << "TimeLine::goToNextPage()"; this->timelineOffset += this->postsPerPage; this->requestTimelinePage(); } /* * Decrease internal counter of unread posts (by 1), and inform * the parent window, so it can update its tab titles * */ void TimeLine::decreaseUnreadPostsCount() { --unreadPostsCount; emit unreadPostsCountChanged(this->timelineType, this->unreadPostsCount); } dianara-v1.1/src/markdown.h000664 000764 000764 00000002377 12260657136 015337 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef MARKDOWN_H #define MARKDOWN_H #include #include #include #include #include #include "mischelpers.h" class Markdown : public QObject { Q_OBJECT public: explicit Markdown(QObject *parent = 0); static QString toHtml(QString markdownString); static QStringList imageList(QString markdownString); signals: public slots: }; #endif // MARKDOWN_H dianara-v1.1/src/markdown.cpp000664 000764 000764 00000022075 12260657136 015667 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "markdown.h" Markdown::Markdown(QObject *parent) : QObject(parent) { // Creating object not required } QString Markdown::toHtml(QString markdownString) { //qDebug() << "Markdown::toHtml():" << markdownString; int foundMatch; // Remove DOCTYPE part markdownString.remove(""); // FIXME: it's hardcoded for now ///////////////////////////////////////////////////////// Headers // Header 1 QRegExp header1RE(">#\\s(.+)<"); // Match after ">", since it comes from HTML header1RE.setMinimal(true); // so either

or
or some tag do { foundMatch = header1RE.indexIn(markdownString); if (foundMatch > -1) { markdownString.replace(header1RE.cap(0), ">

" + header1RE.cap(1) +"

<"); } // restore the matched ">" and "<" too!! } while (foundMatch != -1); // Header 2 QRegExp header2RE(">##\\s(.+)<"); header2RE.setMinimal(true); do { foundMatch = header2RE.indexIn(markdownString); if (foundMatch > -1) { markdownString.replace(header2RE.cap(0), ">

" + header2RE.cap(1) +"

<"); } } while (foundMatch != -1); // Header 3 QRegExp header3RE(">###\\s(.+)<"); header3RE.setMinimal(true); do { foundMatch = header3RE.indexIn(markdownString); if (foundMatch > -1) { markdownString.replace(header3RE.cap(0), ">

" + header3RE.cap(1) +"

<"); } } while (foundMatch != -1); // Header 4 QRegExp header4RE(">####\\s(.+)<"); header4RE.setMinimal(true); do { foundMatch = header4RE.indexIn(markdownString); if (foundMatch > -1) { markdownString.replace(header4RE.cap(0), ">

" + header4RE.cap(1) +"

<"); } } while (foundMatch != -1); ///////////////////////////////////////////////////////// Bold, italic // bold + italic: ***text*** QRegExp boldItalicRE("\\*\\*\\*([^\\\\]+)\\*\\*\\*"); boldItalicRE.setMinimal(true); do { foundMatch = boldItalicRE.indexIn(markdownString); if (foundMatch > -1) { markdownString.replace(boldItalicRE.cap(0), "" + boldItalicRE.cap(1) + ""); } } while (foundMatch != -1); // bold: **text** QRegExp boldRE("\\*\\*([^\\\\]+)\\*\\*"); boldRE.setMinimal(true); do { foundMatch = boldRE.indexIn(markdownString); if (foundMatch > -1) { markdownString.replace(boldRE.cap(0), "" + boldRE.cap(1) + ""); } } while (foundMatch != -1); // italic: *text* QRegExp italicRE("\\*([^\\\\]+)\\*"); italicRE.setMinimal(true); do { foundMatch = italicRE.indexIn(markdownString); if (foundMatch > -1) { markdownString.replace(italicRE.cap(0), "" + italicRE.cap(1) +""); } } while (foundMatch != -1); // Image+link: [ ![image-alt-text](image-url) ](URL) QRegExp imagelinkRE("\\[\\s*!\\[(.*)\\]\\((.*)\\)\\s*\\]\\((.*)\\)"); imagelinkRE.setMinimal(true); do { foundMatch = imagelinkRE.indexIn(markdownString); if (foundMatch > -1) { QString imageURL = imagelinkRE.cap(2).trimmed(); QString imageFilename = MiscHelpers::getCachedImageFilename(imageURL); QString linkURL = imagelinkRE.cap(3).trimmed(); //qDebug() << "image+link found:" << imagelinkRE.cap(0) << imagelinkRE.cap(1) << imagelinkRE.cap(2) << imagelinkRE.cap(3); markdownString.replace(imagelinkRE.cap(0), "\"""); ////////////////////// ////////////////////// ////////////////////// ////////////////////// // TODO: FIX THIS! ////////////////////// ////////////////////// ////////////////////// ////////////////////// } } while (foundMatch != -1); // Image: ![alt-text](URL) QRegExp imageRE("!\\[(.*)\\]\\((.*)\\)"); imageRE.setMinimal(true); do { foundMatch = imageRE.indexIn(markdownString); if (foundMatch > -1) { QString imageURL = imageRE.cap(2).trimmed(); //qDebug() << "Post with image:=" << imageURL; //QString imageFilename = "file:///cache.path/" + imageURL.toUtf8().toBase64(); QString imageFilename = MiscHelpers::getCachedImageFilename(imageURL); //qDebug() << "Local filename:" << imageFilename; // TODO // Send SIGNAL() to get imageURL and store as imageFilename // done in ::imageList() method for now / FIXME? markdownString.replace(imageRE.cap(0), "\"""); } } while (foundMatch != -1); qDebug() << "markdown::tohtml after image"; /* * Non-markdown links (bare links) conversion to HTML links comes first, * to avoid messing the markdown-links * Some other checks are needed, like checking there's no "(" before "http" * --- FIXME --- */ // Non-markdown link: http://something... // "http" preceded by space or ;"> from Qt's toHtml() conversion (FIXME!) QRegExp bareLinkRE("(\\s+|;\">)(https?://.+)(\\s+|<)"); bareLinkRE.setMinimal(true); foundMatch = 0; do { foundMatch = bareLinkRE.indexIn(markdownString, foundMatch); //qDebug() << "\n\n---\n\nfoundMatch:" << foundMatch; //qDebug() << markdownString; if (foundMatch > -1) { QString htmlLink = "" + bareLinkRE.cap(2) +""; //qDebug() << "barelinkRE, about to replace()"; //qDebug() << "foundMatch = " << foundMatch; //qDebug() << "markdownString length:" << markdownString.length(); markdownString.replace(bareLinkRE.cap(2), // replace only the link itself htmlLink); foundMatch += htmlLink.length(); // skip the just-changed link //qDebug() << htmlLink; #if 0 if (markdownString.length() > 5000) break; //TMP #endif } qDebug() << "barelinkRE loop"; } while (foundMatch != -1); qDebug() << "markdown::tohtml after barelink"; // Link: [text](URL) QRegExp linkRE("\\[(.*)\\]\\((.*)\\)"); linkRE.setMinimal(true); do { foundMatch = linkRE.indexIn(markdownString); if (foundMatch > -1) { markdownString.replace(linkRE.cap(0), "" + linkRE.cap(1) +""); // FIXME: fails with URL's with parentheses in it, like // http://en.wikipedia.org/wiki/Diaspora_(software) } } while (foundMatch != -1); //qDebug() << "After conversion:" << markdownString; return markdownString; } /* * Get list of URL's of images inserted via ![]() sintax * */ QStringList Markdown::imageList(QString markdownString) { QStringList images; int matchPosition = 0; QRegExp imageRE("!\\[(.*)\\]\\((.*)\\)"); imageRE.setMinimal(true); do { matchPosition = imageRE.indexIn(markdownString, matchPosition); //qDebug() << "matchPosition:" << matchPosition; if (matchPosition > -1) { matchPosition += imageRE.matchedLength(); QString imageURL = imageRE.cap(2).trimmed(); images.append(imageURL); } } while (matchPosition != -1); return images; } dianara-v1.1/src/publisher.cpp000664 000764 000764 00000072407 12262611005 016031 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "publisher.h" Publisher::Publisher(PumpController *pumpController, QWidget *parent) : QWidget(parent) { this->pController = pumpController; connect(pController, SIGNAL(postPublished()), this, SLOT(onPublishingOk())); connect(pController, SIGNAL(postPublishingFailed()), this, SLOT(onPublishingFailed())); // After receiving the list of lists, update the "Lists" submenus connect(pController, SIGNAL(listsListReceived(QVariantList)), this, SLOT(updateListsMenus(QVariantList))); this->editingMode = false; // False unless set from setEditingMode // after clicking "Edit" in a post this->setFocusPolicy(Qt::StrongFocus); // To keep the publisher from getting focus by accident this->audienceSelectorTo = new AudienceSelector(pController, "to", this); connect(audienceSelectorTo, SIGNAL(audienceSelected(QString,QStringList)), this, SLOT(updateToCcFields(QString,QStringList))); this->audienceSelectorCC = new AudienceSelector(pController, "cc", this); connect(audienceSelectorCC, SIGNAL(audienceSelected(QString,QStringList)), this, SLOT(updateToCcFields(QString,QStringList))); titleLabel = new QLabel(tr("Title") + ":"); titleLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); titleLineEdit = new QLineEdit(); titleLineEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); titleLineEdit->setPlaceholderText(tr("Optional title for the post")); titleLineEdit->setToolTip(tr("Title for the post. Setting a title " "helps make the Meanwhile feed " "more informative.") + ""); pictureLabel = new QLabel(); pictureLabel->setAlignment(Qt::AlignCenter); //pictureLabel->setScaledContents(true); pictureLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Raised); pictureLabel->hide(); pictureInfoLabel = new QLabel(); pictureInfoLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); selectPictureButton = new QPushButton(QIcon::fromTheme("insert-image"), tr("Select Picture...")); selectPictureButton->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); selectPictureButton->setToolTip(tr("Find the picture in your folders")); connect(selectPictureButton, SIGNAL(clicked()), this, SLOT(findPictureFile())); selectPictureButton->hide(); // Set default pixmap and "picture not set" message this->setEmptyPictureData(); lastUsedDirectory = QDir::homePath(); // Composer composerBox = new Composer(true); // forPublisher = true composerBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); connect(composerBox, SIGNAL(focusReceived()), this, SLOT(setFullMode())); connect(composerBox, SIGNAL(editingFinished()), this, SLOT(sendPost())); connect(composerBox, SIGNAL(editingCancelled()), this, SLOT(setMinimumMode())); // Add formatting button exported from Composer this->toolsButton = composerBox->getToolsButton(); // To... menu toPublicAction = new QAction(tr("Public"), this); toPublicAction->setCheckable(true); connect(toPublicAction, SIGNAL(toggled(bool)), this, SLOT(setToPublic(bool))); toFollowersAction = new QAction(tr("Followers"), this); toFollowersAction->setCheckable(true); connect(toFollowersAction, SIGNAL(toggled(bool)), this, SLOT(setToFollowers(bool))); toSelectorListsMenu = new QMenu(tr("Lists")); toSelectorListsMenu->setDisabled(true); // Disabled until lists are received, if any connect(toSelectorListsMenu, SIGNAL(triggered(QAction*)), this, SLOT(updateToListsFields(QAction*))); toSelectorMenu = new QMenu("to-menu"); toSelectorMenu->addAction(toPublicAction); toSelectorMenu->addAction(toFollowersAction); toSelectorMenu->addMenu(toSelectorListsMenu); toSelectorMenu->addSeparator(); toSelectorMenu->addAction(tr("People..."), audienceSelectorTo, SLOT(show())); toSelectorButton = new QPushButton(QIcon::fromTheme("system-users"), tr("To...")); toSelectorButton->setToolTip(tr("Select who will see this post")); toSelectorButton->setMenu(toSelectorMenu); // CC... menu ccPublicAction = new QAction(tr("Public"), this); ccPublicAction->setCheckable(true); connect(ccPublicAction, SIGNAL(toggled(bool)), this, SLOT(setCCPublic(bool))); ccFollowersAction = new QAction(tr("Followers"), this); ccFollowersAction->setCheckable(true); connect(ccFollowersAction, SIGNAL(toggled(bool)), this, SLOT(setCCFollowers(bool))); ccSelectorListsMenu = new QMenu(tr("Lists")); ccSelectorListsMenu->setDisabled(true); // Disabled until lists are received, if any connect(ccSelectorListsMenu, SIGNAL(triggered(QAction*)), this, SLOT(updateCcListsFields(QAction*))); ccSelectorMenu = new QMenu("cc-menu"); ccSelectorMenu->addAction(ccPublicAction); ccSelectorMenu->addAction(ccFollowersAction); ccSelectorMenu->addMenu(ccSelectorListsMenu); ccSelectorMenu->addSeparator(); ccSelectorMenu->addAction(tr("People..."), audienceSelectorCC, SLOT(show())); ccSelectorButton = new QPushButton(QIcon::fromTheme("system-users"), tr("CC...")); ccSelectorButton->setToolTip(tr("Select who will get a copy of this post")); ccSelectorButton->setMenu(ccSelectorMenu); QFont audienceLabelsFont; // "To" column will be normal, "CC" will be italic audienceLabelsFont.setPointSize(audienceLabelsFont.pointSize()-1); // These will hold the names of the *people* selected for the To or CC fields, if any toAudienceLabel = new QLabel(); toAudienceLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); toAudienceLabel->setFont(audienceLabelsFont); // Normal toAudienceLabel->setWordWrap(true); ccAudienceLabel = new QLabel(); ccAudienceLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); audienceLabelsFont.setItalic(true); ccAudienceLabel->setFont(audienceLabelsFont); // Italic ccAudienceLabel->setWordWrap(true); // And these will show "public" or "followers" current selection (in bold) toPublicFollowersLabel = new QLabel(); toPublicFollowersLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); audienceLabelsFont.setBold(true); audienceLabelsFont.setItalic(false); toPublicFollowersLabel->setFont(audienceLabelsFont); // Bold ccPublicFollowersLabel = new QLabel(); ccPublicFollowersLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); audienceLabelsFont.setItalic(true); ccPublicFollowersLabel->setFont(audienceLabelsFont); // Bold + italic // To set the 'picture mode' pictureButton = new QPushButton(QIcon::fromTheme("camera-photo"), tr("Ad&d Picture")); pictureButton->setToolTip(tr("Upload photo")); connect(pictureButton, SIGNAL(clicked()), this, SLOT(setPictureMode())); // Sending status info label statusInfoLabel = new QLabel(); statusInfoLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); statusInfoLabel->setAlignment(Qt::AlignCenter); statusInfoLabel->setWordWrap(true); audienceLabelsFont.setBold(false); audienceLabelsFont.setItalic(false); statusInfoLabel->setFont(audienceLabelsFont); // To send the post postButton = new QPushButton(QIcon::fromTheme("mail-send"), tr("Post")); postButton->setToolTip(tr("Hit Control+Enter to post with the keyboard")); connect(postButton, SIGNAL(clicked()), this, SLOT(sendPost())); cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel"), tr("Cancel")); cancelButton->setToolTip(tr("Cancel the post")); connect(cancelButton, SIGNAL(clicked()), composerBox, SLOT(cancelPost())); // Now the layout, starting with the Title field and "picture mode" stuff titleLayout = new QHBoxLayout(); titleLayout->addWidget(titleLabel); titleLayout->addWidget(titleLineEdit); titleLayout->addSpacing(12); titleLayout->addWidget(toolsButton); mainLayout = new QGridLayout(); mainLayout->setVerticalSpacing(1); mainLayout->setContentsMargins(1, 1, 1, 1); mainLayout->addLayout(titleLayout, 0, 0, 1, 8, Qt::AlignTop | Qt::AlignLeft); mainLayout->addWidget(pictureLabel, 1, 0, 2, 4); mainLayout->addWidget(pictureInfoLabel, 1, 4, 1, 4, Qt::AlignTop); mainLayout->addWidget(selectPictureButton, 2, 4, 1, 4, Qt::AlignBottom | Qt::AlignLeft); mainLayout->addWidget(composerBox, 3, 0, 3, 8); // 3 rows, 8 columns mainLayout->addWidget(toSelectorButton, 6, 0, 1, 1, Qt::AlignLeft); mainLayout->addWidget(ccSelectorButton, 6, 1, 1, 1, Qt::AlignLeft); mainLayout->addWidget(pictureButton, 6, 3, 1, 2, Qt::AlignLeft); mainLayout->addWidget(statusInfoLabel, 6, 5, 3, 2, Qt::AlignCenter); mainLayout->addWidget(postButton, 6, 7, 1, 1); // The 2 labels holding people's names, if any mainLayout->addWidget(toAudienceLabel, 7, 0, 1, 1); mainLayout->addWidget(ccAudienceLabel, 7, 1, 1, 1); // The 2 labels holding "public/followers", if selected mainLayout->addWidget(toPublicFollowersLabel, 8, 0, 1, 1); mainLayout->addWidget(ccPublicFollowersLabel, 8, 1, 1, 1); // The "Cancel post" button mainLayout->addWidget(cancelButton, 8, 7, 1, 1); this->setLayout(mainLayout); this->setMinimumMode(); qDebug() << "Publisher created"; } Publisher::~Publisher() { qDebug() << "Publisher destroyed"; } /* * Set if "public" option for audience is checked as default * */ void Publisher::setDefaultPublicPosting(bool defaultPublicPosts) { this->defaultPublicPosting = defaultPublicPosts; // Ensure status is sync'ed this->toSelectorMenu->actions().at(0)->setChecked(this->defaultPublicPosting); } /* * Set default "no photo" pixmap and "picture not set" message * * Clear the filename and content type variables too */ void Publisher::setEmptyPictureData() { pictureLabel->setPixmap(QIcon::fromTheme("image-x-generic") .pixmap(200, 150) .scaled(200, 150, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); pictureLabel->setToolTip(tr("Picture not set")); pictureInfoLabel->clear(); this->pictureFilename.clear(); this->pictureContentType.clear(); } void Publisher::updatePublicFollowersLabels() { QString toString; foreach (QString listName, toListsNameStringList) { toString.append(QString::fromUtf8("\342\236\224 ") // arrow sign in front + listName + "\n"); } if (toPublicAction->isChecked()) { toString.append("+" + tr("Public") + "\n"); } if (toFollowersAction->isChecked()) { toString.append("+" + tr("Followers") + "\n"); } toPublicFollowersLabel->setText(toString); QString ccString; foreach (QString listName, ccListsNameStringList) { ccString.append(QString::fromUtf8("\342\236\224 ") // arrow sign + listName + "\n"); } if (ccPublicAction->isChecked()) { ccString.append("+" + tr("Public") + "\n"); } if (ccFollowersAction->isChecked()) { ccString.append("+" + tr("Followers") + "\n"); } ccPublicFollowersLabel->setText(ccString); } /* * Create a key:value map, listing who will receive a post, like: * * "collection" : "http://activityschema.org/collection/public" * "collection" : "https://pump.example/api/user/followers" * "person" : "acct:somecontact@pumpserver.example" * */ QMap Publisher::getAudienceMap() { QMap audienceMap; // To: Public is checked if (toPublicAction->isChecked()) { audienceMap.insertMulti("to|collection", "http://activityschema.org/collection/public"); } // To: List of individual people foreach (QString address, this->toAddressStringList) { audienceMap.insertMulti("to|person", "acct:" + address); } // To: Lists foreach (QString address, this->toListsIdStringList) { audienceMap.insertMulti("to|collection", address); } // To: Followers is checked if (toFollowersAction->isChecked()) { audienceMap.insertMulti("to|collection", this->pController->currentFollowersUrl()); } // CC: Public is checked if (ccPublicAction->isChecked()) { audienceMap.insertMulti("cc|collection", "http://activityschema.org/collection/public"); } // CC: List of individual people foreach (QString address, this->ccAddressStringList) { audienceMap.insertMulti("cc|person", "acct:" + address); } // CC: Lists foreach (QString address, this->ccListsIdStringList) { audienceMap.insertMulti("cc|collection", address); } // Last check: if CC: Followers is checked, or nothing is, add Followers if (ccFollowersAction->isChecked() || audienceMap.isEmpty()) { audienceMap.insertMulti("cc|collection", this->pController->currentFollowersUrl()); } return audienceMap; } /********************************************************************/ /***************************** SLOTS ********************************/ /********************************************************************/ void Publisher::setMinimumMode() { qDebug() << "setting Publisher to minimum mode"; this->postButton->setFocus(); // Give focus to button, // in case user shared with Ctrl+Enter // Disable possible scrollbars this->composerBox->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); this->composerBox->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // ~1 row int composerHeight = composerBox->getMessageLabelHeight(); this->composerBox->setMinimumHeight(composerHeight); this->composerBox->setMaximumHeight(composerHeight); this->setMinimumHeight(composerHeight + 2); this->setMaximumHeight(composerHeight + 2); this->audienceSelectorTo->resetLists(); this->audienceSelectorCC->resetLists(); this->toAudienceLabel->setText("..."); toAudienceLabel->repaint(); // Avoid a flicker-like effect later toAudienceLabel->clear(); toAudienceLabel->hide(); this->toAddressStringList.clear(); this->toListsNameStringList.clear(); this->toListsIdStringList.clear(); toPublicFollowersLabel->clear(); toPublicFollowersLabel->hide(); this->ccAudienceLabel->setText("..."); ccAudienceLabel->repaint(); ccAudienceLabel->clear(); ccAudienceLabel->hide(); this->ccAddressStringList.clear(); this->ccListsNameStringList.clear(); this->ccListsIdStringList.clear(); ccPublicFollowersLabel->clear(); ccPublicFollowersLabel->hide(); this->toSelectorButton->hide(); this->ccSelectorButton->hide(); // Check "public" if "public posts" is set in the preferences toPublicAction->setChecked(this->defaultPublicPosting); toFollowersAction->setChecked(false); ccPublicAction->setChecked(false); ccFollowersAction->setChecked(true); // CC: Followers by default // Uncheck the lists, very TMP / FIXME // This doesn't clear the To/CC labels sometimes foreach (QAction *action, toSelectorListsMenu->actions()) { action->setChecked(false); } foreach (QAction *action, ccSelectorListsMenu->actions()) { action->setChecked(false); } titleLabel->hide(); this->titleLineEdit->clear(); titleLineEdit->hide(); toolsButton->hide(); this->statusInfoLabel->clear(); statusInfoLabel->hide(); this->pictureButton->hide(); this->postButton->hide(); this->cancelButton->hide(); // Clear formatting options like bold, italic and underline this->composerBox->setCurrentCharFormat(QTextCharFormat()); // Hide "picture mode" controls this->pictureLabel->hide(); this->pictureInfoLabel->hide(); this->selectPictureButton->hide(); this->setEmptyPictureData(); // Clear "editing mode", restore stuff if (editingMode) { this->editingMode = false; this->editingPostId.clear(); this->postButton->setText(tr("Post")); // Button text back to "Post" as usual this->toSelectorButton->setEnabled(true); this->ccSelectorButton->setEnabled(true); this->pictureButton->setEnabled(true); } } void Publisher::setFullMode() { qDebug() << "setting Publisher to full mode"; this->titleLabel->show(); this->titleLineEdit->show(); this->toolsButton->show(); this->composerBox->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); this->composerBox->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); this->composerBox->setMaximumHeight(2048); this->setMaximumHeight(2048); // i.e. "unlimited" this->toSelectorButton->show(); this->ccSelectorButton->show(); this->toAudienceLabel->show(); this->toPublicFollowersLabel->show(); this->ccAudienceLabel->show(); this->ccPublicFollowersLabel->show(); updatePublicFollowersLabels(); // Avoid re-enabling the picture button when re-focusing publisher, but still in picture mode... if (pictureButton->isHidden()) { this->pictureButton->setEnabled(true); // If it wasn't hidden, don't re-enable } this->pictureButton->show(); this->statusInfoLabel->show(); this->postButton->show(); this->cancelButton->show(); this->composerBox->setFocus(); // In case user used menu or shortcut // instead of clicking on it } void Publisher::setPictureMode() { this->pictureButton->setDisabled(true); this->pictureLabel->show(); this->pictureInfoLabel->show(); this->selectPictureButton->show(); this->findPictureFile(); // Show the open file dialog directly } /* * Set Publiser to edit mode, after user clicks on "Edit" in a post * */ void Publisher::setEditingMode(QString postID, QString postText, QString postTitle) { if (postButton->isVisible()) // FIXME, use a proper state variable { // TMPFIX; temporary way of preventing the "Edit" option // from destroying a post currently being composed! QMessageBox::warning(this, tr("Error: Already composing"), tr("You can't edit a post at this time, " "because a post is already being composed.")); return; } this->editingMode = true; this->editingPostId = postID; setFullMode(); // Fill in the contents of the post this->titleLineEdit->setText(postTitle); this->composerBox->setText(postText); // Change/disable some controls this->postButton->setText(tr("Update")); this->toSelectorButton->setDisabled(true); this->ccSelectorButton->setDisabled(true); this->pictureButton->setDisabled(true); this->statusInfoLabel->setText(tr("Editing post")); } /* * After the post is confirmed to have been received by the server * re-enable publisher, clear text, etc. * */ void Publisher::onPublishingOk() { this->statusInfoLabel->clear(); this->setEnabled(true); this->composerBox->erase(); // Done composing message, hide buttons until we get focus again setMinimumMode(); } /* * If there was a HTTP error while posting... * */ void Publisher::onPublishingFailed() { qDebug() << "Posting failed, re-enabling Publisher"; this->statusInfoLabel->setText(tr("Posting failed.\n\nTry again.")); this->setEnabled(true); this->composerBox->setFocus(); } /* * These are called when selecting Public or Followers in the menus * * When selecting "CC: Followers", "To: Followers" gets unselected, etc. * */ void Publisher::setToPublic(bool activated) { if (activated) { ccPublicAction->setChecked(false); // Unselect "CC: Public" } updatePublicFollowersLabels(); } void Publisher::setToFollowers(bool activated) { if (activated) { ccFollowersAction->setChecked(false); // Unselect "CC: Followers" } updatePublicFollowersLabels(); } void Publisher::setCCPublic(bool activated) { if (activated) { toPublicAction->setChecked(false); // Unselect "To: Public" } updatePublicFollowersLabels(); } void Publisher::setCCFollowers(bool activated) { if (activated) { toFollowersAction->setChecked(false); // Unselect "To: Followers" } updatePublicFollowersLabels(); } /* * Receive a list of addresses for the To or CC fields * */ void Publisher::updateToCcFields(QString selectorType, QStringList contactsList) { qDebug() << "Publisher::updateToCcFields()" << selectorType << contactsList; QRegExp contactRE("(.+)\\s+\\<(.+@.+)\\>", Qt::CaseInsensitive); contactRE.setMinimal(true); QString addressesString; QStringList addressesStringList; foreach (QString contactString, contactsList) { contactRE.indexIn(contactString); addressesString.append(contactRE.cap(1).trimmed()); addressesString.append(", "); addressesStringList.append(contactRE.cap(2).trimmed()); } addressesString.remove(-2, 2); // remove ", " at the end if (selectorType == "to") { this->toAudienceLabel->setText(addressesString); this->toAddressStringList = addressesStringList; } else // "cc" { this->ccAudienceLabel->setText(addressesString); this->ccAddressStringList = addressesStringList; } qDebug() << "Names:" << addressesString; qDebug() << "Addresses:" << addressesStringList; } /* * Fill the "Lists" submenus with PumpController's lists info * * */ void Publisher::updateListsMenus(QVariantList listsList) { // First, clear the menus toSelectorListsMenu->clear(); // clear() should delete the actions ccSelectorListsMenu->clear(); if (listsList.length() > 0) // if there are some lists, enable the menu { toSelectorListsMenu->setEnabled(true); ccSelectorListsMenu->setEnabled(true); } QString listName; QVariant listId; foreach (QVariant list, listsList) { listName = list.toMap().value("displayName").toString(); listId = list.toMap().value("id"); QAction *toListAction = new QAction(listName, this); toListAction->setCheckable(true); toListAction->setData(listId); toSelectorListsMenu->addAction(toListAction); QAction *ccListAction = new QAction(listName, this); ccListAction->setCheckable(true); ccListAction->setData(listId); ccSelectorListsMenu->addAction(ccListAction); } } void Publisher::updateToListsFields(QAction *listAction) { QString listName = listAction->text(); QString listId = listAction->data().toString(); if (listAction->isChecked()) { qDebug() << "To list selected" << listName << listId; toListsNameStringList.append(listName); toListsIdStringList.append(listId); } else { qDebug() << "To list unselected" << listName; toListsNameStringList.removeAll(listName); toListsIdStringList.removeAll(listId); } this->updatePublicFollowersLabels(); qDebug() << "Current To Lists:" << toListsNameStringList << toListsIdStringList; } void Publisher::updateCcListsFields(QAction *listAction) { QString listName = listAction->text(); QString listId = listAction->data().toString(); if (listAction->isChecked()) { qDebug() << "CC list selected" << listName << listId; ccListsNameStringList.append(listName); ccListsIdStringList.append(listId); } else { qDebug() << "CC list unselected" << listName; ccListsNameStringList.removeAll(listName); ccListsIdStringList.removeAll(listId); } this->updatePublicFollowersLabels(); qDebug() << "Current CC Lists:" << ccListsNameStringList << ccListsIdStringList; } /* * Send the post (note, image...) to the server * */ void Publisher::sendPost() { qDebug() << "Publisher character count:" << composerBox->textCursor().document()->characterCount(); // If there's some text in the post, or attached picture, send it if (composerBox->textCursor().document()->characterCount() > 1 // kinda tmp || !pictureFilename.isEmpty()) { QString postTitle = this->titleLineEdit->text(); postTitle.replace("\n", " "); // Post title could have newlines if copy-pasted QString cleanHtmlString = MiscHelpers::cleanupHtml(composerBox->toHtml()); QMap audienceMap = this->getAudienceMap(); // Don't erase just yet!! Just disable until we get "200 OK" from the server. this->setDisabled(true); if (!editingMode) { this->statusInfoLabel->setText(tr("Posting...")); if (this->pictureFilename.isEmpty()) { this->pController->postNote(audienceMap, cleanHtmlString, postTitle); } else { this->pController->postImage(audienceMap, cleanHtmlString, postTitle, pictureFilename, pictureContentType); } } else { this->statusInfoLabel->setText(tr("Updating...")); this->pController->updatePost(this->editingPostId, cleanHtmlString, postTitle); } } else { this->statusInfoLabel->setText(tr("Post is empty.")); qDebug() << "Can't send post: text is empty, and no picture"; } } /* * Let the user find an image file in his/her folders, for picture upload * */ void Publisher::findPictureFile() { QString filename; filename = QFileDialog::getOpenFileName(this, tr("Select one image"), this->lastUsedDirectory, tr("Image files") + "(*.png *.jpg *.jpeg *.gif);;" + tr("All files") + " (*)"); if (!filename.isEmpty()) { qDebug() << "Selected" << filename << "for upload"; this->pictureLabel->setToolTip(filename); this->pictureContentType = MiscHelpers::getImageContentType(filename); if (!pictureContentType.isEmpty()) { this->pictureFilename = filename; QPixmap imagePixmap = QPixmap(filename); // FIXME: check imagePixmap.isNull() ? this->pictureLabel->setPixmap(imagePixmap.scaled(320, 180, // 16:9 Qt::KeepAspectRatio, Qt::SmoothTransformation)); QFileInfo fileInfo(filename); this->lastUsedDirectory = fileInfo.path(); qDebug() << "last used directory:" << lastUsedDirectory; pictureInfoLabel->setText(QString("" +tr("Resolution") + ": %1x%2" "
" "" + tr("Size") + ": %3") .arg(imagePixmap.width()) .arg(imagePixmap.height()) .arg(MiscHelpers::fileSizeString(filename))); } else { // In the future, avoid this by using libmagic or similar qDebug() << "Unknown image format; Extension is probably wrong"; QMessageBox::warning(this, tr("Invalid image"), tr("The image format cannot be detected.\n" "The extension might be wrong, like a GIF " "image renamed to image.jpg or similar.")); this->pictureContentType.clear(); this->pictureFilename.clear(); } } } dianara-v1.1/src/composer.cpp000664 000764 000764 00000040274 12262611023 015660 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "composer.h" Composer::Composer(bool forPublisher) { this->forPublisher = forPublisher; this->setAcceptRichText(true); this->setTabChangesFocus(true); // FIXME: do something about the links being styled with user's color //this->setStyleSheet("{ a:link { color: #FF0000;} }"); QFont startConversationFont; startConversationFont.setPointSize(startConversationFont.pointSize() - 2); startConversationLabel = new QLabel(tr("Click here or press Control+N to post a note...")); startConversationLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); startConversationLabel->setFont(startConversationFont); // A menu to insert some Unicode symbols symbolsMenu = new QMenu(tr("Symbols")); symbolsMenu->setIcon(QIcon::fromTheme("character-set")); symbolsMenu->addAction(QString::fromUtf8("\342\230\272")); // Smiling face symbolsMenu->addAction(QString::fromUtf8("\342\230\271")); // Sad face symbolsMenu->addAction(QString::fromUtf8("\342\231\245")); // Heart symbolsMenu->addAction(QString::fromUtf8("\342\231\253")); // Musical note symbolsMenu->addAction(QString::fromUtf8("\342\230\225")); // Coffee symbolsMenu->addAction(QString::fromUtf8("\342\234\224")); // Check mark symbolsMenu->addAction(QString::fromUtf8("\342\230\205")); // Black star symbolsMenu->addAction(QString::fromUtf8("\342\254\205")); // Arrow to the left symbolsMenu->addAction(QString::fromUtf8("\342\236\241")); // Arrow to the right connect(symbolsMenu, SIGNAL(triggered(QAction*)), this, SLOT(insertSymbol(QAction*))); toolsMenu = new QMenu(tr("Formatting")); toolsMenu->addAction(QIcon::fromTheme(""), tr("Normal"), this, SLOT(makeNormal())); toolsMenu->addAction(QIcon::fromTheme("format-text-bold"), tr("Bold"), this, SLOT(makeBold()), QKeySequence("Ctrl+B")); toolsMenu->addAction(QIcon::fromTheme("format-text-italic"), tr("Italic"), this, SLOT(makeItalic()), QKeySequence("Ctrl+I")); toolsMenu->addAction(QIcon::fromTheme("format-text-underline"), tr("Underline"), this, SLOT(makeUnderline()), QKeySequence("Ctrl+U")); toolsMenu->addAction(QIcon::fromTheme("format-text-strikethrough"), tr("Strikethrough"), this, SLOT(makeStrikethrough())); toolsMenu->addSeparator(); toolsMenu->addAction(QIcon::fromTheme("format-font-size-more"), tr("Header"), this, SLOT(makeHeader()), QKeySequence("Ctrl+H")); toolsMenu->addAction(QIcon::fromTheme("format-justify-fill"), tr("Preformatted block"), this, SLOT(makePreformatted())); toolsMenu->addAction(QIcon::fromTheme("format-text-italic"), tr("Quote block"), this, SLOT(makeQuote())); toolsMenu->addSeparator(); toolsMenu->addAction(QIcon::fromTheme("insert-link"), tr("Make a link"), this, SLOT(makeLink()), QKeySequence("Ctrl+L")); toolsMenu->addAction(QIcon::fromTheme("insert-image"), tr("Insert an image from a web site"), this, SLOT(insertImage()), QKeySequence("Ctrl+P")); toolsMenu->addAction(QIcon::fromTheme("insert-horizontal-rule"), tr("Insert line"), this, SLOT(insertLine())); toolsMenu->addSeparator(); toolsMenu->addMenu(symbolsMenu); toolsButton = new QPushButton(QIcon::fromTheme("format-list-ordered"), tr("&Format", "Button for text formatting and related options")); toolsButton->setMenu(toolsMenu); toolsButton->setToolTip(tr("Text Formatting Options")); toolsButton->setHidden(true); // Hidden on startup (Publisher in miniMode) // Extra action for the context menu, paste as plaintext this->pastePlaintextAction = new QAction(QIcon::fromTheme("paste"), tr("Paste Text Without Formatting"), this); pastePlaintextAction->setShortcut(QKeySequence("Ctrl+Shift+V")); connect(pastePlaintextAction, SIGNAL(triggered()), this, SLOT(pasteAsPlaintext())); // Add action to this object, in order for the shortcut to work // Otherwise, the action isn't available until the context menu is present this->addAction(pastePlaintextAction); mainLayout = new QHBoxLayout(); if (this->forPublisher) // Publisher mode { this->setToolTip(tr("Type a message here to post it")); mainLayout->addWidget(startConversationLabel, 1, Qt::AlignLeft | Qt::AlignVCenter); } else // or Commenter mode { this->setToolTip(tr("Type a comment here")); } this->setLayout(mainLayout); qDebug() << "Composer box created"; } Composer::~Composer() { qDebug() << "Composer box destroyed"; } void Composer::erase() { this->clear(); if (this->forPublisher) { startConversationLabel->show(); toolsButton->hide(); /// FIXME: maybe move this line to focusOutEvent } } void Composer::insertLink(QString url, QString title) { if (title.isEmpty()) { title = url; } this->insertHtml("" "" + title + " "); // Important, space after! } int Composer::getMessageLabelHeight() { // kinda TMP / FIXME return (this->startConversationLabel->font().pointSize() * 4) + 4; } /* * Send a signal when getting focus * */ void Composer::focusInEvent(QFocusEvent *event) { emit focusReceived(); // inform Publisher() or Commenter() that we have focus if (this->forPublisher) { startConversationLabel->hide(); // hide placeholder message } toolsButton->show(); QTextEdit::focusInEvent(event); // process standard event: allows context menu qDebug() << "Composer box got focus"; } void Composer::keyPressEvent(QKeyEvent *event) { //qDebug() << "Composer::keyPressEvent()"; // Control+Enter = Send message (post) if ((event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) && event->modifiers() == Qt::ControlModifier) { qDebug() << "Control+Enter was pressed"; emit editingFinished(); } else if (event->key() == Qt::Key_Escape) { qDebug() << "Escape was pressed"; if (this->toPlainText().isEmpty()) { qDebug() << "There was no text, canceling post"; this->cancelPost(); } } else { QTextEdit::keyPressEvent(event); } } /* * For custom context menu * */ void Composer::contextMenuEvent(QContextMenuEvent *event) { this->customContextMenu = this->createStandardContextMenu(); // tools menu before default context menu customContextMenu->insertMenu(customContextMenu->actions().at(0), toolsMenu); customContextMenu->insertSeparator(customContextMenu->actions().at(1)); // And options added after default context menu customContextMenu->addSeparator(); customContextMenu->addAction(pastePlaintextAction); if (this->canPaste()) { pastePlaintextAction->setEnabled(true); } else { pastePlaintextAction->setDisabled(true); } customContextMenu->exec(event->globalPos()); // FIXME: Possible leak... should delete customContextMenu? customContextMenu->deleteLater(); event->accept(); } /* * Intervene when pasting, so we can turn links into real HTML links * */ void Composer::insertFromMimeData(const QMimeData *source) { QString pastedText = source->text().trimmed(); if (pastedText.startsWith("http://") || pastedText.startsWith("https://")) { this->insertLink(pastedText); } else // Just paste original contents { QTextEdit::insertFromMimeData(source); } } /*************************************************************************/ /****************************** SLOTS ************************************/ /*************************************************************************/ /* * Remove text formatting from selection, bold, italic, etc. * */ void Composer::makeNormal() { QTextCharFormat charFormat; charFormat.clearForeground(); charFormat.clearBackground(); this->setCurrentCharFormat(charFormat); this->setFocus(); } /* * Make selected text bold * */ void Composer::makeBold() { //qDebug() << this->textCursor().selectionStart() << " -> " << this->textCursor().selectionEnd(); QTextCharFormat charFormat; if (this->currentCharFormat().fontWeight() == QFont::Bold) { charFormat.setFontWeight(QFont::Normal); } else { charFormat.setFontWeight(QFont::Bold); } this->mergeCurrentCharFormat(charFormat); this->setFocus(); // give focus back to text editor } /* * Make selected text italic * */ void Composer::makeItalic() { QTextCharFormat charFormat; charFormat.setFontItalic(!this->currentCharFormat().fontItalic()); this->mergeCurrentCharFormat(charFormat); this->setFocus(); } /* * Underline selected text * */ void Composer::makeUnderline() { QTextCharFormat charFormat; charFormat.setFontUnderline(!this->currentCharFormat().fontUnderline()); this->mergeCurrentCharFormat(charFormat); this->setFocus(); } /* * Strike out selected text * */ void Composer::makeStrikethrough() { QTextCharFormat charFormat; charFormat.setFontStrikeOut(!this->currentCharFormat().fontStrikeOut()); this->mergeCurrentCharFormat(charFormat); this->setFocus(); } /* * Turn the selected text into an

header * */ void Composer::makeHeader() { QString selectedText = this->textCursor().selectedText(); if (!selectedText.isEmpty()) { this->textCursor().removeSelectedText(); this->insertHtml("

" + selectedText + "

"); } this->setFocus(); } /* * Put selected text into a
 block
 *
 */
void Composer::makePreformatted()
{
    QString selectedText = this->textCursor().selectedText();

    if (!selectedText.isEmpty())
    {
        this->textCursor().removeSelectedText();
        this->insertHtml("
" + selectedText + "
"); } this->setFocus(); } /* * Mark selected as quoted, using
* */ void Composer::makeQuote() { QString selectedText = this->textCursor().selectedText(); if (!selectedText.isEmpty()) { this->textCursor().removeSelectedText(); this->insertHtml("
" "
“" + selectedText + "”
"); } // FIXME: Qt's HTML changes
into its own formatting // so other clients and the web UI might not display it as other people's blockquote's this->setFocus(); } /* * Convert selected text into a link * */ void Composer::makeLink() { QString selectedText = this->textCursor().selectedText(); QString link; if (selectedText.isEmpty()) { link = QInputDialog::getText(this, tr("Insert a link"), tr("Type or paste a web address here.\n"), QLineEdit::Normal, "http://"); } else { link = QInputDialog::getText(this, tr("Make a link from selected text"), tr("Type or paste a web address here.\n" "The selected text (%1) will be converted " "to a link.").arg(selectedText), QLineEdit::Normal, "http://"); } if (!link.isEmpty()) { link = link.trimmed(); // Remove possible spaces before or after this->textCursor().removeSelectedText(); this->insertLink(link, selectedText); } else { qDebug() << "makeLink(): Invalid URL"; } this->setFocus(); } /* * Insert an image from a URL * */ void Composer::insertImage() { // FIXME: should make sure there is no text selected QString imageURL; imageURL = QInputDialog::getText(this, tr("Insert an image from a URL"), tr("Type or paste the image address here.\n"), QLineEdit::Normal, "http://"); if (!imageURL.isEmpty()) { if (imageURL.startsWith("http://") || imageURL.startsWith("https://")) { this->insertHtml(""); } else { QMessageBox::warning(this, tr("Error: Invalid URL"), tr("The address you entered (%1) " "is not valid.\n" "Image addresses should begin " "with http:// or https://").arg(imageURL)); } } else { qDebug() << "insertImage(): Image URL is empty"; } this->setFocus(); } /* * Insert a horizontal line,
* */ void Composer::insertLine() { this->insertHtml("

"); this->setFocus(); } void Composer::insertSymbol(QAction *action) { this->insertPlainText(action->text()); this->insertPlainText(" "); this->setFocus(); } void Composer::pasteAsPlaintext() { QString subtype("plain"); QString clipboardContents = QApplication::clipboard()->text(subtype, QClipboard::Clipboard); this->makeNormal(); // Ensure normal text before insterting this->insertPlainText(clipboardContents); } /* * Cancel editing of the post, clear it, return to minimum mode * */ void Composer::cancelPost() { int cancelConfirmed = 1; if (this->document()->isEmpty()) { cancelConfirmed = 0; // Cancelling doesn't need confirmation if it's empty } else { cancelConfirmed = QMessageBox::question(this, tr("Cancel message?"), tr("Are you sure you want to cancel this message?"), tr("&Yes, cancel it"), tr("&No"), "", 1, 1); } if (cancelConfirmed == 0) { this->erase(); // emit signal to make Publisher go back to minimum mode emit editingCancelled(); qDebug() << "Post canceled"; } } QPushButton *Composer::getToolsButton() { return this->toolsButton; } dianara-v1.1/src/composer.h000664 000764 000764 00000004525 12260657135 015340 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef COMPOSER_H #define COMPOSER_H #include #include #include #include #include #include #include #include #include #include #include // Needed in Qt5 #include class Composer : public QTextEdit { Q_OBJECT public: Composer(bool forPublisher); ~Composer(); void erase(); void insertLink(QString url, QString title=""); int getMessageLabelHeight(); signals: void focusReceived(); void editingFinished(); void editingCancelled(); public slots: void makeNormal(); void makeBold(); void makeItalic(); void makeUnderline(); void makeStrikethrough(); void makeHeader(); void makePreformatted(); void makeQuote(); void makeLink(); void insertImage(); void insertLine(); void insertSymbol(QAction *action); void pasteAsPlaintext(); void cancelPost(); QPushButton *getToolsButton(); protected: virtual void focusInEvent(QFocusEvent *event); virtual void keyPressEvent(QKeyEvent *event); virtual void contextMenuEvent(QContextMenuEvent *event); virtual void insertFromMimeData(const QMimeData *source); private: QHBoxLayout *mainLayout; QLabel *startConversationLabel; QPushButton *toolsButton; QMenu *toolsMenu; QMenu *symbolsMenu; QAction *pastePlaintextAction; QMenu *customContextMenu; bool forPublisher; }; #endif // COMPOSER_H dianara-v1.1/src/comment.h000664 000764 000764 00000005100 12260657135 015141 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef COMMENT_H #define COMMENT_H #include #include #include #include #include #include #include #include #include #include #include "pumpcontroller.h" #include "mischelpers.h" #include "timestamp.h" class Comment : public QFrame { Q_OBJECT public: explicit Comment(PumpController *pumpController, QVariantMap commentMap, QWidget *parent = 0); ~Comment(); void fixLikeLabelText(); void setLikesCount(int count, QVariantList namesVariantList); void setCommentContents(); void setCommentHeight(); void enqueueImageForDownload(QString url); signals: void commentQuoteRequested(QString content); public slots: void likeComment(QString clickedLink); void quoteComment(); void deleteComment(); void showUrlInfo(QString url); void redrawAvatar(QString avatarUrl, QString avatarFilename); void redrawImages(QString imageUrl); protected: virtual void leaveEvent(QEvent *event); private: QHBoxLayout *mainLayout; QVBoxLayout *leftLayout; QVBoxLayout *rightLayout; QHBoxLayout *rightTopLayout; QLabel *avatarLabel; QLabel *fullNameLabel; QLabel *timestampLabel; QLabel *contentLabel; QLabel *likesCountLabel; QLabel *likeLabel; QLabel *quoteLabel; QLabel *deleteLabel; QHBoxLayout *contentLabelLayout; QLabel *urlInfoLabel; QString commentID; QString objectType; QString commentAuthorAvatarUrl; bool commentIsOwn; bool commentIsLiked; QString commentOriginalText; QStringList pendingImagesList; PumpController *pController; }; #endif // COMMENT_H dianara-v1.1/src/timestamp.h000664 000764 000764 00000002476 12260657137 015521 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef TIMESTAMP_H #define TIMESTAMP_H #include #include #include class Timestamp : public QObject { Q_OBJECT public: explicit Timestamp(QObject *parent = 0); // isoTime = ISO 8601, UTC, like "2012-02-07T01:32:02Z" static QString localTimeDate(QString isoTime); // returns "07-02-2012\n01:32:02" static QString fuzzyTime(QString isoTime); // returns "about an hour ago", etc. signals: public slots: }; #endif // TIMESTAMP_H dianara-v1.1/src/timestamp.cpp000664 000764 000764 00000011376 12260657137 016053 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "timestamp.h" Timestamp::Timestamp(QObject *parent) : QObject(parent) { // Creating object not required } /* * returns " 07-02-2012 - 01:32:02", adjusted to local time * * isoTime = ISO 8601, UTC, like "2012-02-07T01:32:02Z" */ QString Timestamp::localTimeDate(QString isoTime) { //qDebug() << "::localTimeDate - isoTime: " << isoTime; QDateTime dateTimeConverter = QDateTime::fromString(isoTime, Qt::ISODate).toLocalTime(); QString localTimeDateString; localTimeDateString = dateTimeConverter.date().toString(); localTimeDateString.append(", "); localTimeDateString.append(dateTimeConverter.time().toString()); return localTimeDateString; } /* * returns "about an hour ago", etc, based on local time * * isoTime = ISO 8601, UTC, like "2012-02-07T01:32:02Z" */ QString Timestamp::fuzzyTime(QString isoTime) { if (isoTime.isEmpty()) { return tr("Invalid timestamp!"); } QDateTime dateTimeConverter = QDateTime::fromString(isoTime, Qt::ISODate); int timeDifference = QDateTime::currentDateTimeUtc().toTime_t() - dateTimeConverter.toTime_t(); //qDebug() << "time difference in seconds:" << timeDifference; QString localTimeDateString = QString("About ... ago"); // At this point, timeDifference is in SECONDS if (timeDifference < 60) // less than a minute { localTimeDateString = tr("Less than a minute ago"); } else { timeDifference /= 60; // convert to minutes if (timeDifference < 60) // less than an hour { if (timeDifference == 1) { localTimeDateString = tr("A minute ago"); } else { localTimeDateString = tr("%1 minutes ago").arg(timeDifference); } } else { timeDifference /= 60; // convert to hours if (timeDifference < 24) // less than a day { if (timeDifference == 1) { localTimeDateString = tr("An hour ago"); } else { localTimeDateString = tr("%1 hours ago").arg(timeDifference); } } else { timeDifference /= 24; // convert to days if (timeDifference < 30) // less than a (average) month { if (timeDifference == 1) { localTimeDateString = tr("A day ago"); } else { localTimeDateString = tr("%1 days ago").arg(timeDifference); } } else { timeDifference /= 30; // convert to months if (timeDifference < 12) // less than a year { if (timeDifference == 1) { localTimeDateString = tr("A month ago"); } else { localTimeDateString = tr("%1 months ago").arg(timeDifference); } } else { timeDifference /= 12; if (timeDifference == 1) { localTimeDateString = tr("A year ago"); } else { localTimeDateString = tr("%1 years ago").arg(timeDifference); } } } } } } // No, I'm not particularly proud of this code.... oh, well... //qDebug() << "Posted:" << localTimeDateString; return localTimeDateString; } dianara-v1.1/src/contactlist.h000664 000764 000764 00000005222 12260657140 016027 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef CONTACTLIST_H #define CONTACTLIST_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pumpcontroller.h" #include "contactcard.h" #include "listsmanager.h" class ContactList : public QWidget { Q_OBJECT public: ContactList(PumpController *pumpController, QWidget *parent = 0); ~ContactList(); void clearFollowingListContents(); void clearFollowersListContents(); void setTabLabels(); void exportContactsToFile(QString listType); signals: public slots: void setContactListContents(QString listType, QVariantList contactList, int totalReceivedCount); void setListsListContents(QVariantList listsList); void refreshFollowing(); void refreshFollowers(); void exportFollowing(); void exportFollowers(); void refreshLists(); void followContact(); private: QVBoxLayout *mainLayout; QHBoxLayout *topLayout; QLabel *enterAddressLabel; QLineEdit *addressLineEdit; QPushButton *followButton; QTabWidget *tabWidget; QWidget *followingWidget; QVBoxLayout *followingLayout; QScrollArea *followingScrollArea; int followingCount; QString followingListString; QWidget *followersWidget; QVBoxLayout *followersLayout; QScrollArea *followersScrollArea; int followersCount; QString followersListString; ListsManager *listsManager; QScrollArea *listsScrollArea; int listsCount; QPushButton *optionsButton; QMenu *optionsMenu; PumpController *pController; }; #endif // CONTACTLIST_H dianara-v1.1/src/contactlist.cpp000664 000764 000764 00000033556 12260657136 016402 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "contactlist.h" ContactList::ContactList(PumpController *pumpController, QWidget *parent) : QWidget(parent) { this->pController = pumpController; mainLayout = new QVBoxLayout(); mainLayout->setAlignment(Qt::AlignTop); QString webfingerHelpMessage = tr("username@server.org or https://server.org/username"); topLayout = new QHBoxLayout(); enterAddressLabel = new QLabel(tr("&Enter address to follow:")); enterAddressLabel->setToolTip(webfingerHelpMessage); addressLineEdit = new QLineEdit(); addressLineEdit->setPlaceholderText(webfingerHelpMessage); addressLineEdit->setToolTip(webfingerHelpMessage); connect(addressLineEdit, SIGNAL(returnPressed()), this, SLOT(followContact())); enterAddressLabel->setBuddy(addressLineEdit); followButton = new QPushButton(QIcon::fromTheme("list-add-user"), tr("&Follow")); connect(followButton, SIGNAL(clicked()), this, SLOT(followContact())); topLayout->addWidget(enterAddressLabel); topLayout->addWidget(addressLineEdit); topLayout->addWidget(followButton); mainLayout->addLayout(topLayout); mainLayout->addSpacing(8); // Widget for list of 'following' this->followingWidget = new QWidget(); followingLayout = new QVBoxLayout(); followingLayout->setAlignment(Qt::AlignTop); QMap demoFollowingData; demoFollowingData.insert("name", "Demo Contact"); demoFollowingData.insert("id", "democontact@pumpserver.org"); demoFollowingData.insert("avatar", ""); demoFollowingData.insert("hometown", "Some city"); demoFollowingData.insert("following", "true"); followingLayout->addWidget(new ContactCard(this->pController, demoFollowingData, this)); followingWidget->setLayout(followingLayout); followingScrollArea = new QScrollArea(); followingScrollArea->setWidget(followingWidget); followingScrollArea->setWidgetResizable(true); // Widget for list of 'followers' this->followersWidget = new QWidget(); followersLayout = new QVBoxLayout(); followersLayout->setAlignment(Qt::AlignTop); QMap demoFollowerData; demoFollowerData.insert("name", "Demo Follower"); demoFollowerData.insert("id", "demofollower@pumpserver.org"); demoFollowerData.insert("avatar", ""); demoFollowerData.insert("hometown", "Demo Town"); demoFollowerData.insert("following", "false"); followersLayout->addWidget(new ContactCard(this->pController, demoFollowerData, this)); followersWidget->setLayout(followersLayout); followersScrollArea = new QScrollArea(); followersScrollArea->setWidget(followersWidget); followersScrollArea->setWidgetResizable(true); // Widget for the list of 'lists' this->listsManager = new ListsManager(this->pController); listsScrollArea = new QScrollArea(); listsScrollArea->setWidget(this->listsManager); listsScrollArea->setWidgetResizable(true); // Options menu optionsMenu = new QMenu("*options-menu*"); optionsMenu->addAction(QIcon::fromTheme("view-refresh"), tr("Reload Followers"), this, SLOT(refreshFollowers())); optionsMenu->addAction(QIcon::fromTheme("view-refresh"), tr("Reload Following"), this, SLOT(refreshFollowing())); optionsMenu->addSeparator(); optionsMenu->addAction(QIcon::fromTheme("document-export"), tr("Export Followers"), this, SLOT(exportFollowers())); optionsMenu->addAction(QIcon::fromTheme("document-export"), tr("Export Following"), this, SLOT(exportFollowing())); optionsMenu->addSeparator(); optionsMenu->addAction(QIcon::fromTheme("view-refresh"), tr("Reload Lists"), this, SLOT(refreshLists())); optionsButton = new QPushButton(QIcon::fromTheme("configure"), tr("Optio&ns")); optionsButton->setMenu(optionsMenu); this->tabWidget = new QTabWidget(); tabWidget->setDocumentMode(true); // To have less margins, with so many tabs inside tabs... tabWidget->addTab(followersScrollArea, QIcon::fromTheme("meeting-observer"), "followers"); tabWidget->addTab(followingScrollArea, QIcon::fromTheme("meeting-participant"), "following"); tabWidget->addTab(listsScrollArea, QIcon::fromTheme("preferences-contact-list"), "lists"); tabWidget->setCornerWidget(this->optionsButton); this->followersCount = 0; this->followingCount = 0; this->listsCount = 0; this->setTabLabels(); mainLayout->addWidget(tabWidget); this->setLayout(mainLayout); // FIXME: this doesn't really work this->followButton->setFocus(); // Set focus here, away from addressLineEdit qDebug() << "Contact list created"; } ContactList::~ContactList() { qDebug() << "Contact list destroyed"; } void ContactList::clearFollowingListContents() { QLayoutItem *child; while ((child = followingLayout->takeAt(0)) != 0) { child->widget()->deleteLater(); // Delete the widget (contactCard) itself delete child; } this->followingListString.clear(); } void ContactList::clearFollowersListContents() { QLayoutItem *child; while ((child = followersLayout->takeAt(0)) != 0) { child->widget()->deleteLater(); delete child; } this->followersListString.clear(); } void ContactList::setTabLabels() { this->tabWidget->setTabText(0, tr("Follo&wers") + QString(" (%1)").arg(this->followersCount)); this->tabWidget->setTabText(1, tr("Followin&g") + QString(" (%1)").arg(this->followingCount)); this->tabWidget->setTabText(2, tr("&Lists") + QString(" (%1)").arg(this->listsCount)); } /* * Write the list of contacts (following or followers) * to a file selected by the user * */ void ContactList::exportContactsToFile(QString listType) { QString dialogTitle = listType == "following" ? tr("Export list of 'following' to a file") : tr("Export list of 'followers' to a file"); QString suggestedFilename = "dianara-" + pController->currentUsername() + "-" + listType; QString filename = QFileDialog::getSaveFileName(this, dialogTitle, QDir::homePath() + "/" + suggestedFilename, "").trimmed(); if (filename.isEmpty()) // If dialog canceled, do nothing { return; } qDebug() << "Exporting to:" << filename; QFile exportFile(filename); exportFile.open(QIODevice::WriteOnly); if (listType == "following") { exportFile.write(this->followingListString.toLocal8Bit()); } else // "followers" { exportFile.write(this->followersListString.toLocal8Bit()); } exportFile.close(); } /*****************************************************************************/ /*********************************** SLOTS ***********************************/ /*****************************************************************************/ void ContactList::setContactListContents(QString listType, QVariantList contactList, int totalReceivedCount) { qDebug() << "Setting contact list contents"; if (listType == "following") { if (totalReceivedCount == 200) // Only for the first batch { this->clearFollowingListContents(); followingCount = 0; } this->followingCount += contactList.size(); } else { if (totalReceivedCount == 200) { this->clearFollowersListContents(); followersCount = 0; } this->followersCount += contactList.size(); } QMap contactData; QString contactInfoLineString; QStringList followingIdStringList; foreach (QVariant contact, contactList) { QVariantMap contactMap = contact.toMap(); contactData.insert("id", contactMap.value("id").toString().remove(0,5)); // remove "acct:" contactData.insert("name", contactMap.value("displayName").toString()); contactData.insert("avatar", contactMap.value("image").toMap().value("url").toString()); contactData.insert("hometown", contactMap.value("location").toMap().value("displayName").toString()); contactData.insert("bio", contactMap.value("summary").toString()); contactData.insert("url", contactMap.value("url").toString()); contactData.insert("following", contactMap.value("pump_io").toMap().value("followed").toString()); contactInfoLineString = contactData.value("name") + " <" + contactData.value("id") + ">\n"; if (listType == "following") { this->followingLayout->addWidget(new ContactCard(this->pController, contactData, this)); followingListString.append(contactInfoLineString); followingIdStringList.append(contactData.value("id")); } else // == "followers" { this->followersLayout->addWidget(new ContactCard(this->pController, contactData, this)); followersListString.append(contactInfoLineString); } } if (listType == "following") { this->pController->updateInternalFollowingIdList(followingIdStringList); if (totalReceivedCount < pController->currentFollowingCount()) { pController->getContactList(listType, totalReceivedCount); } } else // followers { if (totalReceivedCount < pController->currentFollowersCount()) { pController->getContactList(listType, totalReceivedCount); } } // Update tab labels with number of following or followers, which were updated before this->setTabLabels(); } /* * Fill the list of lists * */ void ContactList::setListsListContents(QVariantList listsList) { this->listsCount = listsList.count(); // Update tab labels with number of following or followers, which were updated before this->setTabLabels(); this->listsManager->setListsList(listsList); } /* * Ask for the updated list of Following * */ void ContactList::refreshFollowing() { qDebug() << "Refreshing list of Following..."; this->pController->getContactList("following"); } /* * Ask for the updated list of followers * */ void ContactList::refreshFollowers() { qDebug() << "Refreshing list of Followers..."; this->pController->getContactList("followers"); } /* * Export list of "Following" to a text file * */ void ContactList::exportFollowing() { qDebug() << "Exporting Following..."; exportContactsToFile("following"); } /* * Export list of "Followers" to a text file * */ void ContactList::exportFollowers() { qDebug() << "Exporting Followers..."; exportContactsToFile("followers"); } void ContactList::refreshLists() { qDebug() << "Refreshing list of lists..."; this->pController->getListsList(); } /* * Add the address entered by the user to the /following list. * * This supports adding webfinger addresses in the form * user@hostname or https://host/username. * */ void ContactList::followContact() { QString address = this->addressLineEdit->text().trimmed(); bool validAddress = false; qDebug() << "ContactList::followContact(); Address entered:" << address; // FIXME, check correctly!!! //////////// if (address.contains("@")) { validAddress = true; } else if (address.startsWith("https://") || address.startsWith("http://")) { address.remove("https://"); address.remove("http://"); if (address.contains("/")) // Very basic sanity check { QStringList addressParts = address.split("/"); address = addressParts.at(1) + "@" + addressParts.at(0); validAddress = true; } else { qDebug() << "Invalid webfinger address!"; } } if (validAddress) { qDebug() << "About to follow this address:" << address; this->pController->followContact(address); // this will trigger a reload of the contact list this->addressLineEdit->clear(); } } dianara-v1.1/src/contactcard.h000664 000764 000764 00000004115 12260657137 015773 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef CONTACTCARD_H #define CONTACTCARD_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mischelpers.h" #include "pumpcontroller.h" class ContactCard : public QFrame { Q_OBJECT public: ContactCard(PumpController *pumpController, QMap contactData, QWidget *parent = 0); ~ContactCard(); void setButtonToFollow(); void setButtonToUnfollow(); signals: public slots: void followContact(); void unfollowContact(); void openProfileInBrowser(); void redrawAvatar(QString avatarUrl, QString avatarFilename); private: QHBoxLayout *mainLayout; QVBoxLayout *centerLayout; QVBoxLayout *rightLayout; QLabel *avatarLabel; QLabel *nameLabel; QLabel *userInfoLabel; QPushButton *followButton; QPushButton *optionsButton; QMenu *optionsMenu; QAction *openProfileAction; QMenu *addToListMenu; PumpController *pController; QString contactID; QString contactURL; QString contactAvatarUrl; }; #endif // CONTACTCARD_H dianara-v1.1/src/contactcard.cpp000664 000764 000764 00000020014 12260657137 016322 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "contactcard.h" ContactCard::ContactCard(PumpController *pumpController, QMap contactData, QWidget *parent) : QFrame(parent) { this->pController = pumpController; this->setFrameStyle(QFrame::Box | QFrame::Raised); this->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); mainLayout = new QHBoxLayout(); avatarLabel = new QLabel(); this->contactAvatarUrl = contactData.value("avatar"); // Get local file name, which is stored in base64 hash form QString avatarFilename = MiscHelpers::getCachedAvatarFilename(contactAvatarUrl); QFile avatarFile(avatarFilename); if (avatarFile.exists(avatarFilename) && !contactAvatarUrl.isEmpty()) { // Load avatar if already cached avatarLabel->setPixmap(QPixmap(avatarFilename).scaled(48, 48, Qt::KeepAspectRatio, Qt::SmoothTransformation)); qDebug() << "ContactCard: Using cached avatar"; } else { // Placeholder image avatarLabel->setPixmap(QIcon::fromTheme("user-identity").pixmap(48,48)); qDebug() << "ContactCard: Using placeholder, downloading avatar"; connect(pController, SIGNAL(avatarStored(QString,QString)), this, SLOT(redrawAvatar(QString,QString))); // Download avatar for next time (if empty, pumpController will do nothing) pController->getAvatar(contactAvatarUrl); } centerLayout = new QVBoxLayout(); QFont nameFont; nameFont.setBold(true); nameFont.setUnderline(true); QString contactNameString = contactData.value("name"); nameLabel = new QLabel(); nameLabel->setText(contactNameString); nameLabel->setFont(nameFont); this->contactID = contactData.value("id"); this->contactURL = contactData.value("url"); userInfoLabel = new QLabel(); userInfoLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); userInfoLabel->setText(QString("<%1>\n").arg(contactID) + tr("Hometown") + QString(": %2").arg(contactData.value("hometown"))); userInfoLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); userInfoLabel->setWordWrap(true); // Bio as tooltip for the whole contact card QString contactBio = contactData.value("bio"); if (!contactBio.isEmpty()) { // Make it rich text, so that it gets wordwrap this->setToolTip("" + tr("Bio for %1", "Abbreviation for Biography, " "but you can use the full word; " "%1=contact name") .arg(contactNameString) + "" "

" + contactBio); } else { if (contactNameString.isEmpty()) { this->setToolTip(tr("This user doesn't have a biography")); } else { this->setToolTip(tr("No biography for %1", "%1=contact name").arg(contactNameString)); } } centerLayout->addWidget(nameLabel); centerLayout->addWidget(userInfoLabel); rightLayout = new QVBoxLayout(); followButton = new QPushButton("*follow*"); followButton->setFlat(true); if (contactData.value("following") == "false") { this->setButtonToFollow(); } else { this->setButtonToUnfollow(); } openProfileAction = new QAction(QIcon::fromTheme("internet-web-browser"), tr("Open Profile in Web Browser"), this); connect(openProfileAction, SIGNAL(triggered()), this, SLOT(openProfileInBrowser())); addToListMenu = new QMenu(tr("In Lists...")); addToListMenu->setIcon(QIcon::fromTheme("format-list-unordered")); addToListMenu->addAction("fake list 1")->setCheckable(true); // FIXME... addToListMenu->addAction("fake list 2")->setCheckable(true); addToListMenu->addAction("fake list 3")->setCheckable(true); optionsMenu = new QMenu("*options*"); optionsMenu->addAction(openProfileAction); //optionsMenu->addMenu(addToListMenu); // Don't include it for now, until 1.2 FIXME optionsButton = new QPushButton(QIcon::fromTheme("user-properties"), tr("User Options")); optionsButton->setFlat(true); optionsButton->setMenu(optionsMenu); rightLayout->addWidget(followButton); rightLayout->addWidget(optionsButton); mainLayout->addWidget(avatarLabel, 1); // Stretch 1/8 mainLayout->addLayout(centerLayout, 6); // Stretch 6/8 mainLayout->addLayout(rightLayout, 1); // 1/8 this->setLayout(mainLayout); qDebug() << "ContactCard created" << this->contactID; } ContactCard::~ContactCard() { qDebug() << "ContactCard destroyed" << this->contactID; } void ContactCard::setButtonToFollow() { followButton->setIcon(QIcon::fromTheme("list-add")); followButton->setText(tr("Follow")); connect(followButton, SIGNAL(clicked()), this, SLOT(followContact())); disconnect(followButton, SIGNAL(clicked()), this, SLOT(unfollowContact())); } void ContactCard::setButtonToUnfollow() { followButton->setIcon(QIcon::fromTheme("list-remove")); followButton->setText(tr("Stop Following")); connect(followButton, SIGNAL(clicked()), this, SLOT(unfollowContact())); disconnect(followButton, SIGNAL(clicked()), this, SLOT(followContact())); } /**************************************************************************/ /******************************** SLOTS ***********************************/ /**************************************************************************/ void ContactCard::followContact() { this->pController->followContact(this->contactID); this->setButtonToUnfollow(); } void ContactCard::unfollowContact() { int confirmation = QMessageBox::question(this, tr("Stop following?"), tr("Are you sure you want to stop following %1?") .arg(this->contactID), tr("&Yes, stop following"), tr("&No"), "", 1, 1); if (confirmation == 0) { this->pController->unfollowContact(this->contactID); this->setButtonToFollow(); } } void ContactCard::openProfileInBrowser() { QDesktopServices::openUrl(this->contactURL); } /* * Redraw contact's avatar after it's been downloaded and stored * */ void ContactCard::redrawAvatar(QString avatarUrl, QString avatarFilename) { if (avatarUrl == this->contactAvatarUrl) { disconnect(pController, SIGNAL(avatarStored(QString,QString)), this, SLOT(redrawAvatar(QString,QString))); avatarLabel->setPixmap(QPixmap(avatarFilename).scaled(48, 48, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } } dianara-v1.1/src/mischelpers.h000664 000764 000764 00000003533 12260657136 016026 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef MISCHELPERS_H #define MISCHELPERS_H #include #include #include #include #include #include #include #include #include // TMP: getting avatar directory #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) #include #endif #include class MiscHelpers : public QObject { Q_OBJECT public: explicit MiscHelpers(QObject *parent = 0); static QString getCachedAvatarFilename(QString url); static QString getCachedImageFilename(QString url); static QString getImageContentType(QString fileURI); static int getImageWidth(QString fileURI); static QString fixLongName(QString name); static QString fileSizeString(QString fileURI); static QStringList htmlWithReplacedImages(QString originalHtml, int postWidth); static QString cleanupHtml(QString originalHtml); static QString quotedText(QString author, QString content); }; #endif // MISCHELPERS_H dianara-v1.1/src/mischelpers.cpp000664 000764 000764 00000016657 12260657135 016373 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "mischelpers.h" MiscHelpers::MiscHelpers(QObject *parent) : QObject(parent) { // Creating object not required } QString MiscHelpers::getCachedAvatarFilename(QString url) { QString localFilename; if (!url.isEmpty()) { #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) localFilename = QDesktopServices::storageLocation(QDesktopServices::DataLocation); #else localFilename = QStandardPaths::standardLocations(QStandardPaths::DataLocation).first(); #endif localFilename.append("/avatars/"); localFilename.append(url.trimmed().toUtf8().toBase64()); QString fileExtension = url; fileExtension.remove(QRegExp(".*\\.")); // remove all but the extension localFilename.append("."); localFilename.append(fileExtension); } return localFilename; } QString MiscHelpers::getCachedImageFilename(QString url) { QString localFilename; if (!url.isEmpty()) { url = url.trimmed(); if (url.startsWith("http://")) { url.remove(0, 7); // remove http:// } if (url.startsWith("https://")) { url.remove(0, 8); // remove https:// } QByteArray base64url = url.toUtf8().toBase64(); base64url.truncate(255); // Limit filename lenght! Just in case the URL is VERY long #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) localFilename = QDesktopServices::storageLocation(QDesktopServices::DataLocation); #else localFilename = QStandardPaths::standardLocations(QStandardPaths::DataLocation).first(); #endif localFilename.append("/images/"); localFilename.append(base64url); } return localFilename; } /* * Return MIME content type, like image/png, etc. * * */ QString MiscHelpers::getImageContentType(QString fileURI) { // QImageReader::setDecideFormatFromContent(true) ? QString formatString = QString::fromLocal8Bit(QImageReader::imageFormat(fileURI)); qDebug() << "Image format:" << formatString; if (formatString == "png" || formatString == "jpeg" || formatString == "gif") { return QString("image/" + formatString); } else { return QString(); } } /* * Return width of an image * */ int MiscHelpers::getImageWidth(QString fileURI) { QImageReader imageReader(fileURI); return imageReader.size().width(); } QString MiscHelpers::fixLongName(QString name) { // very TMP optimization of LOOONG names / FIXME if (name.length() > 16) { name.replace("@", "@ "); name.replace(".", ". "); } return name; } /* * Return a pretty string with the size of a file, like * "33 KiB", "512 bytes" or "3 MiB" * */ QString MiscHelpers::fileSizeString(QString fileURI) { QFileInfo fileInfo(fileURI); int fileSize = fileInfo.size(); QString sizeUnit = tr("bytes"); if (fileSize > 1024) // if > 1024 bytes, transform to KiB { fileSize /= 1024; sizeUnit = "KiB"; } if (fileSize > 1024) // if > 1024 KiB, transform to MiB { fileSize /= 1024; sizeUnit = "MiB"; } return QString("%1 %2").arg(fileSize).arg(sizeUnit); } /* * Parse a string of HTML and replace the URL in each tag with * the corresponding locally cached filename. * * Return also the string list of the URL's to download * */ QStringList MiscHelpers::htmlWithReplacedImages(QString originalHtml, int postWidth) { // if no tags..."; return QStringList(originalHtml); } //qDebug() << "MiscHelpers::htmlWithReplacedImages(); HTML contains some tags..."; QString newHtml = originalHtml; QStringList imageList; QString imgSrc; QRegExp regExp("\\"); regExp.setMinimal(true); int matchedLength = 0; int stringPos = 0; while (matchedLength != -1) { stringPos = regExp.indexIn(newHtml, stringPos); matchedLength = regExp.matchedLength(); //qDebug() << "regExp match = " << regExp.cap(0); //qDebug() << "Groups:" << regExp.cap(1) << " // " << regExp.cap(2) // << " // " << regExp.cap(3) << " // " << regExp.cap(4) // << " // " << regExp.cap(5); //qDebug() << "Matched length is:" << matchedLength; imgSrc = regExp.cap(3); if (!imgSrc.isEmpty()) // if not an empty string, add to the list, and replace HTML { imageList.append(imgSrc); QString cachedImageFilename = getCachedImageFilename(imgSrc); int imageWidth = getImageWidth(cachedImageFilename); // if width is bigger than the post, make it smaller to fit if (imageWidth > postWidth - 40) { imageWidth = postWidth - 40; // Some margins, to account for a scrollbar } newHtml.replace(stringPos, matchedLength, ""); } stringPos += matchedLength; // FIXME: error control } imageList.insert(0, newHtml); // The modified HTML goes before the image list //qDebug() << "Returned HTML and images:\n" << imageList; return imageList; } /* * Basic cleanup of HTML stuff * */ QString MiscHelpers::cleanupHtml(QString originalHtml) { QString cleanHtml = originalHtml; cleanHtml.replace("\n", " "); // Remove line breaks, as that results in server error 500 QRegExp doctypeRE(""); doctypeRE.setMinimal(true); cleanHtml.remove(doctypeRE); QRegExp headRE(".*"); headRE.setMinimal(true); cleanHtml.remove(headRE); cleanHtml.remove(""); QRegExp bodyRE(""); bodyRE.setMinimal(true); cleanHtml.remove(bodyRE); return cleanHtml.trimmed(); } /* * Return some HTML with a blockquote, quote symbols, etc. * * */ QString MiscHelpers::quotedText(QString author, QString content) { QTextDocument textDocument; textDocument.setHtml(content); content = textDocument.toPlainText().trimmed(); content.replace("\n", "
"); QString quoteHtml = ">> "+ author + ":" // >>> + name "
" "“" + content + "”" "

"; return quoteHtml; } dianara-v1.1/src/minorfeed.h000664 000764 000764 00000003514 12260657140 015452 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef MINORFEED_H #define MINORFEED_H #include #include #include #include #include #include #include #include "pumpcontroller.h" #include "mischelpers.h" #include "minorfeeditem.h" #include "asactivity.h" class MinorFeed : public QFrame { Q_OBJECT public: explicit MinorFeed(PumpController *pumpController, QWidget *parent = 0); ~MinorFeed(); void clearContents(); void markAllAsRead(); signals: void newItemsCountChanged(int count); public slots: void updateFeed(); void getMoreActivities(); void setFeedContents(QVariantList activitiesList); void decreaseNewItemsCount(); private: int feedOffset; QString previousNewestActivityId; QList itemsInFeed; int newItemsCount; QVBoxLayout *mainLayout; QVBoxLayout *itemsLayout; QPushButton *getMoreButton; PumpController *pController; }; #endif // MINORFEED_H dianara-v1.1/src/pumpcontroller.cpp000664 000764 000764 00000213137 12264102321 017114 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "pumpcontroller.h" PumpController::PumpController(QObject *parent) : QObject(parent) { connect(&nam, SIGNAL(destroyed()), this, SLOT(tmpfixmeNotifyQNAMdestroyed())); this->userAgentString = "Dianara/1.1"; this->postsPerPageMain = 20; this->postsPerPageOther = 10; this->updatesToTimelineBlocked = false; qoauth = new QOAuth::Interface(); qoauth->setRequestTimeout(10000); // 10 sec timeout QSettings settings; this->clientID = settings.value("clientID", "").toString(); this->clientSecret = settings.value("clientSecret", "").toString(); qoauth->setConsumerKey(clientID.toLocal8Bit()); qoauth->setConsumerSecret(clientSecret.toLocal8Bit()); this->isApplicationAuthorized = settings.value("isApplicationAuthorized", false).toBool(); if (isApplicationAuthorized) { qDebug() << "Dianara is already authorized for user ID:" << settings.value("userID").toString(); this->token = settings.value("token", "").toString().toLocal8Bit(); this->tokenSecret = settings.value("tokenSecret", "").toString().toLocal8Bit(); qDebug() << "Using token" << token; qDebug() << "And token secret" << tokenSecret; } emit this->authorizationStatusChanged(isApplicationAuthorized); connect(&nam, SIGNAL(finished(QNetworkReply*)), this, SLOT(requestFinished(QNetworkReply*))); // FIXME: setting this up for now, to at least have debug messages just in case connect(&nam, SIGNAL(sslErrors(QNetworkReply*,QList)), this, SLOT(sslErrorsHandler(QNetworkReply*,QList))); this->initialDataStep = 0; initialDataTimer = new QTimer(this); initialDataTimer->setSingleShot(false); // Triggered constantly until stopped connect(initialDataTimer, SIGNAL(timeout()), this, SLOT(getInitialData())); qDebug() << "PumpController created"; } PumpController::~PumpController() { qDebug() << "PumpController destroyed"; } void PumpController::setPostsPerPageMain(int ppp) { this->postsPerPageMain = ppp; qDebug() << "PumpController: setting postsPerPage (main) to" << this->postsPerPageMain; } void PumpController::setPostsPerPageOther(int ppp) { this->postsPerPageOther = ppp; qDebug() << "PumpController: setting postsPerPage (other) to" << this->postsPerPageOther; } /* * Block timeline updates. Used while commenting. * */ void PumpController::setUpdatesToTimelineBlocked(bool blocked) { this->updatesToTimelineBlocked = blocked; qDebug() << "PumpController: Updates to timeline blocked? " << this->updatesToTimelineBlocked; } void PumpController::setFilters(QVariantList filterList) { this->currentFilters = filterList; qDebug() << "PumpController::setFilters()" << currentFilters; } QVariantList PumpController::getCurrentFilters() { return this->currentFilters; } /* * Set new user ID (user@domain.tld) and clear OAuth-related tokens/secrets * * */ void PumpController::setNewUserId(QString userID) { this->userId = userID; QStringList splittedUserID = this->userId.split("@"); this->userName = splittedUserID.at(0); // get username, before @ this->serverURL = splittedUserID.at(1); // get URL, after @ qDebug() << "Server URL to connect:" << serverURL << "; username:" << userName; this->clientID.clear(); this->clientSecret.clear(); this->token.clear(); this->tokenSecret.clear(); this->isApplicationAuthorized = false; emit this->authorizationStatusChanged(isApplicationAuthorized); } /* * Get "pumpserver.org" and "user" from "user@pumpserver.org", set OAuth token from Account dlg * */ void PumpController::setUserCredentials(QString userID) { this->initialDataTimer->stop(); // Just in case it was running before this->userId = userID; QStringList splittedUserID = this->userId.split("@"); this->userName = splittedUserID.at(0); this->serverURL = splittedUserID.at(1); qDebug() << "New userID is:" << this->userId; // This will call getUserProfile(), getContactList(), getMainTimeline(), etc. this->haveProfile = false; this->haveFollowing = false; this->haveFollowers = false; this->havePersonLists = false; this->initialDataStep = 0; this->initialDataTimer->start(2000); // start 2 seconds after setting the ID // (mainly on program startup) } QString PumpController::currentUserId() { return this->userId; } QString PumpController::currentUsername() { return this->userName; } QString PumpController::currentFollowersUrl() { return this->userFollowersURL; } int PumpController::currentFollowersCount() { return this->userFollowersCount; } int PumpController::currentFollowingCount() { return this->userFollowingCount; } /* * Get any user's profile (not only our own) * * GET https://pumpserver.example/api/user/username * */ void PumpController::getUserProfile(QString userID) { QStringList splittedUserID = userID.split("@"); QString url = "https://" + splittedUserID.at(1) + "/api/user/" + splittedUserID.at(0); QNetworkRequest userProfileRequest = this->prepareRequest(url, QOAuth::GET, UserProfileRequest); nam.get(userProfileRequest); qDebug() << "Requested user profile:" << userProfileRequest.url().toString(); } /* * Update user's profile * */ void PumpController::updateUserProfile(QString avatarUrl, QString fullName, QString hometown, QString bio) { QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/profile"; QNetworkRequest updateProfileRequest = this->prepareRequest(url, QOAuth::PUT, UpdateProfileRequest); QVariantMap jsonVariantImage; jsonVariantImage.insert("url", avatarUrl); jsonVariantImage.insert("width", 90); // FIXME: don't hardcode this jsonVariantImage.insert("height", 90); // get values from actual pixmap QVariantMap jsonVariantLocation; jsonVariantLocation.insert("objectType", "place"); jsonVariantLocation.insert("displayName", hometown); QVariantMap jsonVariant; jsonVariant.insert("objectType", "person"); if (!avatarUrl.isEmpty()) // Only add image object if a new image was uploaded { jsonVariant.insert("image", jsonVariantImage); } jsonVariant.insert("displayName", fullName); jsonVariant.insert("location", jsonVariantLocation); jsonVariant.insert("summary", bio); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); nam.put(updateProfileRequest, data); qDebug() << "Updating user profile" << fullName << hometown; } void PumpController::getAvatar(QString avatarURL) { if (avatarURL.isEmpty()) { return; } qDebug() << "Getting avatar"; QNetworkRequest avatarRequest(QUrl((const QString)avatarURL)); avatarRequest.setRawHeader("User-Agent", userAgentString); avatarRequest.setAttribute(QNetworkRequest::User, QVariant(AvatarRequest)); nam.get(avatarRequest); } void PumpController::getImage(QString imageURL) { if (imageURL.isEmpty()) { return; } QNetworkRequest imageRequest = this->prepareRequest(imageURL, QOAuth::GET, ImageRequest); nam.get(imageRequest); qDebug() << "imageRequest sent"; } void PumpController::notifyAvatarStored(QString avatarUrl, QString avatarFilename) { emit avatarStored(avatarUrl, avatarFilename); } void PumpController::notifyImageStored(QString imageUrl) { emit imageStored(imageUrl); } /* * GET https://pumpserver.example/api/user/username/following or /followers * */ void PumpController::getContactList(QString listType, int offset) { qDebug() << "Getting contact list, type" << listType << "; offset:" << offset; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/" + listType; QOAuth::ParamMap paramMap; paramMap.insert("count", "200"); // 200 each time paramMap.insert("offset", QString("%1").arg(offset).toLocal8Bit()); QNetworkRequest contactListRequest; if (listType == "following") { emit currentJobChanged(tr("Getting list of 'Following'...")); contactListRequest = this->prepareRequest(url, QOAuth::GET, FollowingListRequest, paramMap); if (offset == 0) { totalReceivedFollowing = 0; followingIdList.clear(); } } else { emit currentJobChanged(tr("Getting list of 'Followers'...")); contactListRequest = this->prepareRequest(url, QOAuth::GET, FollowersListRequest, paramMap); if (offset == 0) { totalReceivedFollowers = 0; } } nam.get(contactListRequest); } /* * Check if a userID is in the "following" list * */ bool PumpController::userInFollowing(QString contactId) { if (followingIdList.contains(contactId)) { return true; } else { return false; } } void PumpController::updateInternalFollowingIdList(QStringList idList) { this->followingIdList.append(idList); } /* * GET https://pumpserver.example/api/user/username/lists/person * */ void PumpController::getListsList() { qDebug() << "Getting list of lists"; QString url = "https://" + this->serverURL + "/api/user/" +this->userName + "/lists/person"; QNetworkRequest listsListRequest = this->prepareRequest(url, QOAuth::GET, ListsListRequest, QOAuth::ParamMap()); emit currentJobChanged(tr("Getting list of person lists...")); nam.get(listsListRequest); } /* * Create a person list * */ void PumpController::createPersonList(QString name, QString description) { qDebug() << "PumpController() creating person list:" << name; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest postRequest = this->prepareRequest(url, QOAuth::POST, CreatePersonListRequest); QVariantList jsonVariantObjectTypes; jsonVariantObjectTypes << "person"; QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "collection"); jsonVariantObject.insert("objectTypes", jsonVariantObjectTypes); jsonVariantObject.insert("displayName", name); jsonVariantObject.insert("content", description); QVariantMap jsonVariant; jsonVariant.insert("verb", "create"); jsonVariant.insert("object", jsonVariantObject); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "About to POST:" << data; emit currentJobChanged(tr("Creating person list...")); nam.post(postRequest, data); } void PumpController::deletePersonList(QString id) { qDebug() << "PumpController::deletePersonList() deleting list" << id; QNetworkRequest deleteRequest = this->prepareRequest(id, QOAuth::DELETE, DeletePersonListRequest); emit currentJobChanged(tr("Deleting person list...")); nam.deleteResource(deleteRequest); } void PumpController::getPersonList(QString url) { qDebug() << "Getting a person list:" << url; QOAuth::ParamMap paramMap; paramMap.insert("count", "200"); // Get 200 members QNetworkRequest personListRequest = this->prepareRequest(url, QOAuth::GET, PersonListRequest, paramMap); emit currentJobChanged(tr("Getting a person list...")); nam.get(personListRequest); } /* * Add a new member to a list * */ void PumpController::addPersonToList(QString listId, QString personId) { qDebug() << "PumpController() adding person to list:" << personId << listId; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest postRequest = this->prepareRequest(url, QOAuth::POST, AddMemberToListRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "person"); jsonVariantObject.insert("id", personId); QVariantMap jsonVariantTarget; jsonVariantTarget.insert("objectType", "collection"); jsonVariantTarget.insert("id", listId); QVariantMap jsonVariant; jsonVariant.insert("verb", "add"); jsonVariant.insert("object", jsonVariantObject); jsonVariant.insert("target", jsonVariantTarget); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "About to POST:" << data; emit currentJobChanged(tr("Adding person to list...")); nam.post(postRequest, data); } /* * Remove member from a list * */ void PumpController::removePersonFromList(QString listId, QString personId) { qDebug() << "PumpController() removing person from list:" << personId << listId; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest postRequest = this->prepareRequest(url, QOAuth::POST, RemoveMemberFromListRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "person"); jsonVariantObject.insert("id", personId); QVariantMap jsonVariantTarget; jsonVariantTarget.insert("objectType", "collection"); jsonVariantTarget.insert("id", listId); QVariantMap jsonVariant; jsonVariant.insert("verb", "remove"); jsonVariant.insert("object", jsonVariantObject); jsonVariant.insert("target", jsonVariantTarget); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "About to POST:" << data; emit currentJobChanged(tr("Removing person from list...")); nam.post(postRequest, data); } /* * Get main timeline * * GET https://pumpserver.example/api/username/inbox/major * */ void PumpController::getMainTimeline(int timelineOffset) { if (this->updatesToTimelineBlocked) { emit currentJobChanged(tr("Main timeline update requested, " "but updates are blocked.")); qDebug() << "Updating timelines requested, but updates are blocked; ignoring"; return; } emit currentJobChanged(tr("Getting main timeline...")); QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/inbox/major"; QOAuth::ParamMap paramMap; paramMap.insert("count", QString("%1").arg(this->postsPerPageMain).toLocal8Bit()); paramMap.insert("offset", QString("%1").arg(timelineOffset).toLocal8Bit()); QNetworkRequest timelineRequest = this->prepareRequest(url, QOAuth::GET, MainTimelineRequest, paramMap); qDebug() << "==================================="; qDebug() << "Getting timeline" << url; qDebug() << "with params:" << paramMap; qDebug() << "\nAuthorization Header:" << timelineRequest.rawHeader("Authorization"); qDebug() << "\n\nFinal URL to retrieve:" << timelineRequest.url().toString(); qDebug() << "==================================="; nam.get(timelineRequest); } /* * Get direct timeline, posts with the user's address in the "To:" field, * that is, sent explicitly to the user * * GET https://pumpserver.example/api/username/inbox/direct * */ void PumpController::getDirectTimeline(int timelineOffset) { if (this->updatesToTimelineBlocked) { emit currentJobChanged(tr("Direct timeline update requested, " "but updates are blocked.")); return; } emit currentJobChanged(tr("Getting direct messages timeline...")); QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/inbox/direct/major"; QOAuth::ParamMap paramMap; paramMap.insert("count", QString("%1").arg(this->postsPerPageOther).toLocal8Bit()); paramMap.insert("offset", QString("%1").arg(timelineOffset).toLocal8Bit()); QNetworkRequest timelineRequest = this->prepareRequest(url, QOAuth::GET, DirectTimelineRequest, paramMap); nam.get(timelineRequest); } /* * Get activity timeline, user's own posts * * GET https://pumpserver.example/api/username/feed/major * */ void PumpController::getActivityTimeline(int timelineOffset) { if (this->updatesToTimelineBlocked) { emit currentJobChanged(tr("Activity timeline update requested, " "but updates are blocked.")); return; } emit currentJobChanged(tr("Getting activity timeline...")); QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed/major"; QOAuth::ParamMap paramMap; paramMap.insert("count", QString("%1").arg(this->postsPerPageOther).toLocal8Bit()); paramMap.insert("offset", QString("%1").arg(timelineOffset).toLocal8Bit()); QNetworkRequest timelineRequest = this->prepareRequest(url, QOAuth::GET, ActivityTimelineRequest, paramMap); nam.get(timelineRequest); } /* * Get favorites timeline, posts where user clicked "like" * * GET https://pumpserver.example/api/username/favorites * */ void PumpController::getFavoritesTimeline(int timelineOffset) { if (this->updatesToTimelineBlocked) { emit currentJobChanged(tr("Favorites timeline update requested, " "but updates are blocked.")); return; } emit currentJobChanged(tr("Getting favorites timeline...")); QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/favorites"; QOAuth::ParamMap paramMap; paramMap.insert("count", QString("%1").arg(this->postsPerPageOther).toLocal8Bit()); paramMap.insert("offset", QString("%1").arg(timelineOffset).toLocal8Bit()); QNetworkRequest timelineRequest = this->prepareRequest(url, QOAuth::GET, FavoritesTimelineRequest, paramMap); nam.get(timelineRequest); } /* * Get list of people who liked a specific post * */ void PumpController::getPostLikes(QString postLikesURL) { qDebug() << "Getting likes for post" << postLikesURL; emit currentJobChanged(tr("Getting likes...")); QOAuth::ParamMap paramMap; paramMap.insert("count", "100"); // TMP, up to 100 likes QNetworkRequest likesRequest = this->prepareRequest(postLikesURL, QOAuth::GET, PostLikesRequest, paramMap); nam.get(likesRequest); } /* * Get comments for one specific post * * GET https://pumpserver.example/api/note/#id#/replies * or proxyed URL. URL is given by the post itself anyway * */ void PumpController::getPostComments(QString postCommentsURL) { qDebug() << "Getting comments for post" << postCommentsURL; emit currentJobChanged(tr("Getting comments...")); QOAuth::ParamMap paramMap; paramMap.insert("count", "200"); // TMP, up to 200 comments / FIXME? QNetworkRequest commentsRequest = this->prepareRequest(postCommentsURL, QOAuth::GET, PostCommentsRequest, paramMap); nam.get(commentsRequest); } /* * Get list of people who shared a specific post * */ void PumpController::getPostShares(QString postSharesURL) { } /* * Get the minor feed, used in the "Meanwhile" column * * GET https://pumpserver.example/api/username/inbox/minor * */ void PumpController::getMinorFeed(int offset) { emit currentJobChanged(tr("Getting minor feed...")); QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/inbox/minor"; QOAuth::ParamMap paramMap; paramMap.insert("count", "50"); paramMap.insert("offset", QString("%1").arg(offset).toLocal8Bit()); QNetworkRequest timelineRequest = this->prepareRequest(url, QOAuth::GET, MinorFeedRequest, paramMap); nam.get(timelineRequest); } /* * Prepare a QNetworkRequest with OAuth header, content type and user agent. * */ QNetworkRequest PumpController::prepareRequest(QString url, QOAuth::HttpMethod method, int requestType, QOAuth::ParamMap paramMap, QString contentTypeString) { QByteArray authorizationHeader = qoauth->createParametersString(url, method, this->token, this->tokenSecret, QOAuth::HMAC_SHA1, paramMap, QOAuth::ParseForHeaderArguments); QNetworkRequest request; // Don't append inline parameters if they're empty, that can mess up things if (!paramMap.isEmpty()) { url.append(qoauth->inlineParameters(paramMap, QOAuth::ParseForInlineQuery)); } request.setUrl(QUrl(url)); // Only add Authorization header if we're requesting something in our server if (request.url().host() == this->serverURL) { request.setRawHeader("Authorization", authorizationHeader); } request.setHeader(QNetworkRequest::ContentTypeHeader, contentTypeString); request.setRawHeader("User-Agent", userAgentString); request.setAttribute(QNetworkRequest::User, QVariant(requestType)); return request; } /* * Upload a file to the /uploads feed for the user * * Used to upload pictures, but might have more uses in the future * */ void PumpController::uploadFile(QString filename, QString contentType, int uploadType) { qDebug() << "PumpController::uploadFile()"; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/uploads"; QNetworkRequest postRequest = this->prepareRequest(url, QOAuth::POST, uploadType, QOAuth::ParamMap(), contentType); QFile file(filename); file.open(QIODevice::ReadOnly); QByteArray data = file.readAll(); file.close(); nam.post(postRequest, data); } QList PumpController::processAudience(QMap audienceMap) { QVariantList jsonVariantTo; QVariantList jsonVariantCC; QVariantMap jsonVariantAudienceItem; while (!audienceMap.isEmpty()) { jsonVariantAudienceItem.clear(); if (audienceMap.keys().contains("to|collection")) // To: { QString collectionID = audienceMap.take("to|collection"); jsonVariantAudienceItem.insert("objectType", "collection"); jsonVariantAudienceItem.insert("id", collectionID); jsonVariantTo.append(jsonVariantAudienceItem); } else if (audienceMap.keys().contains("to|person")) { QString personID = audienceMap.take("to|person"); jsonVariantAudienceItem.insert("objectType", "person"); jsonVariantAudienceItem.insert("id", personID); jsonVariantTo.append(jsonVariantAudienceItem); } else if (audienceMap.keys().contains("cc|collection")) // CC: { QString collectionID = audienceMap.take("cc|collection"); jsonVariantAudienceItem.insert("objectType", "collection"); jsonVariantAudienceItem.insert("id", collectionID); jsonVariantCC.append(jsonVariantAudienceItem); } else if (audienceMap.keys().contains("cc|person")) { QString personID = audienceMap.take("cc|person"); jsonVariantAudienceItem.insert("objectType", "person"); jsonVariantAudienceItem.insert("id", personID); jsonVariantCC.append(jsonVariantAudienceItem); } } QList jsonVariantToAndCCList; jsonVariantToAndCCList.append(jsonVariantTo); jsonVariantToAndCCList.append(jsonVariantCC); return jsonVariantToAndCCList; } /***************************************************************************/ /***************************************************************************/ /***************************************************************************/ /***************************************************************************/ /*********************************** SLOTS *********************************/ /***************************************************************************/ /***************************************************************************/ /***************************************************************************/ /***************************************************************************/ void PumpController::requestFinished(QNetworkReply *reply) { int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); int requestType = reply->request().attribute(QNetworkRequest::User).toInt(); bool finished = reply->isFinished(); QString replyUrl = reply->url().toString(); QString replyHost = reply->url().host(); QString replyErrorString = reply->errorString(); QByteArray replyFirstLine = reply->readLine(); QByteArray replyData = replyFirstLine + reply->readAll(); qDebug() << "Request finished. HTTP code:" << httpCode; qDebug() << "Size:" << reply->size() << "bytes; URL:" << replyUrl; qDebug() << "isFinished()?" << finished << "; Request type:" << requestType; // We got all necessary data, clean up reply->deleteLater(); // Special control after sending a post or a comment if (httpCode != 200) // if not OK { if (requestType == PublishPostRequest) { emit postPublishingFailed(); } else if (requestType == UpdatePostRequest) { emit postPublishingFailed(); // kinda TMP } else if (requestType == CommentPostRequest) { emit commentPostingFailed(); } } QString errorTypeString; switch (httpCode) { // First, handle error codes case 503: errorTypeString = tr("Service Unavailable", "HTTP 503 error string") + " (503)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); // if not just an image, it's important, so popup notification too if (requestType != ImageRequest && requestType != AvatarRequest) { emit showNotification(errorTypeString + "\n" + replyUrl); } qDebug() << "HTTP 503: Service Unavailable."; qDebug() << "Data: " << replyData; return; case 500: errorTypeString = tr("Internal Server Error", "HTTP 500 error string") + " (500)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); // if not just an image, it's important, so popup notification too if (requestType != ImageRequest && requestType != AvatarRequest) { emit showNotification(errorTypeString + "\n" + replyUrl + "\n\n" + replyFirstLine); } qDebug() << "HTTP 500: Internal Server Error."; qDebug() << "Data: " << replyData; return; case 410: errorTypeString = tr("Gone", "HTTP 410 error string") + " (410)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); qDebug() << "HTTP 410: Gone."; qDebug() << "Data: " << replyData; return; case 404: errorTypeString = tr("Not Found", "HTTP 404 error string") + " (404)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); qDebug() << "HTTP 404: Not Found."; qDebug() << "Data: " << replyData; return; case 403: errorTypeString = tr("Forbidden", "HTTP 403 error string") + " (403)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); qDebug() << "HTTP 403: Forbidden."; qDebug() << "Data: " << replyData; return; case 401: errorTypeString = tr("Unauthorized", "HTTP 401 error string") + " (401)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); qDebug() << "HTTP 401: Unauthorized."; qDebug() << "Data: " << replyData; return; case 400: errorTypeString = tr("Bad Request", "HTTP 400 error string") + " (400)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); // if not just an image, it's important, so popup notification too if (requestType != ImageRequest && requestType != AvatarRequest) { emit showNotification(errorTypeString + "\n" + replyUrl + "\n\n" + replyFirstLine); } qDebug() << "HTTP 400: Bad Request."; qDebug() << "Data: " << replyData; return; case 302: errorTypeString = tr("Moved Temporarily", "HTTP 302 error string") + " (302)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); qDebug() << "HTTP 302: Moved Temporarily."; qDebug() << "Data: " << replyData; return; case 301: errorTypeString = tr("Moved Permanently", "HTTP 301 error string") + " (301)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); qDebug() << "HTTP 301: Moved Permanently."; qDebug() << "Data: " << replyData; return; case 0: emit currentJobChanged(tr("Error connecting to %1").arg(replyHost) + ": " + replyErrorString); qDebug() << "Error connecting to" << replyUrl; return; // Other HTTP codes default: emit currentJobChanged(tr("Unhandled HTTP error code %1").arg(httpCode) + ": " + replyUrl); qDebug() << "Unhandled HTTP error " << httpCode; qDebug() << "Data: " << replyData; return; //////////////////////////////////////// The good one! case 200: qDebug() << "HTTP 200: OK!"; } // At this point, httpCode should be 200 = OK // Prepare the JSON parser QJson::Parser jsonParser; bool jsonParsedOK = false; QVariantMap jsonData; QVariantList jsonDataList; // Unless it was an AvatarRequest or an ImageRequest, it should be JSON, so parse it if (requestType != AvatarRequest && requestType != ImageRequest) { jsonData = jsonParser.parse(replyData, &jsonParsedOK).toMap(); qDebug() << "JSON data size (items):" << jsonData.size(); qDebug() << "Keys:" << jsonData.keys(); } ////////////////////////////////////////////////////////////////// switch (requestType) { case ClientRegistrationRequest: qDebug() << "Client Registration was requested"; qDebug() << "Raw JSON:" << jsonData; if (jsonParsedOK && jsonData.size() > 0) { this->clientID = jsonData["client_id"].toString(); this->clientSecret = jsonData["client_secret"].toString(); // FIXME: error control, etc. // check if jsonData.keys().contains("client_id") !! QSettings settings; settings.setValue("clientID", this->clientID); settings.setValue("clientSecret", this->clientSecret); this->getToken(); } break; case UserProfileRequest: qDebug() << "A user profile was requested"; if (jsonParsedOK && jsonData.size() > 0) { emit currentJobChanged(tr("Profile received.")); QVariantMap profileMap = jsonData["profile"].toMap(); if (profileMap.value("id").toString() == "acct:" + this->userId) { qDebug() << "Received OWN profile"; this->haveProfile = true; QString profImageUrl = profileMap.value("image").toMap().value("url").toString(); QString profDisplayName = profileMap.value("displayName").toString(); QString profLocation = profileMap.value("location").toMap().value("displayName").toString(); QString profSummary = profileMap.value("summary").toString(); QString userEmail = jsonData["email"].toString(); qDebug() << "E-mail configured for the account:" << userEmail; emit profileReceived(profImageUrl, profDisplayName, profLocation, profSummary, userEmail); // Store also the user's followers URL, for posting to Followers this->userFollowersURL = profileMap.value("followers").toMap().value("url").toString(); this->userFollowersCount = profileMap.value("followers").toMap().value("totalItems").toInt(); this->userFollowingCount = profileMap.value("following").toMap().value("totalItems").toInt(); qDebug() << "Followers count:" << userFollowersCount; qDebug() << "Following count:" << userFollowingCount; } } break; case UpdateProfileRequest: emit currentJobChanged(tr("Profile updated.")); this->getUserProfile(this->userId); break; ////////////////////////////////////////////////// If a timeline was requested case MainTimelineRequest: // just jump to the next case DirectTimelineRequest: // just jump to next case ActivityTimelineRequest: // just... yeah, jump case FavoritesTimelineRequest: qDebug() << "A timeline was requested"; if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; emit currentJobChanged(tr("Timeline received. Updating post list...")); jsonDataList = jsonData["items"].toList(); qDebug() << "Number of items in timeline:" << jsonDataList.size(); QString previousLink = jsonData["links"].toMap().value("prev").toMap().value("href").toString(); QString nextLink = jsonData["links"].toMap().value("next").toMap().value("href").toString(); if (requestType == MainTimelineRequest) { qDebug() << "It was the main timeline"; emit mainTimelineReceived(jsonDataList, this->postsPerPageMain, previousLink, nextLink); } else if (requestType == DirectTimelineRequest) { qDebug() << "It was the direct messages timeline"; emit directTimelineReceived(jsonDataList, this->postsPerPageOther, previousLink, nextLink); } else if (requestType == ActivityTimelineRequest) { qDebug() << "It was the own activity timeline"; emit activityTimelineReceived(jsonDataList, this->postsPerPageOther, previousLink, nextLink); } else if (requestType == FavoritesTimelineRequest) { qDebug() << "It was the favorites timeline"; emit favoritesTimelineReceived(jsonDataList, this->postsPerPageOther, previousLink, nextLink); } } else { qDebug() << "Error parsing received JSON data!"; qDebug() << "Raw data:" << replyData; // JSON directly } break; //////////////////////////////////// case PublishPostRequest: emit currentJobChanged(tr("Post published successfully.")); if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; QString objectID = jsonData["object"].toMap().value("id").toString(); qDebug() << "Image post ID:" << objectID; if (jsonData["object"].toMap().value("objectType").toString() == "image") { // Update the image with title and description this->postImageStepThree(objectID); } else { // Not an image, notify "posted OK" emit postPublished(); qDebug() << "Non-image post published correctly"; } } else { qDebug() << "Error parsing received JSON data!"; qDebug() << "Raw data:" << replyData; // JSON directly } break; case PublishAvatarRequest: emit currentJobChanged(tr("Avatar published successfully.")); if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; QString imageUrl = jsonData["object"].toMap().value("image").toMap().value("url").toString(); qDebug() << "Avatar post ID:" << imageUrl; // FIXME: get "pump_io: fullImage: url" too if (jsonData["object"].toMap().value("objectType").toString() == "image") { emit avatarUploaded(imageUrl); } else { qDebug() << "Avatar uploaded, but type is not IMAGE!"; } } else { qDebug() << "Error parsing received JSON data!"; qDebug() << "Raw data:" << replyData; // JSON directly } break; //////////////////////////////////// case UpdatePostRequest: emit currentJobChanged(tr("Post updated successfully.")); emit postPublished(); break; ///////////////////////////////////// If liking a post was requested case LikePostRequest: emit currentJobChanged(tr("Message liked or unliked successfully.")); emit likeSet(); break; ///////////////////////////////////// If the likes for a post were requested case PostLikesRequest: qDebug() << "Likes for a post were requested" << replyUrl; if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; emit currentJobChanged(tr("Likes received.")); jsonDataList = jsonData["items"].toList(); qDebug() << "Number of items in comments list:" << jsonDataList.size(); emit likesReceived(jsonDataList, replyUrl); } else { qDebug() << "Error parsing received comment JSON data!"; qDebug() << "Raw data:" << replyData; } break; ///////////////////////////////////// If commenting on a post was requested case CommentPostRequest: emit currentJobChanged(tr("Comment posted successfully.")); emit commentPosted(); // This will be caught by Commenter() break; ///////////////////////////////////// If the comments for a post were requested case PostCommentsRequest: qDebug() << "Comments for a post were requested" << replyUrl; if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; emit currentJobChanged(tr("Comments received.")); jsonDataList = jsonData["items"].toList(); qDebug() << "Number of items in comments list:" << jsonDataList.size(); emit commentsReceived(jsonDataList, replyUrl); } else { qDebug() << "Error parsing received comment JSON data!"; qDebug() << "Raw data:" << replyData; } break; case SharePostRequest: emit currentJobChanged(tr("Post shared successfully.")); break; case PostSharesRequest: // TODO break; case MinorFeedRequest: qDebug() << "The minor feed was requested"; if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; emit currentJobChanged(tr("Minor feed received.")); jsonDataList = jsonData["items"].toList(); qDebug() << "Number of items in minor feed:" << jsonDataList.size(); emit minorFeedReceived(jsonDataList); } else { qDebug() << "Error parsing received JSON data!"; qDebug() << "Raw data:" << replyData; // JSON directly } break; case DeletePostRequest: emit currentJobChanged(tr("Message deleted successfully.")); break; case FollowContactRequest: emit currentJobChanged(tr("Following successfully.")); // Re-request the contact list, now with a new contact (not very optimized...) this->getContactList("following"); break; case UnfollowContactRequest: emit currentJobChanged(tr("Stopped following successfully.")); // Re-request the contact list, now with one contact less (not very optimized...) this->getContactList("following"); break; case FollowingListRequest: // just go to the next case FollowersListRequest: qDebug() << "A contact list was requested"; if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; QVariant contactsVariant = jsonData.value("items"); if (contactsVariant.type() == QVariant::List) { qDebug() << "Parsed a List, listing contacts..."; if (requestType == FollowingListRequest) { totalReceivedFollowing += 200; emit contactListReceived("following", contactsVariant.toList(), this->totalReceivedFollowing); if (totalReceivedFollowing >= this->userFollowingCount) { this->haveFollowing = true; emit currentJobChanged(tr("List of 'following' completely received.")); } else { emit currentJobChanged(tr("Partial list of 'following' received.")); } } else // == FollowersListRequest { totalReceivedFollowers += 200; emit contactListReceived("followers", contactsVariant.toList(), this->totalReceivedFollowers); if (totalReceivedFollowers >= this->userFollowersCount) { this->haveFollowers = true; emit currentJobChanged(tr("List of 'followers' completely received.")); } else { emit currentJobChanged(tr("Partial list of 'followers' received.")); } } } else { qDebug() << "Expected a list of contacts, received something else:"; qDebug() << jsonData; } } else { qDebug() << "Error parsing received JSON data!"; qDebug() << "Raw data:" << replyData; // JSON directly } break; case ListsListRequest: qDebug() << "The list of person lists was requested"; if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; QVariant listsVariant = jsonData.value("items"); if (listsVariant.type() == QVariant::List) { qDebug() << "Parsed a List, listing lists..."; this->havePersonLists = true; emit currentJobChanged(tr("List of 'lists' received.")); emit listsListReceived(listsVariant.toList()); } else { qDebug() << "Expected a list of lists, received something else:"; qDebug() << jsonData; } } else { qDebug() << "Error parsing received JSON data!"; qDebug() << "Raw data:" << replyData; // JSON directly } break; // Person list created OK case CreatePersonListRequest: emit currentJobChanged(tr("Person list created successfully.")); this->getListsList(); // And reload the person lists break; // Person list created OK case DeletePersonListRequest: emit currentJobChanged(tr("Person list deleted successfully.")); this->getListsList(); // And reload the person lists break; case PersonListRequest: qDebug() << "A person list was requested"; if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; QVariant personListVariant = jsonData.value("items"); if (personListVariant.type() == QVariant::List) { qDebug() << "Parsed a List, listing people in list..."; emit currentJobChanged(tr("Person list received.")); emit personListReceived(personListVariant.toList(), replyUrl); } else { qDebug() << "Expected a list of people, received something else:"; qDebug() << jsonData; } } else { qDebug() << "Error parsing received JSON data!"; qDebug() << "Raw data:" << replyData; // JSON directly } break; // Person added to list OK case AddMemberToListRequest: if (jsonParsedOK && jsonData.size() > 0) { QString personId = jsonData.value("object").toMap().value("id").toString(); QString personName = jsonData.value("object").toMap().value("displayName").toString(); //<< jsonData.value("target").toMap().value("id").toString() emit personAddedToList(personId, personName); emit currentJobChanged(tr("Person added to list successfully.")); } break; // Person removed from list OK case RemoveMemberFromListRequest: if (jsonParsedOK && jsonData.size() > 0) { QString personId = jsonData.value("object").toMap().value("id").toString(); emit personRemovedFromList(personId); emit currentJobChanged(tr("Person removed from list successfully.")); } break; case AvatarRequest: qDebug() << "Received AVATAR data, from " << replyUrl; if (finished) { qDebug() << "Avatar received 100%"; emit avatarPictureReceived(replyData, replyUrl); } else { qDebug() << "Avatar not complete yet"; } break; case ImageRequest: qDebug() << "Received IMAGE data, from " << replyUrl; if (finished) { qDebug() << "Image received 100%"; emit imageReceived(replyData, replyUrl); } else { qDebug() << "Image not complete yet"; } break; //////////////////////////////////////// If uploading a file was requested case UploadAvatarRequest: // just jump to next case UploadPictureRequest: // just jump case UploadFileRequest: qDebug() << "Uploading a file was requested"; if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; QString uploadedFileID = jsonData["id"].toString(); qDebug() << "Uploaded file ID:" << uploadedFileID; if (jsonData["objectType"].toString() == "image") { if (requestType == UploadPictureRequest) { emit currentJobChanged(tr("File uploaded successfully. Posting message...")); this->postImageStepTwo(uploadedFileID); } else if (requestType == UploadAvatarRequest) { emit currentJobChanged(tr("Avatar uploaded.")); this->postAvatarStepTwo(uploadedFileID); } } } else { qDebug() << "Error parsing received JSON data!"; qDebug() << "Raw data:" << replyData; // JSON directly } break; } // end switch (requestType) qDebug() << "requestFinished() ended; " << replyUrl; } void PumpController::sslErrorsHandler(QNetworkReply *reply, QList errorList) { qDebug() << "\n==== SSL errors!! ===="; qDebug() << "At:" << reply->url().toString(); qDebug() << "Error list:" << errorList << "\n\n"; // ignore completely for now qDebug() << "Ignoring these errors..."; reply->ignoreSslErrors(); /* // Check list of SSL errors foreach (QSslError sslError, errorList) { // Ignore the "certificate is self-signed" SSL error, it's expected if (sslError.error() == QSslError::SelfSignedCertificate) { qDebug() << "Ignoring self-signed certificate error"; reply->ignoreSslErrors(); // FIXME, maybe ask the user return; } } */ // Clean up reply->deleteLater(); } void PumpController::getToken() { // If we do not have client_id or client_secret, do dynamic client registration if (this->clientID.isEmpty() || this->clientSecret.isEmpty()) { qDebug() << "PumpController::getToken()"; qDebug() << "We do not have client_id/client_secret yet; doing Dynamic Client Registration"; // POST to https://hotpump.net/api/client/register, f.e. QNetworkRequest postRequest(QUrl("https://" + this->serverURL + "/api/client/register")); postRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); postRequest.setRawHeader("User-Agent", userAgentString); postRequest.setAttribute(QNetworkRequest::User, QVariant(ClientRegistrationRequest)); QByteArray data("{" " \"type\": \"client_associate\", " " \"application_type\": \"native\", " " \"application_name\": \"Dianara\" " "}"); qDebug() << "About to POST:" << data; // upon receiving data (id+secret), will execute getToken() again nam.post(postRequest, data); } else { qDebug() << "Using saved client_id and client_secret:" << this->clientID << this->clientSecret; // OAuth stuff..... // 1. obtaining an unauthorized Request Token from the Service Provider, // 2. asking the User to authorize the Request Token, // 3. exchanging the Request Token for the Access Token qDebug() << "Doing OAuth token stuff..."; qDebug() << "NOTE: if you see a crash here, you need QCA and its openSSL plugin:"; qDebug() << ">> qca2-plugin-openssl, libqca2-plugin-ossl, or similar"; qDebug() << "If you compiled Dianara from source, check the INSTALL file carefully"; QStringList QCAsupportedFeatures = QCA::supportedFeatures(); qDebug() << "QCA Supported Features:" << QCAsupportedFeatures; if (QCAsupportedFeatures.contains("hmac(sha1)")) { qDebug() << "HMAC-SHA1 support is OK"; } else { qDebug() << "Warning, HMAC-SHA1 doesn't seem to be supported!"; // TMPFIX notify the user properly, etc. } qoauth->setConsumerKey(this->clientID.toLocal8Bit()); qoauth->setConsumerSecret(this->clientSecret.toLocal8Bit()); QString requestTokenURL = "https://" + this->serverURL + "/oauth/request_token"; qDebug() << "GET: " << requestTokenURL << "with" << qoauth->consumerKey() << qoauth->consumerSecret(); QOAuth::ParamMap oAuthParams; oAuthParams.insert("oauth_callback", "oob"); QOAuth::ParamMap reply = qoauth->requestToken(requestTokenURL, QOAuth::GET, QOAuth::HMAC_SHA1, // or PLAINTEXT? oAuthParams); if (qoauth->error() == QOAuth::NoError) { qDebug() << "requestToken OK:" << reply.keys(); token = reply.value(QOAuth::tokenParameterName()); tokenSecret = reply.value(QOAuth::tokenSecretParameterName()); qDebug() << "Token:" << token; qDebug() << "Token Secret:" << tokenSecret; QUrl oAuthAuthorizeURL("https://" + this->serverURL + "/oauth/authorize"); #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) oAuthAuthorizeURL.addQueryItem("oauth_token", token); #else QUrlQuery query; query.addQueryItem("oauth_token", token); oAuthAuthorizeURL.setQuery(query); #endif QDesktopServices::openUrl(oAuthAuthorizeURL); // Send also a signal, so AccountDialog can show the URL in // a label, in case the browser didn't launch emit openingAuthorizeURL(oAuthAuthorizeURL); // Now, user should enter VERIFIER in AccountDialog to authorize the program } else { qDebug() << "QOAuth error" << qoauth->error() << "!"; qDebug() << reply.keys(); } } } void PumpController::authorizeApplication(QString verifierCode) { qDebug() << "Verifier code entered by user:" << verifierCode; QOAuth::ParamMap moreParams; moreParams.insert("oauth_verifier", verifierCode.toUtf8()); // verifier as QByteArray QString requestAuthorizationURL = "https://" + this->serverURL + "/oauth/access_token"; QOAuth::ParamMap reply = qoauth->accessToken(requestAuthorizationURL, QOAuth::GET, token, tokenSecret, QOAuth::HMAC_SHA1, moreParams); if (qoauth->error() == QOAuth::NoError) // Woooohooo!! { qDebug() << "Got authorized token; Dianara is authorized to access the account"; token = reply.value(QOAuth::tokenParameterName()); tokenSecret = reply.value(QOAuth::tokenSecretParameterName()); this->isApplicationAuthorized = true; QSettings settings; settings.setValue("isApplicationAuthorized", this->isApplicationAuthorized); settings.setValue("token", this->token); settings.setValue("tokenSecret", this->tokenSecret); qDebug() << "Token:" << token; qDebug() << "TokenSecret:" << tokenSecret; emit this->authorizationStatusChanged(isApplicationAuthorized); } else { qDebug() << "OAuth error while authorizing application" << qoauth->error(); } } /* * Called by a QTimer, get initial data (profile, contacts, timelines), * one step at a time * */ void PumpController::getInitialData() { qDebug() << "PumpController::getInitialData() step" << initialDataStep; initialDataTimer->setInterval(3000); // Every 3 sec /* * FIXME: this needs to be way more elaborate. * * Ensure certain stuff has been received before continuing. * * For instance, getting the "following" list is needed before * being able to post, and to know the state of following/not Following * the author of a post in a timeline * */ switch (this->initialDataStep) { case 0: this->getUserProfile(this->userId); break; case 1: this->getContactList("following"); break; case 2: this->getContactList("followers"); break; case 3: this->getMainTimeline(0); break; case 4: this->getListsList(); break; case 5: this->getMinorFeed(); break; case 6: this->getDirectTimeline(0); break; case 7: this->getActivityTimeline(0); break; case 8: this->getFavoritesTimeline(0); break; case 9: // Do nothing, so it takes longer for the final (default) step to arrive break; default: emit currentJobChanged(tr("Ready.")); initialDataTimer->stop(); qDebug() << "--------------------------------------"; qDebug() << "-- All initial data loaded -----------"; qDebug() << "--------------------------------------"; } ++initialDataStep; } /* * Send a NOTE to the server * */ void PumpController::postNote(QMap audienceMap, QString postText, QString postTitle) { qDebug() << "PumpController::postNote()"; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest postRequest = this->prepareRequest(url, QOAuth::POST, PublishPostRequest); qDebug() << "Should be posting to:" << audienceMap; QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "note"); if (!postTitle.isEmpty()) { jsonVariantObject.insert("displayName", postTitle); } jsonVariantObject.insert("content", postText); QVariantMap jsonVariant; jsonVariant.insert("verb", "post"); jsonVariant.insert("object", jsonVariantObject); QList audience = processAudience(audienceMap); jsonVariant.insert("to", audience.at(0)); jsonVariant.insert("cc", audience.at(1)); QJson::Serializer serializer; // bool ok; // QByteArray data = serializer.serialize(jsonVariant, &ok); // This way to make it work with QJSON 0.7.x QByteArray data = serializer.serialize(jsonVariant); qDebug() << "About to POST:" << data; nam.post(postRequest, data); } /* * Send an IMAGE to the server * * First, upload the file. * Then we get it's ID in a signal, and create the post itself * */ void PumpController::postImage(QMap audienceMap, QString postText, QString imageTitle, QString imageFilename, QString contentType) { qDebug() << "PumpController::postImage()"; qDebug() << "Uploading" << imageFilename << "with title:" << imageTitle; // Store postText, imageTitle, and audienceMap, then upload this->currentImageTitle = imageTitle; this->currentImageDescription = postText; this->currentAudienceMap = audienceMap; this->uploadFile(imageFilename, contentType, UploadPictureRequest); } /* * Post Image, step 2: after getting the ID in the file upload request, * create the post itself * */ void PumpController::postImageStepTwo(QString id) { qDebug() << "PumpController::postImageStepTwo() image ID:" << id; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest postRequest = this->prepareRequest(url, QOAuth::POST, PublishPostRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "image"); jsonVariantObject.insert("id", id); QVariantMap jsonVariant; jsonVariant.insert("verb", "post"); jsonVariant.insert("object", jsonVariantObject); QList audience = processAudience(this->currentAudienceMap); jsonVariant.insert("to", audience.at(0)); jsonVariant.insert("cc", audience.at(1)); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "About to POST:" << data; nam.post(postRequest, data); } /* * Workaround for the non-title-non-description issue * * Update the image post with the title and the description * */ void PumpController::postImageStepThree(QString id) { qDebug() << "PumpController::postImageStepThree() post ID:" << id; // Using the ID directly as URL QNetworkRequest postRequest = this->prepareRequest(id, QOAuth::PUT, UpdatePostRequest); QVariantMap jsonVariant; jsonVariant.insert("verb", "update"); // "post" would suffice if (!this->currentImageTitle.isEmpty()) { jsonVariant.insert("displayName", this->currentImageTitle); } jsonVariant.insert("content", this->currentImageDescription); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "About to PUT:" << data; nam.put(postRequest, data); } /* * Second step for avatar upload. * * Post the image to Public * */ void PumpController::postAvatarStepTwo(QString id) { qDebug() << "PumpController::postAvatarStepTwo() image ID:" << id; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest postRequest = this->prepareRequest(url, QOAuth::POST, PublishAvatarRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "image"); jsonVariantObject.insert("id", id); // audience, CC: Public QVariantMap jsonVariantPublic; jsonVariantPublic.insert("objectType", "collection"); jsonVariantPublic.insert("id", "http://activityschema.org/collection/public"); QVariantList jsonVariantAudience; jsonVariantAudience.append(jsonVariantPublic); QVariantMap jsonVariant; jsonVariant.insert("verb", "post"); jsonVariant.insert("object", jsonVariantObject); jsonVariant.insert("cc", jsonVariantAudience); // CC: Public QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "About to POST:" << data; nam.post(postRequest, data); } /* * Update a post contents (the object) * */ void PumpController::updatePost(QString id, QString content, QString title) { qDebug() << "PumpController::updatePost(), post ID:" << id; // Using the ID directly as URL QNetworkRequest postRequest = this->prepareRequest(id, QOAuth::PUT, UpdatePostRequest); QVariantMap jsonVariant; jsonVariant.insert("verb", "update"); // "post" would suffice jsonVariant.insert("displayName", title); // "title" can be empty, as a way to remove titles jsonVariant.insert("content", content); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "About to PUT:" << data; nam.put(postRequest, data); } /* * Like (favorite) a post, by its ID (URL) * */ void PumpController::likePost(QString postID, QString postType, bool like) { qDebug() << "PumpController::likePost() liking post" << postID; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest likeRequest = this->prepareRequest(url, QOAuth::POST, LikePostRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", postType); jsonVariantObject.insert("id", postID); QVariantMap jsonVariant; jsonVariant.insert("verb", like ? "favorite":"unfavorite"); // like or unlike jsonVariant.insert("object", jsonVariantObject); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "about to POST:" << data; nam.post(likeRequest, data); } void PumpController::addComment(QString comment, QString postID, QString postType) { qDebug() << "PumpController::addComment() sending comment to this post:" << postID; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest commentRequest = this->prepareRequest(url, QOAuth::POST, CommentPostRequest); QVariantMap jsonVariantInReplyTo; jsonVariantInReplyTo.insert("id", postID); jsonVariantInReplyTo.insert("objectType", postType); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "comment"); jsonVariantObject.insert("content", comment); jsonVariantObject.insert("inReplyTo", jsonVariantInReplyTo); QVariantMap jsonVariant; jsonVariant.insert("verb", "post"); jsonVariant.insert("object", jsonVariantObject); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "about to POST:" << data; nam.post(commentRequest, data); } void PumpController::sharePost(QString postID, QString postType) { qDebug() << "PumpController::sharePost() sharing post" << postID; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest shareRequest = this->prepareRequest(url, QOAuth::POST, SharePostRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", postType); jsonVariantObject.insert("id", postID); QVariantMap jsonVariant; jsonVariant.insert("verb", "share"); jsonVariant.insert("object", jsonVariantObject); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "about to POST:" << data; nam.post(shareRequest, data); } void PumpController::unsharePost(QString postId, QString postType) { qDebug() << "PumpController::unsharePost() unsharing post" << postId; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest unshareRequest = this->prepareRequest(url, QOAuth::POST, UnsharePostRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", postType); jsonVariantObject.insert("id", postId); QVariantMap jsonVariant; jsonVariant.insert("verb", "unshare"); jsonVariant.insert("object", jsonVariantObject); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "about to POST:" << data; nam.post(unshareRequest, data); } void PumpController::deletePost(QString postID, QString postType) { qDebug() << "PumpController::deletePost() deleting post" << postID; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest deleteRequest = this->prepareRequest(url, QOAuth::POST, DeletePostRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", postType); jsonVariantObject.insert("id", postID); QVariantMap jsonVariant; jsonVariant.insert("verb", "delete"); jsonVariant.insert("object", jsonVariantObject); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "about to POST:" << data; nam.post(deleteRequest, data); } /* * Add a contact to the /following list with their webfinger address * * This will trigger a re-request of the contact list, upon receiving the HTTP 200 * confirming the addition of the contact to /following * */ void PumpController::followContact(QString address) { qDebug() << "PumpController::followContact()" << address; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest followRequest = this->prepareRequest(url, QOAuth::POST, FollowContactRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "person"); jsonVariantObject.insert("id", "acct:" + address); QVariantMap jsonVariant; jsonVariant.insert("verb", "follow"); jsonVariant.insert("object", jsonVariantObject); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "about to POST:" << data; nam.post(followRequest, data); } /* * Remove a contact from the /following list with their webfinger address * * This will trigger a re-request of the contact list, upon receiving the HTTP 200 * confirming the removal of the contact from /following * */ void PumpController::unfollowContact(QString address) { qDebug() << "PumpController::unfollowContact()" << address; QString url = "https://" + this->serverURL + "/api/user/" + this->userName + "/feed"; QNetworkRequest unfollowRequest = this->prepareRequest(url, QOAuth::POST, UnfollowContactRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "person"); jsonVariantObject.insert("id", "acct:" + address); QVariantMap jsonVariant; jsonVariant.insert("verb", "stop-following"); jsonVariant.insert("object", jsonVariantObject); QJson::Serializer serializer; QByteArray data = serializer.serialize(jsonVariant); qDebug() << "about to POST:" << data; nam.post(unfollowRequest, data); } void PumpController::tmpfixmeNotifyQNAMdestroyed() { qDebug() << "QNAM DESTROYED!!! ########################################\n\n\n\nBOOM!!\n\n"; } dianara-v1.1/src/commenterblock.cpp000664 000764 000764 00000024415 12260657137 017052 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "commenterblock.h" CommenterBlock::CommenterBlock(PumpController *pumpController, QWidget *parent) : QWidget(parent) { this->pController = pumpController; this->showAllCommentsLinkLabel = new QLabel("" + tr("Show All Comments") + ""); showAllCommentsLinkLabel->setContextMenuPolicy(Qt::NoContextMenu); QFont showAllFont; showAllFont.setPointSize(showAllFont.pointSize() - 3); showAllCommentsLinkLabel->setFont(showAllFont); connect(showAllCommentsLinkLabel, SIGNAL(linkActivated(QString)), this, SLOT(requestAllComments())); commentsLayout = new QVBoxLayout(); commentsLayout->setAlignment(Qt::AlignLeft); commentsWidget = new QWidget(); QSizePolicy sizePolicy; sizePolicy.setHeightForWidth(false); sizePolicy.setWidthForHeight(false); //sizePolicy.setHorizontalStretch(1); //sizePolicy.setVerticalStretch(10); sizePolicy.setHorizontalPolicy(QSizePolicy::MinimumExpanding); sizePolicy.setVerticalPolicy(QSizePolicy::MinimumExpanding); commentsWidget->setSizePolicy(sizePolicy); commentsWidget->setLayout(commentsLayout); this->commentsScrollArea = new QScrollArea(); commentsScrollArea->setWidget(commentsWidget); commentsScrollArea->setWidgetResizable(true); // Hide these until setComments() is called, if there are any comments showAllCommentsLinkLabel->hide(); commentsScrollArea->hide(); scrollToBottomTimer = new QTimer(this); scrollToBottomTimer->setSingleShot(true); connect(scrollToBottomTimer, SIGNAL(timeout()), this, SLOT(scrollCommentsToBottom())); this->commentComposer = new Composer(false); // forPublisher = false this->commentComposer->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); this->commentComposer->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); this->commentComposer->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); connect(commentComposer, SIGNAL(editingFinished()), this, SLOT(sendComment())); connect(commentComposer, SIGNAL(editingCancelled()), this, SLOT(setMinimumMode())); // Formatting/Tools button exported from Composer this->toolsButton = commentComposer->getToolsButton(); // Info label about sending status this->statusInfoLabel = new QLabel(); statusInfoLabel->setAlignment(Qt::AlignCenter); statusInfoLabel->setWordWrap(true); showAllFont.setPointSize(showAllFont.pointSize() + 1); statusInfoLabel->setFont(showAllFont); this->commentButton = new QPushButton(QIcon::fromTheme("mail-message-new"), tr("&Comment")); commentButton->setToolTip(tr("You can press Control+Enter to send " "the comment with the keyboard") + ""); connect(commentButton, SIGNAL(clicked()), this, SLOT(sendComment())); this->cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel"), tr("C&ancel")); cancelButton->setToolTip(tr("Press ESC to cancel the comment " "if there is no text") + ""); connect(cancelButton, SIGNAL(clicked()), commentComposer, SLOT(cancelPost())); mainLayout = new QGridLayout(); mainLayout->addWidget(showAllCommentsLinkLabel, 0, 0, 1, 4, Qt::AlignRight | Qt::AlignTop); mainLayout->addWidget(commentsScrollArea, 1, 0, 1, 4); mainLayout->addWidget(commentComposer, 2, 0, 4, 3); mainLayout->addWidget(toolsButton, 2, 3, 1, 1); mainLayout->addWidget(statusInfoLabel, 3, 3, 1, 1, Qt::AlignCenter); mainLayout->addWidget(commentButton, 4, 3, 1, 1); mainLayout->addWidget(cancelButton, 5, 3, 1, 1); this->setLayout(mainLayout); this->setMinimumMode(); qDebug() << "Commenter created"; } CommenterBlock::~CommenterBlock() { qDebug() << "Commenter destroyed"; } void CommenterBlock::clearComments() { foreach (Comment *comment, commentsInBlock) { comment->deleteLater(); } commentsInBlock.clear(); } void CommenterBlock::setComments(QVariantList commentsList) { this->clearComments(); if (commentsList.size() > 0) { this->showAllCommentsLinkLabel->show(); foreach (QVariant commentVariant, commentsList) { Comment *comment = new Comment(this->pController, commentVariant.toMap(), this); this->commentsInBlock.append(comment); connect(comment, SIGNAL(commentQuoteRequested(QString)), this, SLOT(quoteComment(QString))); // Add the comment on top, since the list comes in reverse this->commentsLayout->insertWidget(0, comment); } this->commentsScrollArea->show(); // Move scrollbar to the bottom this->scrollCommentsToBottom(); // First, try // Then, 50 msec later, try again, in case there was no scrollbar before, // and the first try didn't work // Trying immediately first avoids a flicker-like effect sometimes scrollToBottomTimer->start(50); // Trigger resizeEvent //this->commentsWidget->resize(commentsWidget->width() - 1, commentsWidget->height() -1); } } void CommenterBlock::resizeEvent(QResizeEvent *event) { /* int commentWidth = this->commentsScrollArea->width(); if (commentsScrollArea->verticalScrollBar()->isVisible()) { // account for the width of the scrollbar commentWidth -= 32; // FIXME, don't hardcode it } commentsWidget->setMaximumWidth(commentWidth); */ foreach (Comment *comment, this->commentsInBlock) { comment->setCommentContents(); } event->accept(); } /*******************************************************************/ /****************************** SLOTS ******************************/ /*******************************************************************/ void CommenterBlock::setMinimumMode() { this->commentComposer->hide(); this->toolsButton->hide(); statusInfoLabel->clear(); this->statusInfoLabel->hide(); this->commentButton->hide(); this->cancelButton->hide(); // Clear formatting options like bold or italic this->commentComposer->setCurrentCharFormat(QTextCharFormat()); // Unblock updates to timeline, since we're done commenting this->pController->setUpdatesToTimelineBlocked(false); } void CommenterBlock::setFullMode(QString initialText) { this->commentComposer->show(); this->toolsButton->show(); this->statusInfoLabel->show(); this->commentButton->show(); this->cancelButton->show(); this->commentComposer->setFocus(); this->commentComposer->append(initialText); // Block updates to timeline while we're commenting... this->pController->setUpdatesToTimelineBlocked(true); // FIXME: this fails if user clicks "comment" in 2 posts, and then // cancels or sends one of them } void CommenterBlock::quoteComment(QString content) { this->setFullMode(); this->commentComposer->append(content); } void CommenterBlock::requestAllComments() { emit allCommentsRequested(); } /* * Called when commentPosted() is emmited by PumpController, * which means comment posted successfully. * */ void CommenterBlock::onPostingCommentOk() { this->requestAllComments(); // Clear info message this->statusInfoLabel->clear(); // Comment was added successfully, so we can re-enable things this->setEnabled(true); // Erase the text from the comment box... this->commentComposer->erase(); // and since we're done posting the comment, hide this setMinimumMode(); disconnect(pController, SIGNAL(commentPosted()), this, SLOT(onPostingCommentOk())); } /* * Executed when commentPostingFailed() signal is received from PumpController * */ void CommenterBlock::onPostingCommentFailed() { qDebug() << "Posting the comment failed, re-enabling Commenter"; // Alert about the error this->statusInfoLabel->setText(tr("Posting comment failed.\n\nTry again.")); // Re-enable things, so user can try again this->setEnabled(true); this->commentComposer->setFocus(); disconnect(pController, SIGNAL(commentPostingFailed()), this, SLOT(onPostingCommentFailed())); } void CommenterBlock::sendComment() { qDebug() << "Commenter character count:" << commentComposer->textCursor().document()->characterCount(); // If there's some text in the comment, send it if (commentComposer->textCursor().document()->characterCount() > 1) { emit commentSent(commentComposer->toHtml()); connect(pController, SIGNAL(commentPosted()), this, SLOT(onPostingCommentOk())); connect(pController, SIGNAL(commentPostingFailed()), this, SLOT(onPostingCommentFailed())); this->statusInfoLabel->setText(tr("Sending comment...")); this->setDisabled(true); } else { this->statusInfoLabel->setText(tr("Comment is empty.")); qDebug() << "Can't post, comment is empty"; } } /* * Called by the QTimer * */ void CommenterBlock::scrollCommentsToBottom() { commentsScrollArea->verticalScrollBar()->triggerAction(QScrollBar::SliderToMaximum); } dianara-v1.1/src/commenterblock.h000664 000764 000764 00000004321 12260657135 016507 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef COMMENTER_H #define COMMENTER_H #include #include #include #include #include #include #include #include "pumpcontroller.h" #include "composer.h" #include "comment.h" class CommenterBlock : public QWidget { Q_OBJECT public: explicit CommenterBlock(PumpController *pumpController, QWidget *parent = 0); ~CommenterBlock(); void clearComments(); void setComments(QVariantList commentsList); signals: void commentSent(QString comment); void allCommentsRequested(); public slots: void setMinimumMode(); void setFullMode(QString initialText=""); void quoteComment(QString content); void requestAllComments(); void onPostingCommentOk(); void onPostingCommentFailed(); void sendComment(); void scrollCommentsToBottom(); protected: virtual void resizeEvent(QResizeEvent *event); private: QGridLayout *mainLayout; QScrollArea *commentsScrollArea; QWidget *commentsWidget; QVBoxLayout *commentsLayout; QTimer *scrollToBottomTimer; QLabel *showAllCommentsLinkLabel; Composer *commentComposer; QPushButton *toolsButton; QLabel *statusInfoLabel; QPushButton *commentButton; QPushButton *cancelButton; QList commentsInBlock; PumpController *pController; }; #endif // COMMENTER_H dianara-v1.1/src/comment.cpp000664 000764 000764 00000034544 12260657136 015513 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "comment.h" Comment::Comment(PumpController *pumpController, QVariantMap commentMap, QWidget *parent) : QFrame(parent) { this->pController = pumpController; QSizePolicy sizePolicy; sizePolicy.setHeightForWidth(false); sizePolicy.setWidthForHeight(false); //sizePolicy.setHorizontalStretch(1); //sizePolicy.setVerticalStretch(6); sizePolicy.setHorizontalPolicy(QSizePolicy::MinimumExpanding); sizePolicy.setVerticalPolicy(QSizePolicy::MinimumExpanding); this->setSizePolicy(sizePolicy); this->commentID = commentMap.value("id").toString(); this->objectType = commentMap.value("objectType").toString(); QVariantMap commentAuthorMap = commentMap.value("author").toMap(); QString commentAuthorID = commentAuthorMap.value("id").toString(); commentAuthorID.remove(0, 5); // remove "acct:" at the beginning if (commentAuthorID == pController->currentUserId()) { commentIsOwn = true; // Comment is ours! // Different frame style depending on whether the comment is ours or not this->setFrameStyle(QFrame::Sunken | QFrame::StyledPanel); } else { commentIsOwn = false; this->setFrameStyle(QFrame::Raised | QFrame::StyledPanel); } // Avatar pixmap avatarLabel = new QLabel(); avatarLabel->setToolTip(commentAuthorID); this->commentAuthorAvatarUrl = commentAuthorMap.value("image").toMap().value("url").toString(); QString commentAuthorAvatarFile = MiscHelpers::getCachedAvatarFilename(commentAuthorAvatarUrl); if (QFile::exists(commentAuthorAvatarFile)) { avatarLabel->setPixmap(QPixmap(commentAuthorAvatarFile) .scaledToWidth(32, Qt::SmoothTransformation)); } else { // Set placeholder avatarLabel->setPixmap(QIcon::fromTheme("user-identity").pixmap(32, 32)); // and download avatar for next time connect(pController, SIGNAL(avatarStored(QString,QString)), this, SLOT(redrawAvatar(QString,QString))); this->pController->getAvatar(commentAuthorAvatarUrl); } QFont commentsFont; commentsFont.setPointSize(commentsFont.pointSize() - 1); // 1 point less than default // Name, with ID as tooltip commentsFont.setBold(true); fullNameLabel = new QLabel(commentAuthorMap.value("displayName").toString()); fullNameLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); fullNameLabel->setFont(commentsFont); fullNameLabel->setToolTip(commentAuthorID); // Timestamp commentsFont.setBold(false); commentsFont.setItalic(true); QString timestamp = commentMap.value("published").toString(); QString commentExactTime = Timestamp::localTimeDate(timestamp); QString commentFuzzyTime = Timestamp::fuzzyTime(timestamp); timestampLabel = new QLabel(commentFuzzyTime); timestampLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); timestampLabel->setFont(commentsFont); timestampLabel->setToolTip(commentExactTime); // Like and Delete "buttons" commentsFont.setBold(true); commentsFont.setItalic(false); likeLabel = new QLabel("*like*"); likeLabel->setContextMenuPolicy(Qt::NoContextMenu); likeLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); likeLabel->setFont(commentsFont); likeLabel->setToolTip(tr("Like or unlike this comment")); connect(likeLabel, SIGNAL(linkActivated(QString)), this, SLOT(likeComment(QString))); commentIsLiked = commentMap.value("liked").toBool(); this->fixLikeLabelText(); quoteLabel = new QLabel("" + tr("Quote", "This is a verb, infinitive") + ""); quoteLabel->setContextMenuPolicy(Qt::NoContextMenu); quoteLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); quoteLabel->setFont(commentsFont); quoteLabel->setToolTip(tr("Reply quoting this comment")); connect(quoteLabel, SIGNAL(linkActivated(QString)), this, SLOT(quoteComment())); deleteLabel = new QLabel("" + tr("Delete") + ""); deleteLabel->setContextMenuPolicy(Qt::NoContextMenu); deleteLabel->setAlignment(Qt::AlignTop | Qt::AlignRight); deleteLabel->setFont(commentsFont); deleteLabel->setToolTip(tr("Delete this comment")); connect(deleteLabel, SIGNAL(linkActivated(QString)), this, SLOT(deleteComment())); // The likes count likesCountLabel = new QLabel(); likesCountLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); commentsFont.setBold(false); likesCountLabel->setFont(commentsFont); this->setLikesCount(commentMap.value("likes").toMap().value("totalItems").toInt(), commentMap.value("likes").toMap().value("items").toList()); // The comment itself this->commentOriginalText = commentMap.value("content").toString(); contentLabel = new QLabel(); contentLabel->setAlignment(Qt::AlignLeft); contentLabel->setFont(commentsFont); contentLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); contentLabel->setWordWrap(true); contentLabel->setOpenExternalLinks(true); contentLabel->setTextFormat(Qt::RichText); contentLabel->setSizePolicy(sizePolicy); connect(contentLabel, SIGNAL(linkHovered(QString)), this, SLOT(showUrlInfo(QString))); QStringList commentImageList = MiscHelpers::htmlWithReplacedImages(commentOriginalText, 64); // Arbitrary width commentImageList.removeFirst(); // First is the HTML with images replaced // If the image list is not empty, get them if (!commentImageList.isEmpty()) { qDebug() << "Comment has" << commentImageList.size() << "images included..."; foreach (QString imageUrl, commentImageList) { this->enqueueImageForDownload(imageUrl); } } urlInfoLabel = new QLabel(); urlInfoLabel->setFont(commentsFont); urlInfoLabel->setAutoFillBackground(true); urlInfoLabel->setForegroundRole(QPalette::ToolTipText); urlInfoLabel->setBackgroundRole(QPalette::ToolTipBase); urlInfoLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken); urlInfoLabel->hide(); // Hidden initially contentLabelLayout = new QHBoxLayout(); contentLabelLayout->addWidget(urlInfoLabel, 0, Qt::AlignRight | Qt::AlignBottom); contentLabel->setLayout(contentLabelLayout); leftLayout = new QVBoxLayout(); leftLayout->setAlignment(Qt::AlignLeft); leftLayout->addWidget(avatarLabel, 0, Qt::AlignLeft | Qt::AlignTop); leftLayout->addWidget(likesCountLabel, 1, Qt::AlignHCenter | Qt::AlignTop); rightTopLayout = new QHBoxLayout(); rightTopLayout->addWidget(fullNameLabel, 0, Qt::AlignLeft); rightTopLayout->addWidget(timestampLabel, 0, Qt::AlignLeft); rightTopLayout->addWidget(likeLabel, 0, Qt::AlignLeft); rightTopLayout->addWidget(quoteLabel, 0, Qt::AlignLeft); rightTopLayout->addStretch(1); if (commentIsOwn) { rightTopLayout->addWidget(deleteLabel, 0, Qt::AlignRight); } rightLayout = new QVBoxLayout(); rightLayout->addLayout(rightTopLayout, 0); rightLayout->addSpacing(4); // 4px vertical space separation rightLayout->addWidget(contentLabel, 10); mainLayout = new QHBoxLayout(); mainLayout->setAlignment(Qt::AlignTop); mainLayout->addLayout(leftLayout); mainLayout->addLayout(rightLayout); this->setLayout(mainLayout); setCommentContents(); qDebug() << "Comment created" << this->commentID; } Comment::~Comment() { qDebug() << "Comment destroyed" << this->commentID; } void Comment::fixLikeLabelText() { if (commentIsLiked) { this->likeLabel->setText("" + tr("Unlike") +""); } else { this->likeLabel->setText("" + tr("Like") +""); } } void Comment::setLikesCount(int count, QVariantList namesVariantList) { if (count > 0) { QString likesString; foreach (QVariant likesMap, namesVariantList) { likesString.append(likesMap.toMap().value("displayName").toString() + ", "); } likesString.remove(-2, 2); // remove last comma+space: ", " if (count > 1) { likesString = tr("%1 like this comment", "Plural: %1=list of people like John, Jane, Smith").arg(likesString); } else { likesString = tr("%1 likes this comment", "Singular: %1=name of just 1 person").arg(likesString); } likesCountLabel->setText(QString::fromUtf8("\342\231\245") // heart symbol + QString(" %1").arg(count)); // set tooltip as HTML, so it gets wordwrapped likesCountLabel->setToolTip(likesString + ""); } else { likesCountLabel->clear(); likesCountLabel->setToolTip(""); } } /* * Set the contents of the comment, parsing images, etc. * */ void Comment::setCommentContents() { int imageWidth = this->contentLabel->width() - 20; // Kinda TMP QStringList commentImageList = MiscHelpers::htmlWithReplacedImages(commentOriginalText, imageWidth); QString commentContents = commentImageList.takeAt(0); // Comment's HTML with images replaced this->contentLabel->setText(commentContents); setCommentHeight(); } void Comment::setCommentHeight() { // int height = this->contentLabel->height(); /* if (height > 200) { height = 200; } */ /* this->contentLabel->setMinimumHeight(height); this->contentLabel->setMaximumHeight(height); */ } void Comment::enqueueImageForDownload(QString url) { if (QFile::exists(MiscHelpers::getCachedImageFilename(url)) || pendingImagesList.contains(url)) { qDebug() << "Comment::enqueueImageForDownload(), " "Using cached post image, or requested image is pending download..."; } else { qDebug() << "Comment::enqueueImageForDownload(), " "post image not cached, downloading" << url; connect(pController, SIGNAL(imageStored(QString)), this, SLOT(redrawImages(QString))); this->pendingImagesList.append(url); pController->getImage(url); } } /* * Ensure urlInfoLabel is hidden when the mouse leaves the comment * */ void Comment::leaveEvent(QEvent *event) { this->urlInfoLabel->clear(); this->urlInfoLabel->hide(); event->accept(); } /****************************************************************************/ /******************************** SLOTS *************************************/ /****************************************************************************/ void Comment::likeComment(QString clickedLink) { if (clickedLink == "like://") { commentIsLiked = true; } else // unlike:// { commentIsLiked = false; } this->pController->likePost(this->commentID, this->objectType, this->commentIsLiked); this->fixLikeLabelText(); } /* * Take the contents of a comment and put them, as a quote block * in the comment composer * * FIXME: It'd be nice to only quote the selected text, * unless nothing is selected, then use full comment, but clicking * the "quote" label removes the selection */ void Comment::quoteComment() { QString quotedComment = MiscHelpers::quotedText(this->fullNameLabel->text(), this->contentLabel->text()); emit commentQuoteRequested(quotedComment); } void Comment::deleteComment() { int confirmation = QMessageBox::question(this, tr("WARNING: Delete comment?"), tr("Are you sure you want to delete this comment?"), tr("&Yes, delete it"), tr("&No"), "", 1, 1); if (confirmation == 0) { qDebug() << "Deleting comment" << this->commentID; this->pController->deletePost(this->commentID, this->objectType); this->setDisabled(true); // disable... maybe hide? } else { qDebug() << "Confirmation canceled, not deleting the comment"; } } /* * Show the URL of a link hovered in a comment * */ void Comment::showUrlInfo(QString url) { if (url.isEmpty()) { this->urlInfoLabel->clear(); this->urlInfoLabel->hide(); } else { qDebug() << "Link hovered in comment:" << url; this->urlInfoLabel->setText(url); this->urlInfoLabel->show(); } } /* * Redraw comment author's avatar after receiving it * */void Comment::redrawAvatar(QString avatarUrl, QString avatarFilename) { if (avatarUrl == this->commentAuthorAvatarUrl) { avatarLabel->setPixmap(QPixmap(avatarFilename) .scaledToWidth(32, Qt::SmoothTransformation)); disconnect(pController, SIGNAL(avatarStored(QString,QString)), this, SLOT(redrawAvatar(QString,QString))); } } /* * Redraw comment contents after receiving downloaded images * */ void Comment::redrawImages(QString imageUrl) { if (pendingImagesList.contains(imageUrl)) { this->pendingImagesList.removeAll(imageUrl); if (pendingImagesList.isEmpty()) // If there are no more, disconnect { disconnect(pController, SIGNAL(imageStored(QString)), this, SLOT(redrawImages(QString))); } setCommentContents(); } } dianara-v1.1/src/imageviewer.h000664 000764 000764 00000003223 12260657137 016011 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef IMAGEVIEWER_H #define IMAGEVIEWER_H #include #include #include #include #include #include #include #include #include #include #include "mischelpers.h" class ImageViewer : public QLabel { Q_OBJECT public: explicit ImageViewer(QString fileURI, QString title, QWidget *parent = 0); ~ImageViewer(); void createContextMenu(); signals: public slots: void saveImage(); protected: virtual void closeEvent(QCloseEvent *event); virtual void keyPressEvent(QKeyEvent *event); virtual void contextMenuEvent(QContextMenuEvent *event); private: QString originalFileURI; QMenu *contextMenu; }; #endif // IMAGEVIEWER_H dianara-v1.1/src/imageviewer.cpp000664 000764 000764 00000010520 12260657136 016341 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "imageviewer.h" ImageViewer::ImageViewer(QString fileURI, QString title, QWidget *parent) : QLabel(parent) { this->setMinimumSize(250, 100); if (title.isEmpty()) { title = "--"; } fileURI.remove(0, 7); // remove "image:/" from filename URI this->originalFileURI = fileURI; QDesktopWidget desktopWidget; QPixmap pixmap(originalFileURI); QString resolution = QString("%1x%2").arg(pixmap.width()).arg(pixmap.height()); this->setWindowTitle("Dianara - " + tr("Image") + ": " + title + " (" + resolution + ", " + MiscHelpers::fileSizeString(originalFileURI) + ")"); this->setWindowIcon(QIcon::fromTheme("folder-image")); this->setAlignment(Qt::AlignCenter); this->setToolTip(tr("ESC to close, secondary-click for options")); // Resize pixmap according to desktop (screen) size if (pixmap.height() > desktopWidget.height()) { pixmap = pixmap.scaledToHeight(desktopWidget.height() - 100, Qt::SmoothTransformation); } if (pixmap.width() > desktopWidget.width()) { pixmap = pixmap.scaledToWidth(desktopWidget.width() - 100, Qt::SmoothTransformation); } this->setPixmap(pixmap); this->resize(pixmap.size()); this->createContextMenu(); qDebug() << "ImageViewer created"; } ImageViewer::~ImageViewer() { qDebug() << "ImageViewer destroyed"; } void ImageViewer::createContextMenu() { contextMenu = new QMenu("imaveViewerMenu"); contextMenu->addAction(QIcon::fromTheme("document-save-as"), tr("Save Image..."), this, SLOT(saveImage())); contextMenu->addAction(QIcon::fromTheme("window-close"), tr("Close Viewer"), this, SLOT(close())); } void ImageViewer::closeEvent(QCloseEvent *event) { qDebug() << "ImageViewer::closeEvent()"; event->accept(); this->deleteLater(); } void ImageViewer::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { this->close(); } else { event->accept(); } } void ImageViewer::contextMenuEvent(QContextMenuEvent *event) { this->contextMenu->exec(event->globalPos()); } /****************************************************************************/ /************************************ SLOTS *********************************/ /****************************************************************************/ void ImageViewer::saveImage() { bool savedCorrectly; QString filename; filename = QFileDialog::getSaveFileName(this, tr("Save Image As..."), QDir::homePath(), tr("Image files") + "(*.jpg *.png);;" + tr("All files") + " (*)"); if (!filename.isEmpty()) { // Save pixmap from original file savedCorrectly = QPixmap(this->originalFileURI).save(filename); if (!savedCorrectly) { QMessageBox::warning(this, tr("Error saving image"), tr("There was a problem while saving %1.\n\n" "Filename should end in .jpg " "or .png extensions.").arg(filename)); } } } dianara-v1.1/src/minorfeed.cpp000664 000764 000764 00000022553 12260657137 016017 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "minorfeed.h" MinorFeed::MinorFeed(PumpController *pumpController, QWidget *parent) : QFrame(parent) { this->pController = pumpController; this->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); // Layout for the items itemsLayout = new QVBoxLayout(); itemsLayout->setAlignment(Qt::AlignTop); // Button to get more items (older stuff) getMoreButton = new QPushButton(QIcon::fromTheme("list-add"), tr("Get More")); getMoreButton->setFlat(true); getMoreButton->setToolTip(tr("Get older activities")); connect(getMoreButton, SIGNAL(clicked()), this, SLOT(getMoreActivities())); // Set disabled initially; will be enabled when contents are set getMoreButton->setDisabled(true); // Main layout mainLayout = new QVBoxLayout(); mainLayout->setAlignment(Qt::AlignTop); mainLayout->addLayout(itemsLayout); mainLayout->addWidget(getMoreButton); this->setLayout(mainLayout); // Demo activity, stating that there's nothing to show QVariantMap demoGenerator; demoGenerator.insert("displayName", "Dianara"); QVariantMap demoActivityMap; demoActivityMap.insert("published", QDateTime::currentDateTimeUtc() .toString(Qt::ISODate)); demoActivityMap.insert("generator", demoGenerator); demoActivityMap.insert("content", tr("There are no activities to show yet.")); ASActivity *demoActivity = new ASActivity(demoActivityMap, this); MinorFeedItem *demoFeedItem = new MinorFeedItem(demoActivity, "", this->pController); this->itemsLayout->addWidget(demoFeedItem); this->itemsInFeed.append(demoFeedItem); this->feedOffset = 0; this->newItemsCount = 0; QSettings settings; settings.beginGroup("MinorFeedState"); this->previousNewestActivityId = settings.value("previousNewestItemId", "").toString(); settings.endGroup(); qDebug() << "MinorFeed created"; } MinorFeed::~MinorFeed() { QSettings settings; settings.beginGroup("MinorFeedState"); settings.setValue("previousNewestItemId", this->previousNewestActivityId); settings.endGroup(); qDebug() << "MinorFeed destroyed"; } void MinorFeed::clearContents() { qDebug() << "MinorFeed::clearContents()"; foreach (MinorFeedItem *feedItem, itemsInFeed) { this->itemsLayout->removeWidget(feedItem); delete feedItem; } itemsInFeed.clear(); } void MinorFeed::markAllAsRead() { foreach (MinorFeedItem *feedItem, itemsInFeed) { feedItem->setItemAsNew(false); } } /*******************************************************************************/ /********************************** SLOTS **************************************/ /*******************************************************************************/ /* * Get the latest activities * */ void MinorFeed::updateFeed() { this->feedOffset = 0; this->pController->getMinorFeed(); } /* * Get additional older activities * */ void MinorFeed::getMoreActivities() { this->feedOffset += 50; this->pController->getMinorFeed(feedOffset); } void MinorFeed::setFeedContents(QVariantList activitiesList) { this->getMoreButton->setDisabled(true); if (feedOffset == 0) // Not asking for more, but a reload { this->clearContents(); this->newItemsCount = 0; } // Process event queue, so GUI can get updated qApp->processEvents(); // This also acts as "scroll to top", incidentally QStringList filteredContent; QStringList filteredAuthor; QStringList filteredGenerator; // Define filters foreach (QVariant filter, pController->getCurrentFilters()) { int filterType = filter.toMap().value("type").toInt(); QString filterText = filter.toMap().value("text").toString(); switch (filterType) { case 0: // content filteredContent.append(filterText); break; case 1: // author filteredAuthor.append(filterText); break; case 2: // application (generator) filteredGenerator.append(filterText); break; default: break; } } QString activityActorAvatarUrl; QString activityActorAvatarFilename; QString activityContent; QString activityAuthor; QString activityGenerator; bool filtered; bool allNewItemsCounted = false; QString newestActivityId; // To store the activity ID for the newest item in the feed // so we can know how many new items we receive next time foreach (QVariant activityVariant, activitiesList) { ASActivity *activity = new ASActivity(activityVariant.toMap(), this); ////////////////////////////////////////////////////////////// Filtering Start filtered = false; // Innocent until proven guilty! activityContent = activity->getContent(); foreach (QString content, filteredContent) { if (activityContent.contains(content)) { qDebug() << "Filtering item because of Content:" << content; filtered = true; } } activityAuthor = activity->getAuthorName(); foreach (QString author, filteredAuthor) { if (activityAuthor.contains(author)) { qDebug() << "Filtering item because of Author:" << author; filtered = true; } } activityGenerator = activity->getGenerator(); foreach (QString generator, filteredGenerator) { if (activityGenerator.contains(generator)) { qDebug() << "Filtering item because of Application (generator):" << generator; filtered = true; } } ////////////////////////////////////////////////////////////// Filtering End bool itemIsNew; // If there is no reason to filter out the item, add it to the feed if (!filtered) { itemIsNew = false; // Determine which activities are new if (newestActivityId.isEmpty()) // Only first time, for newest item { if (this->feedOffset == 0) { newestActivityId = activity->getId(); } else { newestActivityId = this->previousNewestActivityId; allNewItemsCounted = true; } } if (!allNewItemsCounted) { if (activity->getId() == this->previousNewestActivityId) { allNewItemsCounted = true; } else { // If activity is not ours, add it to the count if (activity->getAuthorId() != pController->currentUserId()) { ++newItemsCount; itemIsNew = true; } } } activityActorAvatarUrl = activity->getAuthorAvatar(); activityActorAvatarFilename = MiscHelpers::getCachedAvatarFilename(activityActorAvatarUrl); if (!QFile::exists(activityActorAvatarFilename)) { this->pController->getAvatar(activityActorAvatarUrl); } MinorFeedItem *newFeedItem = new MinorFeedItem(activity, activityActorAvatarFilename, this->pController); if (itemIsNew) { newFeedItem->setItemAsNew(true); connect(newFeedItem, SIGNAL(itemRead()), this, SLOT(decreaseNewItemsCount())); } this->itemsLayout->addWidget(newFeedItem); this->itemsInFeed.append(newFeedItem); } else { // Since the item is not added to the feed, we need to delete the activity delete activity; } } this->previousNewestActivityId = newestActivityId; emit newItemsCountChanged(newItemsCount); qDebug() << "Meanwhile feed updated; New items:" << newItemsCount; this->getMoreButton->setEnabled(true); } void MinorFeed::decreaseNewItemsCount() { --newItemsCount; emit newItemsCountChanged(newItemsCount); } dianara-v1.1/src/accountdialog.cpp000664 000764 000764 00000024135 12260657141 016654 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "accountdialog.h" AccountDialog::AccountDialog(PumpController *pumpController, QWidget *parent) : QWidget(parent) { this->setWindowTitle("Dianara - " + tr("Account Configuration")); this->setWindowIcon(QIcon::fromTheme("dialog-password")); this->setWindowFlags(Qt::Window); this->setWindowModality(Qt::ApplicationModal); this->setMinimumSize(640, 320); this->pController = pumpController; connect(pController, SIGNAL(openingAuthorizeURL(QUrl)), this, SLOT(showAuthorizationURL(QUrl))); connect(pController, SIGNAL(authorizationStatusChanged(bool)), this, SLOT(showAuthorizationStatus(bool))); QFont helpFont; helpFont.setPointSize(helpFont.pointSize() - 1); this->helpMessage1Label = new QLabel(tr("First, enter your Webfinger ID, your pump.io address.") + "\n" + tr("Your address looks like username@pumpserver.org, and " "you can find it in your profile, in the web interface.") + "\n" + tr("If your profile is at https://pump.example/yourname, then " "your address is yourname@pump.example")); helpMessage1Label->setWordWrap(true); helpMessage1Label->setFont(helpFont); userIDIconLabel = new QLabel(); userIDIconLabel->setPixmap(QIcon::fromTheme("preferences-desktop-user").pixmap(32,32)); userIDLabel = new QLabel(tr("Your Pump.io address:")); userIDLineEdit = new QLineEdit(); userIDLineEdit->setPlaceholderText(tr("Webfinger ID, like username@pumpserver.org")); userIDLineEdit->setToolTip(tr("Your address, as username@server")); getVerifierButton = new QPushButton(QIcon::fromTheme("object-unlocked"), tr("Get &Verifier Code")); getVerifierButton->setToolTip(tr("After clicking this button, a web browser will " "open, requesting authorization for Dianara")); connect(getVerifierButton, SIGNAL(clicked()), this, SLOT(askForToken())); this->helpMessage2Label = new QLabel(tr("Once you have authorized Dianara from your Pump server " "web interface, you'll receive a code called VERIFIER.\n" "Copy it and paste it into the field below.", // Comment for the translators "Don't translate the VERIFIER word!")); helpMessage2Label->setWordWrap(true); helpMessage2Label->setFont(helpFont); verifierIconLabel = new QLabel(); verifierIconLabel->setPixmap(QIcon::fromTheme("dialog-password").pixmap(32,32)); verifierLabel = new QLabel(tr("Verifier code:")); verifierLineEdit = new QLineEdit(); verifierLineEdit->setPlaceholderText(tr("Enter the verifier code provided by your Pump server here")); verifierLineEdit->setToolTip(tr("Paste the verifier here")); authorizeApplicationButton = new QPushButton(QIcon::fromTheme("security-high"), tr("&Authorize Application")); connect(authorizeApplicationButton, SIGNAL(clicked()), this, SLOT(setVerifierCode())); // To notify invalid ID or empty verifier code // Cleared when typing in any of the two fields errorsLabel = new QLabel(""); connect(userIDLineEdit, SIGNAL(textChanged(QString)), errorsLabel, SLOT(clear())); connect(verifierLineEdit, SIGNAL(textChanged(QString)), errorsLabel, SLOT(clear())); QFont authorizationStatusFont; authorizationStatusFont.setPointSize(authorizationStatusFont.pointSize() + 2); authorizationStatusFont.setBold(true); authorizationStatusLabel = new QLabel(); authorizationStatusLabel->setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); authorizationStatusLabel->setFont(authorizationStatusFont); saveButton = new QPushButton(QIcon::fromTheme("document-save"), tr("&Save Details")); connect(saveButton, SIGNAL(clicked()), this, SLOT(saveDetails())); cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel"), tr("&Cancel")); connect(cancelButton, SIGNAL(clicked()), this, SLOT(hide())); // Layout this->mainLayout = new QVBoxLayout(); mainLayout->addWidget(helpMessage1Label, 3); mainLayout->addStretch(2); this->idLayout = new QHBoxLayout(); idLayout->addWidget(userIDIconLabel); idLayout->addSpacing(4); idLayout->addWidget(userIDLabel); idLayout->addWidget(userIDLineEdit); idLayout->addWidget(getVerifierButton); mainLayout->addLayout(idLayout, 3); mainLayout->addStretch(3); mainLayout->addSpacing(8); mainLayout->addWidget(helpMessage2Label, 3); mainLayout->addStretch(2); this->verifierLayout = new QHBoxLayout(); verifierLayout->addWidget(verifierIconLabel); verifierLayout->addSpacing(4); verifierLayout->addWidget(verifierLabel); verifierLayout->addWidget(verifierLineEdit); verifierLayout->addWidget(authorizeApplicationButton); mainLayout->addLayout(verifierLayout, 3); mainLayout->addWidget(errorsLabel, 1, Qt::AlignCenter); mainLayout->addWidget(authorizationStatusLabel, 1, Qt::AlignCenter); mainLayout->addStretch(2); mainLayout->addSpacing(8); this->buttonsLayout = new QHBoxLayout(); buttonsLayout->setAlignment(Qt::AlignRight); buttonsLayout->addWidget(saveButton); buttonsLayout->addWidget(cancelButton); mainLayout->addLayout(buttonsLayout, 1); this->setLayout(mainLayout); QSettings settings; // Load saved User ID userIDLineEdit->setText(settings.value("userID", "").toString()); this->showAuthorizationStatus(settings.value("isApplicationAuthorized", false).toBool()); // Disable verifier input field and button initially // They will be used after requesting a token verifierLineEdit->setDisabled(true); authorizeApplicationButton->setDisabled(true); qDebug() << "Account dialog created"; } AccountDialog::~AccountDialog() { qDebug() << "Account dialog destroyed"; } ////////////////////////////////////////////////////////////////////// ///////////////////////////////// SLOTS ////////////////////////////// ////////////////////////////////////////////////////////////////////// void AccountDialog::askForToken() { // Don't fail if there're spaces before or after the ID, they're only human ;) userIDLineEdit->setText(userIDLineEdit->text().trimmed()); // Match username@server.tld AND check for only 1 @ sign if (QRegExp("\\S+@\\S+\\.\\S+").exactMatch(this->userIDLineEdit->text()) && userIDLineEdit->text().count("@") == 1) { // Clear verifier field and re-enable it and the button this->verifierLineEdit->clear(); this->verifierLineEdit->setEnabled(true); this->authorizeApplicationButton->setEnabled(true); // Show message about the web browser that will be started this->errorsLabel->setText("[ " + tr("A web browser will start now, where you can get the verifier code") + " ]"); this->pController->setNewUserId(userIDLineEdit->text()); this->pController->getToken(); } else // userID does not match user@hostname.domain { this->errorsLabel->setText("[ " + tr("Your Pump address is invalid") + " ]"); qDebug() << "userID is invalid!"; } } void AccountDialog::setVerifierCode() { qDebug() << "AccountDialog::setVerifierCode()" << this->verifierLineEdit->text(); if (!this->verifierLineEdit->text().isEmpty()) { this->pController->authorizeApplication(this->verifierLineEdit->text()); } else { this->errorsLabel->setText("[ " + tr("Verifier code is empty") + " ]"); } } void AccountDialog::showAuthorizationStatus(bool authorized) { if (authorized) { this->authorizationStatusLabel->setText(QString::fromUtf8("\342\234\224 ") // Check mark + tr("Dianara is authorized to access your data")); } else { this->authorizationStatusLabel->clear(); } } /* * Show the authorization URL in a label, * in case the browser doesn't open automatically * */ void AccountDialog::showAuthorizationURL(QUrl url) { QString message = tr("If the browser doesn't open automatically, " "copy this address manually") + ":\n" + url.toString() + "\n"; this->authorizationStatusLabel->setText(message); } /* * Save the new userID and inform other parts of the program about it * */ void AccountDialog::saveDetails() { QString newUserID = this->userIDLineEdit->text().trimmed(); if (newUserID.isEmpty() || !newUserID.contains("@")) { return; // If no user ID, or with no "@", ignore } QSettings settings; settings.setValue("userID", newUserID); emit userIDChanged(userIDLineEdit->text()); this->errorsLabel->clear(); // Clear previous error messages, if any this->hide(); // close() would end program if main window was hidden } dianara-v1.1/src/profileeditor.cpp000664 000764 000764 00000020303 12260657141 016700 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "profileeditor.h" ProfileEditor::ProfileEditor(PumpController *pumpController, QWidget *parent) : QWidget(parent) { this->setWindowTitle("Dianara - " + tr("Profile Editor")); this->setWindowIcon(QIcon::fromTheme("user-properties")); this->setWindowFlags(Qt::Window); this->setWindowModality(Qt::NonModal); this->setMinimumSize(500, 350); this->pController = pumpController; QFont infoFont; infoFont.setPointSize(infoFont.pointSize() - 1); webfingerLabel = new QLabel(); webfingerLabel->setFont(infoFont); webfingerLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); webfingerLabel->setToolTip(tr("This is your Pump address")); emailLabel = new QLabel(); emailLabel->setFont(infoFont); emailLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); emailLabel->setToolTip(tr("This is the e-mail address associated with your account, " "for things such as notifications and password recovery") + ""); // Make the tooltip HTML so it gets wordwrap avatarLabel = new QLabel(); avatarLabel->setPixmap(QIcon::fromTheme("user-properties").pixmap(64, 64)); changeAvatarButton = new QPushButton(QIcon::fromTheme("folder-image-people"), tr("Change &Avatar...")); connect(changeAvatarButton, SIGNAL(clicked()), this, SLOT(findAvatarFile())); this->avatarChanged = false; fullNameLineEdit = new QLineEdit(); fullNameLineEdit->setToolTip(tr("This is your visible name")); hometownLineEdit = new QLineEdit(); bioTextEdit = new QTextEdit(); bioTextEdit->setTabChangesFocus(true); saveButton = new QPushButton(QIcon::fromTheme("document-save"), tr("&Save Profile")); connect(saveButton, SIGNAL(clicked()), this, SLOT(saveProfile())); cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel"), tr("&Cancel")); connect(cancelButton, SIGNAL(clicked()), this, SLOT(hide())); // ESC to cancel, too cancelAction = new QAction(this); cancelAction->setShortcut(QKeySequence(Qt::Key_Escape)); connect(cancelAction, SIGNAL(triggered()), this, SLOT(hide())); this->addAction(cancelAction); this->avatarLayout = new QHBoxLayout(); avatarLayout->addWidget(avatarLabel); avatarLayout->addWidget(changeAvatarButton); this->bottomLayout = new QHBoxLayout(); bottomLayout->setAlignment(Qt::AlignRight); bottomLayout->addWidget(saveButton); bottomLayout->addWidget(cancelButton); this->mainLayout = new QFormLayout(); mainLayout->setVerticalSpacing(8); mainLayout->addRow(tr("Webfinger ID"), webfingerLabel); mainLayout->addRow(tr("E-mail"), emailLabel); mainLayout->addRow(tr("Avatar"), avatarLayout); mainLayout->addRow(tr("Full &Name"), fullNameLineEdit); mainLayout->addRow(tr("&Hometown"), hometownLineEdit); mainLayout->addRow(tr("&Bio"), bioTextEdit); mainLayout->addRow(bottomLayout); this->setLayout(mainLayout); qDebug() << "ProfileEditor created"; } ProfileEditor::~ProfileEditor() { qDebug() << "ProfileEditor destroyed"; } /* * Fill the fields from received info * */ void ProfileEditor::setProfileData(QString avatarUrl, QString fullName, QString hometown, QString bio, QString eMail) { this->webfingerLabel->setText(pController->currentUserId()); if (eMail.isEmpty()) { this->emailLabel->setText("<" + tr("Not set", "In reference to the e-mail " "not being set for the account") + ">"); } else { this->emailLabel->setText(eMail); } this->currentAvatarURL = avatarUrl; QString avatarFilename = MiscHelpers::getCachedAvatarFilename(this->currentAvatarURL); if (QFile::exists(avatarFilename)) { this->avatarLabel->setPixmap(QPixmap(avatarFilename).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } this->fullNameLineEdit->setText(fullName); this->hometownLineEdit->setText(hometown); this->bioTextEdit->setText(bio); } /****************************************************************************/ /******************************** SLOTS *************************************/ /****************************************************************************/ void ProfileEditor::findAvatarFile() { newAvatarFilename = QFileDialog::getOpenFileName(this, tr("Select avatar image"), QDir::homePath(), tr("Image files") + "(*.png *.jpg *.jpeg *.gif);;" + tr("All files") + " (*)"); if (!newAvatarFilename.isEmpty()) { qDebug() << "Selected" << newAvatarFilename << "as new avatar for upload"; // FIXME: in the future, check file size and image size // and scale the pixmap to something sane before uploading. QPixmap avatarPixmap = QPixmap(newAvatarFilename); this->newAvatarContentType = MiscHelpers::getImageContentType(newAvatarFilename); if (!avatarPixmap.isNull() && !newAvatarContentType.isEmpty()) { this->avatarLabel->setPixmap(QPixmap(newAvatarFilename).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); this->avatarChanged = true; } else { QMessageBox::warning(this, tr("Invalid image"), tr("The selected image is not valid.")); qDebug() << "Invalid avatar file selected"; } } } void ProfileEditor::saveProfile() { if (avatarChanged) { connect(pController, SIGNAL(avatarUploaded(QString)), this, SLOT(sendProfileData(QString))); this->pController->uploadFile(this->newAvatarFilename, this->newAvatarContentType, PumpController::UploadAvatarRequest); } else { this->sendProfileData(); // without a new image ID } } void ProfileEditor::sendProfileData(QString newImageUrl) { if (avatarChanged) { disconnect(pController, SIGNAL(avatarUploaded(QString)), this, SLOT(sendProfileData(QString))); this->avatarChanged = false; // For next time the dialog is shown } this->pController->updateUserProfile(newImageUrl, this->fullNameLineEdit->text().trimmed(), this->hometownLineEdit->text().trimmed(), this->bioTextEdit->toPlainText().trimmed()); this->hide(); // close() would end the program if main window is hidden } dianara-v1.1/src/asobject.h000664 000764 000764 00000005272 12260657140 015277 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef ASOBJECT_H #define ASOBJECT_H #include #include #include class ASObject : public QObject { Q_OBJECT public: explicit ASObject(QVariantMap objectMap, QObject *parent = 0); ~ASObject(); QString getId(); QString getType(); QString getUrl(); QString getCreatedAt(); QString getUpdatedAt(); QString getLocation(); QString getDeletedTime(); QString isLiked(); QVariantMap getAuthorMap(); QString getAuthorName(); QString getAuthorId(); QString getAuthorAvatar(); QString getAuthorUrl(); QString getAuthorHometown(); QString getAuthorBio(); QString getTitle(); QString getContent(); QString getImageUrl(); QString getLikesCount(); QString getCommentsCount(); QString getSharesCount(); QVariantList getLastLikesList(); QVariantList getLastCommentsList(); QVariantList getLastSharesList(); QString getLikesUrl(); QString getCommentsUrl(); QString getSharesUrl(); QVariantMap getOriginalObject(); QVariantMap getInReplyTo(); signals: public slots: private: QString id; QString type; QString url; QString createdAt; QString updatedAt; QString location; QString deleted; QString liked; QVariantMap authorMap; QString authorName; QString authorId; QString authorAvatar; QString authorUrl; QString authorHometown; QString authorBio; QString title; QString content; QString imageUrl; QString likesCount; QString commentsCount; QString sharesCount; QVariantList lastLikesList; QVariantList lastCommentsList; QVariantList lastSharesList; QString likesUrl; QString commentsUrl; QString sharesUrl; QVariantMap originalObjectMap; QVariantMap inReplyToMap; }; #endif // ASOBJECT_H dianara-v1.1/src/accountdialog.h000664 000764 000764 00000004157 12260657135 016326 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef ACCOUNT_H #define ACCOUNT_H #include #include #include #include #include #include #include #include #include #include #include "pumpcontroller.h" class AccountDialog : public QWidget { Q_OBJECT public: AccountDialog(PumpController *pumpController, QWidget *parent = 0); ~AccountDialog(); signals: void userIDChanged(QString newUserID); public slots: void askForToken(); void setVerifierCode(); void showAuthorizationStatus(bool authorized); void showAuthorizationURL(QUrl url); void saveDetails(); private: QVBoxLayout *mainLayout; QLabel *helpMessage1Label; QHBoxLayout *idLayout; QLabel *userIDIconLabel; QLabel *userIDLabel; QLineEdit *userIDLineEdit; QPushButton *getVerifierButton; QLabel *helpMessage2Label; QHBoxLayout *verifierLayout; QLabel *verifierIconLabel; QLabel *verifierLabel; QLineEdit *verifierLineEdit; QPushButton *authorizeApplicationButton; QLabel *errorsLabel; QLabel *authorizationStatusLabel; QHBoxLayout *buttonsLayout; QPushButton *saveButton; QPushButton *cancelButton; PumpController *pController; }; #endif // ACCOUNT_H dianara-v1.1/src/audienceselector.h000664 000764 000764 00000004257 12260657137 017033 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef AUDIENCESELECTOR_H #define AUDIENCESELECTOR_H #include #include #include #include #include #include #include #include #include #include #include #include "pumpcontroller.h" #include "peoplewidget.h" class AudienceSelector : public QFrame { Q_OBJECT public: explicit AudienceSelector(PumpController *pumpController, QString selectorType, QWidget *parent = 0); ~AudienceSelector(); void resetLists(); protected: virtual void keyPressEvent(QKeyEvent *event); signals: void audienceSelected(QString selectorType, QStringList contactsList); public slots: void copyToSelected(QString contactString); void setAudience(); private: QString selectorType; QHBoxLayout *mainLayout; QVBoxLayout *allGroupboxLayout; QGroupBox *allContactsGroupbox; PeopleWidget *peopleWidget; QVBoxLayout *selectedGroupboxLayout; QGroupBox *selectedListGroupbox; QLabel *explanationLabel; QListWidget *selectedListWidget; QPushButton *clearSelectedListButton; QHBoxLayout *buttonsLayout; QPushButton *doneButton; QPushButton *cancelButton; }; #endif // AUDIENCESELECTOR_H dianara-v1.1/src/audienceselector.cpp000664 000764 000764 00000013270 12260657141 017354 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "audienceselector.h" AudienceSelector::AudienceSelector(PumpController *pumpController, QString selectorType, QWidget *parent) : QFrame(parent) { this->selectorType = selectorType; QString titlePart; if (this->selectorType == "to") { titlePart = tr("'To' List"); } else { titlePart = tr("'CC' List"); } this->setWindowTitle("Dianara - " + titlePart); this->setWindowIcon(QIcon::fromTheme("system-users")); this->setWindowFlags(Qt::Window); this->setWindowModality(Qt::WindowModal); this->setMinimumSize(300, 300); // Left side, all contacts, with filter this->peopleWidget = new PeopleWidget(tr("&Add to Selected") + " >>", PeopleWidget::EmbeddedWidget, pumpController); connect(peopleWidget, SIGNAL(contactSelected(QString)), this, SLOT(copyToSelected(QString))); connect(peopleWidget, SIGNAL(addButtonPressed(QString)), this, SLOT(copyToSelected(QString))); this->allGroupboxLayout = new QVBoxLayout(); allGroupboxLayout->addWidget(peopleWidget); allContactsGroupbox = new QGroupBox(tr("All Contacts")); allContactsGroupbox->setLayout(allGroupboxLayout); // Right side, selected contacts explanationLabel = new QLabel(tr("Select people from the list on the left.\n" "You can drag them with the mouse, click or " "double-click on them, or select them and " "use the button below.")); explanationLabel->setWordWrap(true); selectedListWidget = new QListWidget(); selectedListWidget->setDragDropMode(QListView::DragDrop); selectedListWidget->setDefaultDropAction(Qt::MoveAction); selectedListWidget->setSelectionMode(QListView::ExtendedSelection); this->clearSelectedListButton = new QPushButton(QIcon::fromTheme("edit-clear-list"), tr("Clear &List")); connect(clearSelectedListButton, SIGNAL(clicked()), selectedListWidget, SLOT(clear())); doneButton = new QPushButton(QIcon::fromTheme("dialog-ok"), tr("&Done")); connect(doneButton, SIGNAL(clicked()), this, SLOT(setAudience())); cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel"), tr("&Cancel")); connect(cancelButton, SIGNAL(clicked()), this, SLOT(close())); buttonsLayout = new QHBoxLayout(); buttonsLayout->setAlignment(Qt::AlignRight); buttonsLayout->addWidget(doneButton); buttonsLayout->addWidget(cancelButton); selectedGroupboxLayout = new QVBoxLayout(); selectedGroupboxLayout->addWidget(explanationLabel); selectedGroupboxLayout->addSpacing(12); selectedGroupboxLayout->addWidget(selectedListWidget); selectedGroupboxLayout->addWidget(clearSelectedListButton); selectedGroupboxLayout->addSpacing(8); selectedGroupboxLayout->addLayout(buttonsLayout); this->selectedListGroupbox = new QGroupBox(tr("Selected People")); selectedListGroupbox->setLayout(selectedGroupboxLayout); this->mainLayout = new QHBoxLayout(); mainLayout->addWidget(allContactsGroupbox, 3); mainLayout->addWidget(selectedListGroupbox, 4); this->setLayout(mainLayout); qDebug() << "AudienceSelector created" << titlePart; } AudienceSelector::~AudienceSelector() { qDebug() << "AudienceSelector destroyed"; } /* * Reset lists and widgets to default status * */ void AudienceSelector::resetLists() { this->selectedListWidget->clear(); this->peopleWidget->resetWidget(); } void AudienceSelector::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { this->close(); } else { event->accept(); } } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// SLOTS ////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// /* * Copy a contact to the list of Selected * * The contact string comes in a SIGNAL from PeopleWidget * */ void AudienceSelector::copyToSelected(QString contactString) { if (!contactString.isEmpty()) { this->selectedListWidget->addItem(contactString); } } /* * The "Done" button: emit signal with list of selected people * */ void AudienceSelector::setAudience() { QStringList addressList; int addressCount = this->selectedListWidget->count(); for (int counter=0; counter < addressCount; ++counter) { addressList.append(selectedListWidget->item(counter)->text()); } emit audienceSelected(selectorType, addressList); this->close(); } dianara-v1.1/src/minorfeeditem.h000664 000764 000764 00000003740 12260657135 016336 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef MINORFEEDITEM_H #define MINORFEEDITEM_H #include #include #include #include #include #include #include #include #include "pumpcontroller.h" #include "timestamp.h" #include "asactivity.h" #include "post.h" class MinorFeedItem : public QFrame { Q_OBJECT public: explicit MinorFeedItem(ASActivity *activity, QString activityActorAvatarFilename, PumpController *pumpController, QWidget *parent = 0); ~MinorFeedItem(); void setItemAsNew(bool isNew); signals: void itemRead(); public slots: void openOriginalPost(); protected: virtual void mousePressEvent(QMouseEvent *event); private: PumpController *pController; QHBoxLayout *mainLayout; QVBoxLayout *leftLayout; QVBoxLayout *rightLayout; QLabel *avatarLabel; QLabel *timestampLabel; QLabel *activityDescriptionLabel; QPushButton *openButton; QVariantMap originalObjectMap; QVariantMap inReplyToMap; bool itemIsNew; }; #endif // MINORFEEDITEM_H dianara-v1.1/src/minorfeeditem.cpp000664 000764 000764 00000021264 12262042505 016661 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "minorfeeditem.h" MinorFeedItem::MinorFeedItem(ASActivity *activity, QString activityActorAvatarFilename, PumpController *pumpController, QWidget *parent) : QFrame(parent) { this->pController = pumpController; this->itemIsNew = false; // This sizePolicy prevents cut messages, and the huge space at the end // of the feed, after clicking "More" several times this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); activity->setParent(this); // reparent the passed activity QFont mainFont; mainFont.setPointSize(mainFont.pointSize() - 2); QString authorId = activity->getAuthorId(); avatarLabel = new QLabel(); avatarLabel->setToolTip(authorId); if (QFile::exists(activityActorAvatarFilename)) { avatarLabel->setPixmap(QPixmap(activityActorAvatarFilename) .scaledToWidth(32, Qt::SmoothTransformation)); } else { avatarLabel->setPixmap(QIcon::fromTheme("user-identity") .pixmap(32,32) .scaledToWidth(32, Qt::SmoothTransformation)); } QString generator = activity->getGenerator(); if (!generator.isEmpty()) { generator = "\n" + tr("using %1", "Application used to generate this activity").arg(generator); } QString timestamp = activity->getCreatedAt(); timestampLabel = new QLabel(Timestamp::fuzzyTime(timestamp)); mainFont.setBold(true); timestampLabel->setFont(mainFont); timestampLabel->setWordWrap(true); timestampLabel->setToolTip(Timestamp::localTimeDate(timestamp) + generator); timestampLabel->setAlignment(Qt::AlignCenter); timestampLabel->setAutoFillBackground(true); timestampLabel->setForegroundRole(QPalette::Text); timestampLabel->setBackgroundRole(QPalette::Base); timestampLabel->setFrameStyle(QFrame::Panel | QFrame::Raised); QString activityTooltip; QString activityAuthorName = activity->object()->getAuthorName(); if (activityAuthorName.isEmpty()) { activityAuthorName = activity->object()->getAuthorId(); } // If there's a name or an ID, show it if (!activityAuthorName.isEmpty()) { activityTooltip = "[ " + activityAuthorName + " ]" "

"; } QString activityObjectContent = activity->object()->getContent(); if (!activityObjectContent.isEmpty()) { activityTooltip.append(""); activityTooltip.append(activityObjectContent); } activityDescriptionLabel = new QLabel(activity->getContent()); mainFont.setBold(false); activityDescriptionLabel->setFont(mainFont); activityDescriptionLabel->setWordWrap(true); activityDescriptionLabel->setOpenExternalLinks(true); activityDescriptionLabel->setToolTip(activityTooltip); openButton = new QPushButton("+"); openButton->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum); openButton->setToolTip(tr("Open referenced post")); originalObjectMap = activity->object()->getOriginalObject(); inReplyToMap = activity->object()->getInReplyTo(); QString inReplyToAuthor = inReplyToMap.value("author").toMap().value("id").toString(); if (inReplyToAuthor.startsWith("acct:")) { inReplyToAuthor.remove(0,5); } // Highlight this item if it's about the user bool highlightItem = false; // TMP/FIXME: add different kinds of highlighting if (activity->object()->getId() == pController->currentUserId()) { highlightItem = true; } if (activity->object()->getAuthorId() == pController->currentUserId()) { highlightItem = true; } if (inReplyToAuthor == pController->currentUserId()) { highlightItem = true; } if (activity->getRecipientsIdList().contains("acct:" + pController->currentUserId())) { highlightItem = true; } if (highlightItem) { // Unless they are both empty! if (!pController->currentUserId().isEmpty()) { this->setFrameStyle(QFrame::Panel); this->timestampLabel->setText("** " + timestampLabel->text() + " **"); // kinda TMP } } // Layout leftLayout = new QVBoxLayout(); leftLayout->setAlignment(Qt::AlignTop); leftLayout->addWidget(avatarLabel, 1, Qt::AlignTop | Qt::AlignLeft); // Original post available (inReplyTo) or object available (note, image...) if (!inReplyToMap.isEmpty() || ((activity->object()->getType() == "note" || activity->object()->getType() == "image") && activity->object()->getDeletedTime().isEmpty() ) ) { connect(openButton, SIGNAL(clicked()), this, SLOT(openOriginalPost())); leftLayout->addWidget(openButton, 0, Qt::AlignHCenter); } rightLayout = new QVBoxLayout(); rightLayout->setAlignment(Qt::AlignTop); rightLayout->addWidget(timestampLabel); rightLayout->addWidget(activityDescriptionLabel); mainLayout = new QHBoxLayout(); if (authorId == pController->currentUserId()) { mainLayout->addLayout(rightLayout, 10); mainLayout->addLayout(leftLayout, 1); } else // Normal item, not ours { mainLayout->addLayout(leftLayout, 1); mainLayout->addLayout(rightLayout, 10); } this->setLayout(mainLayout); qDebug() << "MinorFeedItem created"; } MinorFeedItem::~MinorFeedItem() { qDebug() << "MinorFeedItem destroyed"; } /* * Pseudo-highlight for new items * */ void MinorFeedItem::setItemAsNew(bool isNew) { itemIsNew = isNew; if (itemIsNew) { this->setAutoFillBackground(true); this->setBackgroundRole(QPalette::Mid); } else { this->setAutoFillBackground(false); this->setBackgroundRole(QPalette::Window); emit itemRead(); } } /* * On mouse click in any part of the item, set it as read * */ void MinorFeedItem::mousePressEvent(QMouseEvent *event) { if (itemIsNew) { this->setItemAsNew(false); } event->accept(); } /****************************************************************************/ /***************************** SLOTS ****************************************/ /****************************************************************************/ void MinorFeedItem::openOriginalPost() { // Create a fake activity for the object QVariantMap originalPostMap; if (!inReplyToMap.isEmpty()) { originalPostMap.insert("object", this->inReplyToMap); originalPostMap.insert("actor", this->inReplyToMap.value("author").toMap()); } else { originalPostMap.insert("object", this->originalObjectMap); originalPostMap.insert("actor", this->originalObjectMap.value("author").toMap()); } ASActivity *originalPostActivity = new ASActivity(originalPostMap, this); Post *referencedPost = new Post(this->pController, originalPostActivity, true, // Post is standalone this->parentWidget()); // Pass parent widget (MinorFeed) instead // of 'this', so it won't be killed by reloads referencedPost->setWindowFlags(Qt::Window); referencedPost->setWindowModality(Qt::WindowModal); referencedPost->setMinimumSize(420, 360); // FIXME referencedPost->resize(600, 500); // FIXME referencedPost->show(); referencedPost->setPostHeight(); connect(pController, SIGNAL(commentsReceived(QVariantList,QString)), referencedPost, SLOT(setAllComments(QVariantList,QString))); referencedPost->getAllComments(); } dianara-v1.1/src/listsmanager.h000664 000764 000764 00000004544 12260657135 016203 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef LISTSMANAGER_H #define LISTSMANAGER_H #include #include #include #include #include #include #include #include #include #include // TMP!! #include #include "pumpcontroller.h" #include "peoplewidget.h" class ListsManager : public QWidget { Q_OBJECT public: explicit ListsManager(PumpController *pumpController, QWidget *parent = 0); ~ListsManager(); void setListsList(QVariantList listsList); signals: public slots: void setPersonsInList(QVariantList personList, QString listUrl); void createList(); void deleteList(); void enableDisableDeleteButtons(); void showAddPersonDialog(); void addPerson(QString contactString); void removePerson(); void addPersonItemToList(QString personId, QString personName); void removePersonItemFromList(QString personId); private: PumpController *pController; QVBoxLayout *mainLayout; QHBoxLayout *buttonsLayout; QTreeWidget *listsTreeWidget; QPushButton *deleteListButton; QPushButton *addPersonButton; QPushButton *removePersonButton; PeopleWidget *peopleWidget; QGroupBox *newListGroupbox; QHBoxLayout *groupboxMainLayout; QVBoxLayout *groupboxLeftLayout; QLineEdit *newListNameLineEdit; QTextEdit *newListDescTextEdit; QPushButton *createListButton; QStringList personListsUrlList; }; #endif // LISTSMANAGER_H dianara-v1.1/src/listsmanager.cpp000664 000764 000764 00000032564 12260657141 016536 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "listsmanager.h" ListsManager::ListsManager(PumpController *pumpController, QWidget *parent) : QWidget(parent) { this->pController = pumpController; connect(pController, SIGNAL(personListReceived(QVariantList,QString)), this, SLOT(setPersonsInList(QVariantList,QString))); connect(pController, SIGNAL(personAddedToList(QString,QString)), this, SLOT(addPersonItemToList(QString,QString))); connect(pController, SIGNAL(personRemovedFromList(QString)), this, SLOT(removePersonItemFromList(QString))); // To show the current lists this->listsTreeWidget = new QTreeWidget(); listsTreeWidget->setColumnCount(2); listsTreeWidget->setHeaderLabels(QStringList() << tr("Name") << tr("Members")); listsTreeWidget->setAlternatingRowColors(true); listsTreeWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); connect(listsTreeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(enableDisableDeleteButtons())); // Buttons to add/remove people from lists, and the lists; Initially disabled this->addPersonButton = new QPushButton(QIcon::fromTheme("list-add-user"), tr("Add Mem&ber")); addPersonButton->setDisabled(true); connect(addPersonButton, SIGNAL(clicked()), this, SLOT(showAddPersonDialog())); this->removePersonButton = new QPushButton(QIcon::fromTheme("list-remove-user"), tr("&Remove Member")); removePersonButton->setDisabled(true); connect(removePersonButton, SIGNAL(clicked()), this, SLOT(removePerson())); this->deleteListButton = new QPushButton(QIcon::fromTheme("edit-table-delete-row"), tr("&Delete Selected List")); deleteListButton->setDisabled(true); connect(deleteListButton, SIGNAL(clicked()), this, SLOT(deleteList())); // Groupbox for the "create new list" stuff this->newListGroupbox = new QGroupBox(tr("Add New &List")); this->newListNameLineEdit = new QLineEdit(); newListNameLineEdit->setPlaceholderText(tr("Type a name for the new list...")); connect(newListNameLineEdit, SIGNAL(returnPressed()), this, SLOT(createList())); this->newListDescTextEdit = new QTextEdit(); newListDescTextEdit->setToolTip(tr("Type an optional description here")); newListDescTextEdit->setTabChangesFocus(true); this->createListButton = new QPushButton(QIcon::fromTheme("edit-table-insert-row-above"), tr("Create L&ist")); connect(createListButton, SIGNAL(clicked()), this, SLOT(createList())); // Widget to find and select a contact this->peopleWidget = new PeopleWidget(tr("&Add to List"), PeopleWidget::StandaloneWidget, this->pController, this); connect(peopleWidget, SIGNAL(addButtonPressed(QString)), this, SLOT(addPerson(QString))); // Layout this->buttonsLayout = new QHBoxLayout(); buttonsLayout->addWidget(addPersonButton, 0, Qt::AlignLeft); buttonsLayout->addWidget(removePersonButton, 0, Qt::AlignLeft); buttonsLayout->addStretch(1); buttonsLayout->addWidget(deleteListButton, 0, Qt::AlignRight); this->groupboxLeftLayout = new QVBoxLayout(); groupboxLeftLayout->addWidget(newListNameLineEdit); groupboxLeftLayout->addWidget(newListDescTextEdit); this->groupboxMainLayout = new QHBoxLayout(); groupboxMainLayout->addLayout(groupboxLeftLayout); groupboxMainLayout->addWidget(createListButton, 0, Qt::AlignBottom); newListGroupbox->setLayout(groupboxMainLayout); this->mainLayout = new QVBoxLayout(); mainLayout->addWidget(listsTreeWidget, 5); mainLayout->addLayout(buttonsLayout, 1); mainLayout->addStretch(1); mainLayout->addWidget(newListGroupbox, 3); this->setLayout(mainLayout); qDebug() << "ListsManager created"; } ListsManager::~ListsManager() { qDebug() << "ListsManager destroyed"; } void ListsManager::setListsList(QVariantList listsList) { qDebug() << "Setting person lists contents"; this->listsTreeWidget->clear(); this->personListsUrlList.clear(); QString listName; QString listMembersCount; QVariant listId; foreach (QVariant list, listsList) { QVariantMap listMap = list.toMap(); listName = listMap.value("displayName").toString(); listMembersCount = listMap.value("members").toMap().value("totalItems").toString(); listId = listMap.value("id"); qDebug() << "list ID:" << listId.toString(); QTreeWidgetItem *listItem = new QTreeWidgetItem(); listItem->setText(0, listName); listItem->setText(1, listMembersCount); listItem->setData(0, Qt::UserRole, listId); this->listsTreeWidget->addTopLevelItem(listItem); QString membersUrl = listMap.value("members").toMap().value("url").toString(); this->personListsUrlList.append(membersUrl); } // Get list of people in each list foreach (QString url, personListsUrlList) { this->pController->getPersonList(url); } this->listsTreeWidget->resizeColumnToContents(0); } /****************************************************************************/ /********************************* SLOTS ************************************/ /****************************************************************************/ /* * Fill one of the person lists with the names and ID's of the members * */ void ListsManager::setPersonsInList(QVariantList personList, QString listUrl) { qDebug() << "ListsManager::setPersonsInList()" << listUrl; QString listId = listUrl.split("/members").at(0); qDebug() << "List ID:" << listId; // Find out which TreeWidgetItem matches this list QTreeWidgetItem *listItem = 0; // Avoid 'not initialized' warning foreach (QTreeWidgetItem *item, this->listsTreeWidget->findItems("", Qt::MatchContains)) { listItem = item; if (listItem->data(0, Qt::UserRole).toString() == listId) { break; } } foreach (QVariant personVariant, personList) { QVariantMap personMap = personVariant.toMap(); QString id = personMap.value("id").toString(); if (id.startsWith("acct:")) { id.remove(0, 5); } QString name = personMap.value("displayName").toString(); QTreeWidgetItem *childItem = new QTreeWidgetItem(); childItem->setIcon(0, QIcon::fromTheme("user-identity")); childItem->setText(0, name); childItem->setText(1, id); if (listItem != 0) // Ensure it's initialized! { listItem->addChild(childItem); } } } void ListsManager::createList() { QString listName = this->newListNameLineEdit->text().trimmed(); QString listDescription = this->newListDescTextEdit->toPlainText().trimmed(); listDescription.replace("\n", "
"); if (!listName.isEmpty()) { qDebug() << "Creating list:" << listName; this->pController->createPersonList(listName, listDescription); this->newListNameLineEdit->clear(); this->newListDescTextEdit->clear(); } else { qDebug() << "Error: List name is empty!"; } } void ListsManager::deleteList() { if (listsTreeWidget->currentItem() == 0) // if nothing selected, so NULL... { return; } QString listName = this->listsTreeWidget->currentItem()->text(0); int confirmation = QMessageBox::question(this, tr("WARNING: Delete list?"), tr("Are you sure you want to delete %1?", "1=Name of a person list").arg(listName), tr("&Yes, delete it"), tr("&No"), "", 1, 1); if (confirmation == 0) { QString listId = this->listsTreeWidget->currentItem()->data(0, Qt::UserRole).toString(); this->pController->deletePersonList(listId); } else { qDebug() << "Confirmation canceled, not deleting the list"; } } /* * Enable or disable the "remove person" and "delete list" buttons * according to what's currently selected * */ void ListsManager::enableDisableDeleteButtons() { // A list is selected if (listsTreeWidget->currentItem()->parent() == 0) { this->deleteListButton->setEnabled(true); this->removePersonButton->setDisabled(true); } else // One of the items inside a list is selected { this->deleteListButton->setDisabled(true); this->removePersonButton->setEnabled(true); } // Either way... this->addPersonButton->setEnabled(true); } void ListsManager::showAddPersonDialog() { if (listsTreeWidget->currentItem() == 0) { return; } // Show the people widget, which will return the selected contact in a SIGNAL this->peopleWidget->resize(400, this->height()); this->peopleWidget->resetWidget(); this->peopleWidget->show(); } void ListsManager::addPerson(QString contactString) { this->peopleWidget->hide(); QRegExp contactRE("(.+)\\s+\\<(.+@.+)\\>", Qt::CaseInsensitive); contactRE.setMinimal(true); contactRE.indexIn(contactString); QString personId = contactRE.cap(2).trimmed(); // The part between < and > if (personId.isEmpty()) { return; } QString listId; if (listsTreeWidget->currentItem()->parent() == 0) // If a list is selected { listId = this->listsTreeWidget->currentItem()->data(0, Qt::UserRole).toString(); } else // If a member is selected, get its matching list { listId = this->listsTreeWidget->currentItem()->parent()->data(0, Qt::UserRole).toString(); } this->setDisabled(true); pController->addPersonToList(listId, "acct:" + personId); } void ListsManager::removePerson() { if (listsTreeWidget->currentItem() == 0) // Nothing selected, so it's NULL { return; } QString personId = this->listsTreeWidget->currentItem()->text(1); QString personName = this->listsTreeWidget->currentItem()->text(0); if (personName.isEmpty()) { personName = personId; } QString listId; QString listName; if (listsTreeWidget->currentItem()->parent() != 0) // Ensure it's not a list { listId = this->listsTreeWidget->currentItem()->parent()->data(0, Qt::UserRole).toString(); listName = this->listsTreeWidget->currentItem()->parent()->text(0); } int confirmation = QMessageBox::question(this, tr("Remove person from list?"), tr("Are you sure you want to remove %1 " "from the %2 list?", "1=Name of a person, " "2=name of a list") .arg(personName).arg(listName), tr("&Yes"), tr("&No"), "", 1, 1); if (confirmation == 0) { this->setDisabled(true); QString id = this->listsTreeWidget->currentItem()->text(1); this->pController->removePersonFromList(listId, "acct:" + personId); } else { qDebug() << "Confirmation canceled, not removing" << personName << "from" << listName; } } /* * Add the new item itself after a person is added correctly in PumpController * */ void ListsManager::addPersonItemToList(QString personId, QString personName) { if (personId.startsWith("acct:")) { personId.remove(0,5); } QTreeWidgetItem *newItem = new QTreeWidgetItem(); newItem->setIcon(0, QIcon::fromTheme("user-identity")); newItem->setText(0, personName); newItem->setText(1, personId); QTreeWidgetItem *selectedItem = this->listsTreeWidget->currentItem(); if (selectedItem->parent() == 0) // The list itself is selected { selectedItem->addChild(newItem); } else // One of the members is selected, so add the child to the parent item { selectedItem->parent()->addChild(newItem); } this->setEnabled(true); } void ListsManager::removePersonItemFromList(QString personId) { if (personId == "acct:" + listsTreeWidget->currentItem()->text(1)) { delete listsTreeWidget->currentItem(); } this->setEnabled(true); } dianara-v1.1/src/filtereditor.h000664 000764 000764 00000004077 12260657137 016211 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef FILTEREDITOR_H #define FILTEREDITOR_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pumpcontroller.h" class FilterEditor : public QWidget { Q_OBJECT public: explicit FilterEditor(PumpController *pumpController, QWidget *parent = 0); ~FilterEditor(); void loadFilters(); signals: public slots: void addFilter(); void removeFilter(); void saveFilters(); private: QVBoxLayout *mainLayout; QHBoxLayout *topLayout; QGroupBox *newFilterGroupBox; QVBoxLayout *middleLayout; QGroupBox *currentFiltersGroupBox; QHBoxLayout *bottomLayout; QLabel *explanationLabel; QComboBox *filterTypeComboBox; QLineEdit *filterWordsLineEdit; QPushButton *addFilterButton; QListWidget *currentFiltersListWidget; QPushButton *removeFilterButton; QPushButton *saveButton; QPushButton *cancelButton; QVariantList filtersList; PumpController *pController; }; #endif // FILTEREDITOR_H dianara-v1.1/src/asactivity.h000664 000764 000764 00000004377 12262311626 015670 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef ASACTIVITY_H #define ASACTIVITY_H #include #include #include #include #include "asobject.h" #include "mischelpers.h" class ASActivity : public QObject { Q_OBJECT public: explicit ASActivity(QVariantMap activityMap, QObject *parent = 0); ~ASActivity(); ASObject *object(); QString getId(); QString getVerb(); QString getGenerator(); QString getCreatedAt(); QString getUpdatedAt(); QString getContent(); QString getToString(); QString getCCString(); QStringList getRecipientsIdList(); bool isShared(); QString getSharedByName(); QString getSharedById(); QString getSharedByAvatar(); QString getAuthorName(); QString getAuthorId(); QString getAuthorAvatar(); QString getAuthorUrl(); QString getAuthorHometown(); QString getAuthorBio(); signals: public slots: private: ASObject *asObject; QString id; QString verb; QString generator; QString createdAt; QString updatedAt; QString content; QString toString; QString ccString; QStringList recipientsIdList; bool shared; QString sharedByName; QString sharedById; QString sharedByAvatar; QString authorName; QString authorId; QString authorAvatar; QString authorUrl; QString authorHometown; QString authorBio; }; #endif // ASACTIVITY_H dianara-v1.1/src/asobject.cpp000664 000764 000764 00000016044 12260657141 015632 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "asobject.h" ASObject::ASObject(QVariantMap objectMap, QObject *parent) : QObject(parent) { this->originalObjectMap = objectMap; /// Meta information id = objectMap.value("id").toString(); if (id.startsWith("acct:")) { id.remove(0,5); } type = objectMap.value("objectType").toString(); url = objectMap.value("url").toString(); createdAt = objectMap.value("published").toString(); updatedAt = objectMap.value("updated").toString(); // FIXME: kind of basic... location = objectMap.value("location").toMap().value("displayName").toString(); authorMap = objectMap.value("author").toMap(); // FIXME: this "author code" is duplicated from ASActivity and other places authorName = authorMap.value("displayName").toString(); authorId = authorMap.value("id").toString(); // For Author ID, remove the first 5 characters from the field, if they are "acct:" if (authorId.startsWith("acct:")) { authorId.remove(0,5); } authorAvatar = authorMap.value("image").toMap().value("url").toString(); authorUrl = authorMap.value("url").toString(); authorHometown = authorMap.value("location").toMap().value("displayName").toString(); authorBio = authorMap.value("summary").toString(); // This will hold the date when the post was deleted, or empty if it wasn't deleted = objectMap.value("deleted").toString(); liked = objectMap.value("liked").toString(); // The object to which this one replies, if any (note to which a comment replies, etc) inReplyToMap = objectMap.value("inReplyTo").toMap(); /// /// End of "meta"; Start of content /// if (type == "image") { // See if there's a proxy URL for the image first (for private images) imageUrl = objectMap.value("fullImage").toMap().value("pump_io").toMap().value("proxyURL").toString(); qDebug() << "Trying Proxyed fullImage"; // if that's empty, try the "small" version if (imageUrl.isEmpty()) { qDebug() << "Trying Proxyed thumbnail image"; imageUrl = objectMap.value("image").toMap().value("pump_io").toMap().value("proxyURL").toString(); } // If that's also empty, use regular fullImage->url field if (imageUrl.isEmpty()) { qDebug() << "Trying direct fullImage"; imageUrl = objectMap.value("fullImage").toMap().value("url").toString(); } // And if that is ALSO empty, use regular image->url if (imageUrl.isEmpty()) { qDebug() << "Trying direct thumbnail image"; imageUrl = objectMap.value("image").toMap().value("url").toString(); } qDebug() << "postImage:" << imageUrl; } // Title can be in non-image posts, too! title = objectMap.value("displayName").toString(); content = objectMap.value("content").toString(); likesCount = objectMap.value("likes").toMap().value("totalItems").toString(); commentsCount = objectMap.value("replies").toMap().value("totalItems").toString(); sharesCount = objectMap.value("shares").toMap().value("totalItems").toString(); // Get last likes, comments and shares list here, used from Post() lastLikesList = objectMap.value("likes").toMap().value("items").toList(); lastCommentsList = objectMap.value("replies").toMap().value("items").toList(); lastSharesList = objectMap.value("shares").toMap().value("items").toList(); // Get URL for likes; first, proxyURL if it exists likesUrl = objectMap.value("likes").toMap().value("pump_io").toMap().value("proxyURL").toString(); // If still empty, get regular URL (that means the post is in the same server we are) if (likesUrl.isEmpty()) { likesUrl = objectMap.value("likes").toMap().value("url").toString(); } // Get URL for comments; first, proxyURL if it exists commentsUrl = objectMap.value("replies").toMap().value("pump_io").toMap().value("proxyURL").toString(); if (commentsUrl.isEmpty()) // If still empty, get regular URL { commentsUrl = objectMap.value("replies").toMap().value("url").toString(); } // FIXME: get sharesUrl... qDebug() << "ASObject created" << this->id; } ASObject::~ASObject() { qDebug() << "ASObject destroyed" << this->id; } // Getters QString ASObject::getId() { return this->id; } QString ASObject::getType() { return this->type; } QString ASObject::getUrl() { return this->url; } QString ASObject::getCreatedAt() { return this->createdAt; } QString ASObject::getUpdatedAt() { return this->updatedAt; } QString ASObject::getLocation() { return this->location; } QString ASObject::getDeletedTime() { return this->deleted; } QString ASObject::isLiked() { return this->liked; } QVariantMap ASObject::getAuthorMap() { return this->authorMap; } QString ASObject::getAuthorName() { return this->authorName; } QString ASObject::getAuthorId() { return this->authorId; } QString ASObject::getAuthorAvatar() { return this->authorAvatar; } QString ASObject::getAuthorUrl() { return this->authorUrl; } QString ASObject::getAuthorHometown() { return this->authorHometown; } QString ASObject::getAuthorBio() { return this->authorBio; } QString ASObject::getTitle() { return this->title; } QString ASObject::getContent() { return this->content; } QString ASObject::getImageUrl() { return this->imageUrl; } QString ASObject::getLikesCount() { return this->likesCount; } QString ASObject::getCommentsCount() { return this->commentsCount; } QString ASObject::getSharesCount() { return this->sharesCount; } QVariantList ASObject::getLastLikesList() { return this->lastLikesList; } QVariantList ASObject::getLastCommentsList() { return this->lastCommentsList; } QVariantList ASObject::getLastSharesList() { return this->lastSharesList; } QString ASObject::getLikesUrl() { return this->likesUrl; } QString ASObject::getCommentsUrl() { return this->commentsUrl; } QString ASObject::getSharesUrl() { return this->sharesUrl; } QVariantMap ASObject::getOriginalObject() { return this->originalObjectMap; } QVariantMap ASObject::getInReplyTo() { return this->inReplyToMap; } dianara-v1.1/src/asactivity.cpp000664 000764 000764 00000014225 12262311641 016211 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "asactivity.h" ASActivity::ASActivity(QVariantMap activityMap, QObject *parent) : QObject(parent) { id = activityMap.value("id").toString(); // Get the object included with the activity: the post itself, or whatever this->asObject = new ASObject(activityMap.value("object").toMap(), this); // Activity verb: post, share... verb = activityMap.value("verb").toString(); QVariantMap authorMap; if (verb == "share") { shared = true; // Get original author data authorMap = this->asObject->getAuthorMap(); sharedByName = activityMap.value("actor").toMap().value("displayName").toString(); sharedById = activityMap.value("actor").toMap().value("id").toString(); if (sharedById.startsWith("acct:")) { sharedById.remove(0,5); } sharedByAvatar = activityMap.value("actor").toMap().value("image").toMap().value("url").toString(); } else { shared = false; // Not a share, so no original author, just get the actor authorMap = activityMap.value("actor").toMap(); } authorName = authorMap.value("displayName").toString(); authorAvatar = authorMap.value("image").toMap().value("url").toString(); authorId = authorMap.value("id").toString(); // For Author ID, remove the first 5 characters from the field, if they are "acct:" // (users have acct: in front of the ID, but services like OFG don't) if (authorId.startsWith("acct:")) { authorId.remove(0,5); } authorUrl = authorMap.value("url").toString(); authorHometown = authorMap.value("location").toMap().value("displayName").toString(); authorBio = authorMap.value("summary").toString(); // Timestamps createdAt = activityMap.value("published").toString(); updatedAt = activityMap.value("updated").toString(); // Software used to generate the activity: webUI, a client, a service... generator = activityMap.value("generator").toMap().value("displayName").toString(); // Content of the activity, usually a description like "User followed someone" content = activityMap.value("content").toString(); // Audience: To QVariantMap postToMap; foreach (QVariant postToVariant, activityMap.value("to").toList()) { postToMap = postToVariant.toMap(); QString toId = postToMap.value("id").toString(); if (toId == "http://activityschema.org/collection/public") { toString += "" + tr("Public") + ", "; } else { QString displayName = postToMap.value("displayName").toString().trimmed(); displayName = MiscHelpers::fixLongName(displayName); if (!displayName.isEmpty()) { toString += "" + displayName + ", "; } recipientsIdList.append(toId); } } toString.remove(-2, 2); // remove last comma and space // and CC QVariantMap postCCMap; foreach (QVariant postCCVariant, activityMap.value("cc").toList()) { postCCMap = postCCVariant.toMap(); QString ccId = postCCMap.value("id").toString(); if (ccId == "http://activityschema.org/collection/public") { ccString += "" + tr("Public") + ", "; } else { QString displayName = postCCMap.value("displayName").toString().trimmed(); displayName = MiscHelpers::fixLongName(displayName); if (!displayName.isEmpty()) { ccString += "" + displayName + ", "; } recipientsIdList.append(ccId); } } ccString.remove(-2, 2); qDebug() << "ASActivity created" << this->id; } ASActivity::~ASActivity() { qDebug() << "ASActivity destroyed" << this->id; } // Getters! ASObject *ASActivity::object() { return this->asObject; } QString ASActivity::getId() { return this->id; } QString ASActivity::getVerb() { return this->verb; } QString ASActivity::getGenerator() { return this->generator; } QString ASActivity::getCreatedAt() { return this->createdAt; } QString ASActivity::getUpdatedAt() { return this->updatedAt; } QString ASActivity::getContent() { return this->content; } QString ASActivity::getToString() { return this->toString; } QString ASActivity::getCCString() { return this->ccString; } QStringList ASActivity::getRecipientsIdList() { return this->recipientsIdList; } bool ASActivity::isShared() { return this->shared; } QString ASActivity::getSharedByName() { return this->sharedByName; } QString ASActivity::getSharedById() { return this->sharedById; } QString ASActivity::getSharedByAvatar() { return this->sharedByAvatar; } QString ASActivity::getAuthorName() { return this->authorName; } QString ASActivity::getAuthorId() { return this->authorId; } QString ASActivity::getAuthorAvatar() { return this->authorAvatar; } QString ASActivity::getAuthorUrl() { return this->authorUrl; } QString ASActivity::getAuthorHometown() { return this->authorHometown; } QString ASActivity::getAuthorBio() { return this->authorBio; } dianara-v1.1/src/filtereditor.cpp000664 000764 000764 00000016341 12260657141 016534 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "filtereditor.h" FilterEditor::FilterEditor(PumpController *pumpController, QWidget *parent) : QWidget(parent) { this->pController = pumpController; this->setWindowTitle(tr("Filter Editor") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("view-filter")); this->setWindowFlags(Qt::Window); this->setMinimumSize(300, 400); QFont explanationFont; explanationFont.setPointSize(explanationFont.pointSize() - 1); explanationLabel = new QLabel(tr("Here you can set some filters. You can filter " "by content, author or application.\n\n" "For instance, you can filter out messages posted by " "the application Open Farm Game, or which contain the " "word NSFW in the message.") + "\n\n" + tr("At the moment, these filters only apply to " "the Meanwhile feed.")); explanationLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); explanationLabel->setWordWrap(true); explanationLabel->setFont(explanationFont); filterTypeComboBox = new QComboBox(); filterTypeComboBox->addItem(QIcon::fromTheme("accessories-text-editor"), tr("Content")); filterTypeComboBox->addItem(QIcon::fromTheme("user-identity"), tr("Author")); filterTypeComboBox->addItem(QIcon::fromTheme("applications-other"), tr("Application")); filterWordsLineEdit = new QLineEdit(); filterWordsLineEdit->setPlaceholderText(tr("Keywords...")); addFilterButton = new QPushButton(QIcon::fromTheme("list-add"), tr("&Add Filter")); connect(addFilterButton, SIGNAL(clicked()), this, SLOT(addFilter())); currentFiltersListWidget = new QListWidget(); currentFiltersListWidget->setToolTip(tr("Filters in use")); removeFilterButton = new QPushButton(QIcon::fromTheme("list-remove"), tr("&Remove Selected Filter")); connect(removeFilterButton, SIGNAL(clicked()), this, SLOT(removeFilter())); saveButton = new QPushButton(QIcon::fromTheme("document-save"), tr("&Save Filters")); connect(saveButton, SIGNAL(clicked()), this, SLOT(saveFilters())); cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel"), tr("&Cancel")); connect(cancelButton, SIGNAL(clicked()), this, SLOT(hide())); // Layout topLayout = new QHBoxLayout(); topLayout->addWidget(filterTypeComboBox); topLayout->addWidget(filterWordsLineEdit); topLayout->addWidget(addFilterButton); newFilterGroupBox = new QGroupBox(tr("&New Filter")); newFilterGroupBox->setLayout(topLayout); middleLayout = new QVBoxLayout(); middleLayout->addWidget(currentFiltersListWidget); middleLayout->addWidget(removeFilterButton, 0, Qt::AlignRight); currentFiltersGroupBox = new QGroupBox(tr("C&urrent Filters")); currentFiltersGroupBox->setLayout(middleLayout); bottomLayout = new QHBoxLayout(); bottomLayout->setAlignment(Qt::AlignRight); bottomLayout->addWidget(saveButton); bottomLayout->addWidget(cancelButton); mainLayout = new QVBoxLayout(); mainLayout->addWidget(explanationLabel); mainLayout->addSpacing(8); mainLayout->addWidget(newFilterGroupBox); mainLayout->addSpacing(8); mainLayout->addWidget(currentFiltersGroupBox); mainLayout->addSpacing(8); mainLayout->addLayout(bottomLayout); this->setLayout(mainLayout); loadFilters(); qDebug() << "FilterEditor created"; } FilterEditor::~FilterEditor() { qDebug() << "FilterEditor destroyed"; } void FilterEditor::loadFilters() { QSettings settings; this->filtersList = settings.value("Filters/currentFilters").toList(); foreach (QVariant filterItem, filtersList) { int filterType = filterItem.toMap().value("type").toInt(); QString filterWords = filterItem.toMap().value("text").toString(); QVariantMap itemMap; itemMap.insert("type", filterType); itemMap.insert("text", filterWords); QListWidgetItem *item = new QListWidgetItem(filterTypeComboBox->itemText(filterType) + " = " + filterWords); item->setData(Qt::UserRole, itemMap); this->currentFiltersListWidget->addItem(item); } // Let the PumpController know this->pController->setFilters(filtersList); } /****************************************************************************/ /********************************* SLOTS ************************************/ /****************************************************************************/ void FilterEditor::addFilter() { QString words = this->filterWordsLineEdit->text().trimmed(); if (words.isEmpty()) { return; } QVariantMap itemMap; itemMap.insert("type", filterTypeComboBox->currentIndex()); itemMap.insert("text", words); QListWidgetItem *item = new QListWidgetItem(filterTypeComboBox->currentText() + " = " + words); item->setData(Qt::UserRole, itemMap); currentFiltersListWidget->addItem(item); filterWordsLineEdit->clear(); } void FilterEditor::removeFilter() { int selectedFilter = currentFiltersListWidget->currentRow(); qDebug() << "FilterEditor::removeFilter()" << selectedFilter; if (selectedFilter != -1) { QListWidgetItem *removedItem = currentFiltersListWidget->takeItem(selectedFilter); delete removedItem; } } void FilterEditor::saveFilters() { qDebug() << "FilterEditor::saveFilters()"; filtersList.clear(); for (int counter = 0; counter != currentFiltersListWidget->count(); ++counter) { QListWidgetItem *item = currentFiltersListWidget->item(counter); filtersList.append(item->data(Qt::UserRole)); } qDebug() << "filtersList: " << this->filtersList; QSettings settings; settings.setValue("Filters/currentFilters", filtersList); // Send to PumpController too this->pController->setFilters(filtersList); this->hide(); } dianara-v1.1/src/peoplewidget.h000664 000764 000764 00000004145 12260657141 016174 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #ifndef PEOPLEWIDGET_H #define PEOPLEWIDGET_H #include #include #include #include #include #include #include #include "pumpcontroller.h" class PeopleWidget : public QWidget { Q_OBJECT public: explicit PeopleWidget(QString buttonText, int type, PumpController *pumpController, QWidget *parent = 0); ~PeopleWidget(); void resetWidget(); enum WidgetType { EmbeddedWidget, StandaloneWidget }; signals: void contactSelected(QString contactString); void addButtonPressed(QString contactString); public slots: void filterList(QString searchTerms); void updateAllContactsList(QString listType, QVariantList contactsVariantList, int totalReceivedCount); void returnContact(); void returnClickedContact(QModelIndex modelIndex); private: PumpController *pController; QVBoxLayout *mainLayout; QLabel *searchLabel; QLineEdit *searchLineEdit; QListWidget *allContactsListWidget; QPushButton *addToButton; QPushButton *cancelButton; QStringList allContactsStringList; }; #endif // PEOPLEWIDGET_H dianara-v1.1/src/peoplewidget.cpp000664 000764 000764 00000013634 12260657141 016532 0ustar00janjan000000 000000 /* * This file is part of Dianara * Copyright 2012-2014 JanKusanagi * * 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 . */ #include "peoplewidget.h" PeopleWidget::PeopleWidget(QString buttonText, int type, PumpController *pumpController, QWidget *parent) : QWidget(parent) { this->setMinimumSize(120, 120); this->pController = pumpController; connect(pController, SIGNAL(contactListReceived(QString,QVariantList,int)), this, SLOT(updateAllContactsList(QString,QVariantList,int))); this->searchLabel = new QLabel(tr("&Search:")); this->searchLineEdit = new QLineEdit(); searchLineEdit->setPlaceholderText(tr("Enter a name here to search for it")); searchLabel->setBuddy(searchLineEdit); connect(searchLineEdit, SIGNAL(textChanged(QString)), this, SLOT(filterList(QString))); allContactsListWidget = new QListWidget(); allContactsListWidget->setDragDropMode(QListView::DragDrop); allContactsListWidget->setDefaultDropAction(Qt::CopyAction); allContactsListWidget->setSortingEnabled(true); connect(allContactsListWidget, SIGNAL(activated(QModelIndex)), this, SLOT(returnClickedContact(QModelIndex))); addToButton = new QPushButton(QIcon::fromTheme("list-add"), buttonText); connect(addToButton, SIGNAL(clicked()), this, SLOT(returnContact())); // Layout mainLayout = new QVBoxLayout(); mainLayout->addWidget(searchLabel); mainLayout->addWidget(searchLineEdit); mainLayout->addSpacing(2); mainLayout->addWidget(allContactsListWidget); mainLayout->addSpacing(4); mainLayout->addWidget(addToButton); this->setLayout(mainLayout); if (type == EmbeddedWidget) { //allContactsListWidget->setSelectionMode(QListView::ExtendedSelection); // FIXME 1.2: For now, Single selection mode in both cases allContactsListWidget->setSelectionMode(QListView::SingleSelection); } else // StandaloneWidget { this->setWindowTitle("Dianara - " + tr("Add a contact to a list")); this->setWindowIcon(QIcon::fromTheme("system-users")); this->setWindowFlags(Qt::Window); this->setWindowModality(Qt::WindowModal); allContactsListWidget->setSelectionMode(QListView::SingleSelection); cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel"), tr("&Cancel")); connect(cancelButton, SIGNAL(clicked()), this, SLOT(hide())); mainLayout->addWidget(cancelButton); } this->filterList(""); // Populate the list without filter, initially qDebug() << "PeopleWidget created"; } PeopleWidget::~PeopleWidget() { qDebug() << "PeopleWidget destroyed"; } void PeopleWidget::resetWidget() { this->allContactsListWidget->scrollToTop(); this->searchLineEdit->clear(); // Might also trigger filterLists() this->searchLineEdit->setFocus(); } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// SLOTS ////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// /* * Filter the list of all contacts based on what * the user entered in the search box * */ void PeopleWidget::filterList(QString searchTerms) { allContactsListWidget->clear(); allContactsListWidget->addItems(allContactsStringList.filter(searchTerms, Qt::CaseInsensitive)); } /* * Update the list of all contacts (actually, 'following') * from the PumpController * */ void PeopleWidget::updateAllContactsList(QString listType, QVariantList contactsVariantList, int totalReceivedCount) { if (listType != "following") { return; } qDebug() << "PeopleWidget: received list of Following; updating..."; if (totalReceivedCount == 200) // Only on first batch { allContactsStringList.clear(); } QString singleContactString; foreach (QVariant contact, contactsVariantList) { singleContactString = contact.toMap().value("displayName").toString(); singleContactString.append(" <"); singleContactString.append(contact.toMap().value("id").toString().remove(0,5)); singleContactString.append(">"); allContactsStringList.append(singleContactString); } // Populate the list widget, using whatever filter is set this->filterList(searchLineEdit->text()); } /* * Send current contact string in a SIGNAL * * Used when selecting a row and clicking the "add" button * */ void PeopleWidget::returnContact() { QString currentContact; if (this->allContactsListWidget->currentRow() != -1) { currentContact = allContactsListWidget->currentItem()->data(0).toString(); } emit addButtonPressed(currentContact); } /* * Used when clicking a contact * */ void PeopleWidget::returnClickedContact(QModelIndex modelIndex) { emit contactSelected(modelIndex.data().toString()); } dianara-v1.1/translations/000755 000764 000764 00000000000 12264101213 015243 5ustar00janjan000000 000000 dianara-v1.1/translations/dianara_es.ts000644 000764 000764 00000302763 12264101454 017723 0ustar00janjan000000 000000 ASActivity Public Público AccountDialog Your Pump.io address: Tu dirección pump.io: Webfinger ID, like username@pumpserver.org ID Webfinger, tipo usuario@servidorpump.org Your address, as username@server Tu dirección, como usuario@servidor Get &Verifier Code Obtener código de &verificación Verifier code: Código de verificación: &Save Details &Guardar datos If the browser doesn't open automatically, copy this address manually Si el navegador web no se abre automáticamente, copia esta dirección manualmente Account Configuration Configuración de la cuenta First, enter your Webfinger ID, your pump.io address. Primero, introduce tu identificador Webfinger, tu dirección pump.io. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. Tu dirección es como usuario@servidorpump.org, y puedes encontrarla en tu perfil, en la interfaz web. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example Si tu perfil está en https://pump.ejemplo/tunombre, entonces tu dirección es tunombre@pump.ejemplo After clicking this button, a web browser will open, requesting authorization for Dianara Cuando pulses este botón, se abrirá un navegador web, solicitando autorización para Dianara Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. Don't translate the VERIFIER word! Una vez que hayas autorizado a Dianara desde la interfaz web de tu servidor Pump, recibirás un código llamado VERIFIER. Cópialo y pégalo en el campo de abajo. Enter the verifier code provided by your Pump server here Introduce aquí el codigo de verificación proporcionado por tu servidor Pump Paste the verifier here Pega el verificador aquí &Authorize Application &Autorizar la aplicación &Cancel &Cancelar A web browser will start now, where you can get the verifier code Ahora se iniciará un navegador web, donde podrás obtener el código de verificación Your Pump address is invalid Tu dirección Pump no es válida Verifier code is empty El código de verificación está vacío Dianara is authorized to access your data Dianara tiene autorización para acceder a tu cuenta AudienceSelector 'To' List Lista 'Para' 'CC' List Lista 'CC' &Search: &Buscar: Enter a name here to search for it Escribe aquí un nombre para buscarlo &Add to Selected &Añadir a seleccionados All Contacts Todos los contactos Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below. Selecciona gente de la lista de la izquierda. Puedes arrastrarlos con el ratón, hacer clic o doble clic en ellos, o seleccionarlos y usar el botón de abajo. Clear &List Borrar &lista &Done &Hecho &Cancel &Cancelar Selected People Gente seleccionada Comment Like or unlike this comment Decir que te gusta o que ya no te gusta este comentario Quote This is a verb, infinitive Citar Reply quoting this comment Responder citando este comentario Delete Eliminar Delete this comment Eliminar este comentario Unlike Ya no me gusta Like Me gusta %1 like this comment Plural: %1=list of people like John, Jane, Smith A %1 les gusta este comentario %1 likes this comment Singular: %1=name of just 1 person A %1 le gusta este comentario WARNING: Delete comment? ADVERTENCIA: ¿Eliminar comentario? Are you sure you want to delete this comment? ¿Estás seguro de que quieres eliminar este comentario? &Yes, delete it &Sí, borrarlo &No &No CommenterBlock Show All Comments Mostrar todos los comentarios &Comment &Comentar You can press Control+Enter to send the comment with the keyboard Puedes pulsar Control+Enter para enviar el comentario con el teclado C&ancel C&ancelar Press ESC to cancel the comment if there is no text Pulsa ESC para cancelar el comentario si no hay texto Posting comment failed. Try again. Ha fallado la publicación del comentario. Prueba de nuevo. Sending comment... Enviando comentario... Comment is empty. El comentario está vacío. Composer Bold Negrita Italic Cursiva Make a link Hacer un enlace Click here or press Control+N to post a note... Haz clic aquí o pulsa Control+N para publicar una nota... Type a message here to post it Escribe un mensaje aquí para publicarlo Normal Normal Symbols Símbolos Formatting Formato Underline Subrayado Strikethrough Tachado Header Título Preformatted block Bloque preformateado Quote block Bloque de cita Insert an image from a web site Insertar una imagen desde un sitio web Insert line Insertar línea Cancel Post Cancelar el mensaje &F F for formatting, translate accordingly &F Text Formatting Options Opciones de formato de texto Paste Text Without Formatting Pegar texto sin formato Insert an image from a URL Insertar una imagen desde una URL Error: Invalid URL Error: URL no válida The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// La dirección que has introducido (%1) no es válida. Las direcciones de imagenes han de empezar por http:// o https:// Cancel message? ¿Cancelar el mensaje? Are you sure you want to cancel this message? ¿Seguro que quieres cancelar este mensaje? &Yes, cancel it &Sí, cancelarlo &No &No Insert a link Insertar un enlace &Format Button for text formatting and related options &Formato Type a comment here Escribe un comentario aquí Type or paste a web address here. Escribe o pega una dirección web aquí. Make a link from selected text Hacer un link con el texto seleccionado Type or paste a web address here. The selected text (%1) will be converted to a link. Escribe o pega una dirección web aquí. El texto seleccionado (%1) será convertido en un enlace. Type or paste the image address here. Escribe o pega la dirección de la imagen aquí. ConfigDialog minutes minutos posts mensajes Top Parte superior Bottom Parte inferior Left side Lado izquierdo Right side Lado derecho Buttons below Botones debajo Buttons around Botones alrededor Buttons on right side Botones en el lado derecho Restart program to apply this change Reinicia el programa para aplicar este cambio Program Configuration Configuración del programa Timeline &update interval Intervalo de &actualización de la línea temporal &Posts per page &Mensajes por página &Tabs position Posición de las &pestañas &Movable tabs Pes&tañas movibles C&omposer type Tipo de &editor posts Goes after a number, as: 25 posts mensajes &Posts per page, main timeline &Mensajes por página, línea temporal principal posts This goes after a number, like: 10 posts mensajes Posts per page, &other timelines Mensajes por página, &otras líneas temporales Public posts as &default Mensajes públicos por &defecto As system notifications Como notificaciones del sistema Using own notifications Usando notificaciones propias Don't show notifications No mostrar notificaciones Show &notifications Mostrar &notificaciones Dianara stores data in this folder: Dianara guarda datos en esta carpeta: &Save Configuration &Guardar configuración &Cancel &Cancelar ContactCard Hometown Ciudad Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name Bio de %1 This user doesn't have a biography Este usuario no tiene una biografía No biography for %1 %1=contact name No hay biografía para %1 Open Profile in Web Browser Abrir el perfil en el navegador web In Lists... En listas... User Options Opciones de usuario Follow Seguir Stop Following Dejar de seguir Stop following? ¿Dejar de seguir? Are you sure you want to stop following %1? ¿Estás seguro de que quieres dejar de seguir a %1? &Yes, stop following &Sí, dejar de seguir &No &No ContactList &Follow &Seguir Reload Following Actualizar Siguiendo Reload Followers Actualizar Seguidores Export Following Exportar Siguiendo Export Followers Exportar Seguidores username@server.org or https://server.org/username usuario@servidor.org o https://servidor.org/usuario &Enter address to follow: &Introduce dirección para seguir: Optio&ns Opcio&nes Followin&g &Siguiendo Follo&wers Se&guidores Reload Lists Actualizar listas &Lists &Listas Export list of 'following' to a file Exportar lista de 'siguiendo' a un archivo Export list of 'followers' to a file Exportar lista de 'seguidores' a un archivo FDNotifications Dianara Notification Notificación de Dianara FilterEditor Filter Editor Editor de filtros Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. Aquí puedes establecer algunos filtros. Puedes filtrar por contenido, autor o aplicación. Por ejemplo, puedes filtrar mensajes publicados por la aplicación Open Farm Game, o que contienen la palabra NSFW en el mensaje. At the moment, this filters only apply to the Meanwhile feed. De momento, estos filtros solo se aplican a la línea temporal "Mientras tanto". Content Contenido Author Autor Application Aplicación Words to match Palabras para comparar At the moment, these filters only apply to the Meanwhile feed. De momento, estos filtros solo se aplican a la línea temporal "Mientras tanto". Keywords... Palabras clave... &Add Filter &Añadir filtro Filters in use Filtros en uso &Remove Selected Filter &Eliminar filtro seleccionado &Save Filters &Guardar filtros &Cancel &Cancelar &New Filter &Nuevo filtro C&urrent Filters Filtros &actuales ImageViewer Image Imagen ESC to close, secondary-click for options ESC para cerrar, clic secundario para opciones Save Image... Guardar imagen... Close Viewer Cerrar visor Save Image As... Guardar imagen como... Image files Archivos de imagen All files Todos los archivos Error saving image Error guardando la imagen There was a problem while saving %1. Filename should end in .jpg or .png extensions. Ha habido un problema al guardar %1. El nombre del archivo debería acabar con la extensión .jpg o .png. ListsManager Name Nombre Members Miembros Add Mem&ber Añadir miem&bro &Remove Member &Eliminar miembro &Delete Selected List &Borrar lista seleccionada Add New &List Añadir nueva &lista Create L&ist Crear l&ista &Add to List &Añadir a lista Are you sure you want to delete %1? 1=Name of a person list ¿Estás seguro de que quieres eliminar %1? Remove person from list? ¿Quitar persona de la lista? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list ¿Estás seguro de que quieres quitar a %1 de la lista %2? &Yes &Sí Delete Selected List Borrar lista seleccionada Add New List Añadir nueva lista Type a name for the new list... Escribe un nombre para la nueva lista... Type an optional description here Escribe una descripción opcional aquí Create List Crear lista WARNING: Delete list? ADVERTENCIA: ¿Eliminar lista? Are you sure you want to delete this list? ¿Estás seguro de que quieres eliminar esta lista? &Yes, delete it &Sí, borrarla &No &No MainWindow &Messages &Mensajes &Contacts &Contactos &Quit &Salir &Session &Sesión Meanwhile... Mientras tanto... Side &Panel Panel &lateral Status &Bar &Barra de estado &Timeline Línea &temporal &Activity &Actividad Initializing... Inicializando... Your account is not configured yet. Tu cuenta no está configurada todavía. Mark All as Read Marcar todo como leído &Post a Note &Publicar una nota &View &Ver Full &Screen &Pantalla completa S&ettings &Configuración Edit &Profile Editar &perfil &Account &Cuenta &Filters &Filtros &Configure Dianara &Configurar Dianara &Help Ay&uda Visit &Website Visitar sitio &web &Frequently Asked Questions about Pump.io Preguntas &frecuentes sobre pump.io About &Dianara Acerca de &Dianara &Show Window &Mostrar ventana Dianara Notification Notificación de Dianara There is 1 new post. Hay 1 mensaje nuevo. There are %1 new posts. Hay %1 mensajes nuevos. Timeline updated. Línea temporal actualizada. Timeline updated at %1. Línea temporal actualizada a las %1. Fav&orites Fa&voritos Your Pump.io account is not configured Tu cuenta Pump.io no está configurada Dianara is a pump.io social networking client. Dianara es un cliente de red social para pump.io. With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. Con Dianara puedes ver tus líneas temporales, crear nuevos mensajes, subir fotos, interactuar con los mensajes, gestionar tus contactos y seguir gente nueva. Thanks to all the testers, translators and packagers, who help make Dianara better! ¡Gracias a todos los 'testers', traductores y empaquetadores, que ayudan a hacer Dianara mejor! English translation by JanKusanagi. TRANSLATORS: Change this with your language and name ;) Traducción al castellano por JanKusanagi. Dianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) Dianara está licenciado bajo la licencia GNU GPL, y utiliza algunos iconos Oxygen: http://www.oxygen-icons.org/ (Licencia LGPL) The main timeline La línea temporal principal Your own posts Tus propios mensajes Your favorited posts Los mensajes que te gustan Messages sent explicitly to you Mensajes enviados explícitamente a ti The people you follow, and the ones who follow you La gente a la que sigues, y la que te sigue &Update Main Timeline Act&ualizar línea temporal principal Update &Messages Timeline Actualizar línea temporal de &mensajes Update &Activity Timeline Actualizar línea temporal de &actividad Update Favorites &Timeline Actualizar línea temporal de &favoritos Update Minor &Feed meh... Actualizar línea temporal meno&r Update All Timelines Actualizar todas las líneas temporales Timeline updated. No new posts. Línea temporal actualizada. No hay mensajes nuevos. About Dianara Acerca de Dianara MinorFeed Get More Obtener más Get older activities Obtener actividades anteriores There are no activities to show yet. Aún no hay actividades para mostrar. MinorFeedItem using %1 Application used to generate this activity usando %1 Open referenced post Abrir el mensaje referenciado MiscHelpers bytes bytes PeopleWidget &Search: &Buscar: Enter a name here to search for it Escribe aquí un nombre para buscarlo Add a contact to a list Añadir un contacto a una lista &Cancel &Cancelar Post Like this post Decir que te gusta este mensaje Share this post Compartir este mensaje Hometown Ciudad Like Me gusta Comment Comentar Post noun, not verb Mensaje Shared on %1 Compartido el %1 using %1 usando %1 using %1 1=Program used for posting usando %1 Edited on %1 Editado el %1 In En To Para CC CC 1 like Le gusta a 1 1 comment 1 comentario Shared %1 times Compartido %1 veces Share Compartir Note Nota Image Imagen Other As in: other type of post Otro Post Noun, not verb Mensaje Edit Editar Edit this post Editar este mensaje %1 likes Les gusta a %1 %1 comments %1 comentarios Delete Eliminar Post Publicar Via %1 Meh... A través de %1 Edited: %1 Editado: %1 Posted on %1 1=Date Publicado el %1 Posted on %1 using %2 1=Date of post, 2=Program used for posting Publicado el %1 usando %2 Comment on this post. Comentar en este mensaje. If you select some text, it will be quoted. Si seleccionas parte del texto, será citado. Unshare Dejar de compartir Unshare this post Dejar de compartir este mensaje Delete this post Eliminar este mensaje Open %1's profile in web browser Abrir el perfil de %1 en el navegador web Open post in web browser Abrir el mensaje en el navegador web Copy post link to clipboard Copiar el enlace del mensaje al portapapeles Normalize text colors Normalizar colores del texto Stop following Dejar de seguir Follow Seguir Image is loading... La imagen está cargando... %1 likes this One person A %1 le gusta %1 like this More than one person A %1 les gusta %1 shared this %1 = One person name %1 ha compartido esto %1 shared this %1 = Names for more than one person %1 han compartido esto Shared once Compartido una vez You like this Te gusta esto Unlike Ya no me gusta Share post? ¿Compartir mensaje? Do you want to share %1's post? ¿Quieres compartir el mensaje de %1? &Yes, share it &Sí, compartirlo &No &No Unshare post? ¿Dejar de compartir el mensaje? Do you want to unshare %1's post? ¿Quieres dejar de compartir el mensaje de %1? &Yes, unshare it &Sí,dejar de compartirlo WARNING: Delete post? ADVERTENCIA: ¿Eliminar mensaje? Link to: %1 Enlace a: %1 Stop following? ¿Dejar de seguir? Are you sure you want to stop following %1? ¿Estás seguro de que quieres dejar de seguir a %1? &Yes, stop following &Sí, dejar de seguir Are you sure you want to delete this post? ¿Estás seguro de que quieres eliminar este mensaje? &Yes, delete it &Sí, borrarlo Click the image to see it in full size Haz clic en la imagen para verla en tamaño completo ProfileEditor Profile Editor Editor de perfil This is your Pump address Esta es tu dirección Pump This is the e-mail address associated with your account, for things such as notifications and password recovery Esta es la dirección de e-mail asociada con tu cuenta, para cosas como notificaciones y recuperación de la contraseña Change &Avatar... Cambiar &avatar... This is your visible name Este es tu nombre visible &Save Profile &Guardar perfil &Cancel &Cancelar Webfinger ID ID Webfinger E-mail E-mail Avatar Avatar Full &Name &Nombre completo &Hometown Ciuda&d &Bio &Bio Not set In reference to the e-mail not being set for the account Sin definir Select avatar image Selecciona imagen de avatar Image files Archivos de imagen All files Todos los archivos Invalid image Imagen no válida The selected image is not valid. La imagen seleccionada no es válida. Publisher Public Público Followers Seguidores Select Picture... Seleccionar foto... Title Título Title for the post. Setting a title helps make the Meanwhile feed more informative. Título para el mensaje. Añadir un título ayuda a hacer el contenido del "Mientras tanto" más informativo. Find the picture in your folders Encontrar la foto en tus carpetas People... Personas... Select who will see this post Selecciona quien verá este mensaje To... Para... Title: Título: Optional title for the post Título opcional para el mensaje Title for the post Título para el mensaje Lists Listas CC... CC... Select who will get a copy of this post Selecciona quien recibirá una copia de este mensaje Ad&d Picture &Añadir foto Upload photo Subir foto Post Publicar Cancel Cancelar Cancel the post Cancelar el mensaje Picture not set Foto no seleccionada Error: Already composing Error: Ya se está redactando You can't edit a post at this time, because a post is already being composed. No puedes editar un mensaje en este momento, porque ya se está redactando un mensaje. Update Actualizar Editing post Editando mensaje Posting failed. Try again. Ha fallado la publicación. Prueba de nuevo. Updating... Actualizando... Post is empty. El mensaje está vacío. Select one image Selecciona una imagen Image files Archivos de imagen All files Todos los archivos Resolution Resolución Size Tamaño Invalid image Imagen no válida The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. No se puede detectar el formato de la imagen. La extensión podría estar equivocada, como una imagen GIF renombrada a imagen.jpg o similar. Posting... Publicando... Hit Control+Enter to post with the keyboard Pulsa Control+Enter para publicar con el teclado PumpController Getting likes... meh... Recibiendo "likes"... Getting comments... Recibiendo comentarios... Getting minor feed... meh... Recibiendo línea temporal menor... Error connecting to %1 Error conectando a %1 Unhandled HTTP error code %1 Código de error HTTP no gestionado: %1 Post published successfully. Mensaje publicado correctamente. Comment posted successfully. Comentario publicado correctamente. Post shared successfully. Mensaje compartido correctamente. Minor feed received. meh Línea temporal menor recibida. Following successfully. Siguiendo correctamente. Stopped following successfully. Se ha dejado de seguir correctamente. List of 'following' completely received. Lista de 'siguiendo' completamente recibida. Partial list of 'following' received. Parte de la lista de 'siguiendo' recibida. List of 'followers' completely received. Lista de 'seguidores' completamente recibida. Partial list of 'followers' received. Parte de la lista de 'seguidores' recibida. Person list received. Lista de personas recibida. Person added to list successfully. Persona añadida a lista correctamente. Person removed from list successfully. Persona eliminada de la lista correctamente. File uploaded successfully. Posting message... Archivo subido correctamente. Publicando mensaje... Timeline received. Updating post list... Línea temporal recibida. Actualizando lista de mensajes... Getting list of 'Following'... Recibiendo lista de 'Siguiendo'... Getting list of 'Followers'... Recibiendo lista de 'Seguidores'... Getting list of person lists... Recibiendo lista de listas de personas... Creating person list... Creando lista de personas... Deleting person list... Borrando lista de personas... Getting a person list... Recibiendo una lista de personas... Adding person to list... Añadiendo una persona a una lista... Removing person from list... Quitando a una persona de una lista... Main timeline update requested, but updates are blocked. Actualización de línea temporal principal solicitada, pero las actualizaciones están bloqueadas. Getting main timeline... Recibiendo línea temporal principal... Direct timeline update requested, but updates are blocked. Actualización de línea temporal directa solicitada, pero las actualizaciones están bloqueadas. Getting direct messages timeline... Recibiendo línea temporal de mensajes directos... Activity timeline update requested, but updates are blocked. Actualización de línea temporal de actividad solicitada, pero las actualizaciones están bloqueadas. Getting activity timeline... Recibiendo línea temporal de actividad... Favorites timeline update requested, but updates are blocked. Actualización de línea temporal de favoritos solicitada, pero las actualizaciones están bloqueadas. Getting favorites timeline... Recibiendo línea temporal de favoritos... Service Unavailable HTTP 503 error string Servicio no disponible Internal Server Error HTTP 500 error string Error interno Gone HTTP 410 error string Ya no disponible Not Found HTTP 404 error string No encontrado Forbidden HTTP 403 error string Prohibido Unauthorized HTTP 401 error string No autorizado Bad Request HTTP 400 error string Solicitud incorrecta Moved Temporarily HTTP 302 error string Movido temporalmente Moved Permanently HTTP 301 error string Movido permanentemente Profile received. Perfil recibido. Profile updated. Perfil actualizado. Avatar published successfully. Avatar publicado correctamente. Post updated successfully. Mensaje actualizado correctamente. Message liked or unliked successfully. Mensaje marcado o desmarcado "Me gusta" correctamente. Likes received. meh... "Me gusta" recibidos. Comments received. Comentarios recibidos. Message deleted successfully. Mensaje eliminado correctamente. List of 'following' received. Lista de 'siguiendo' recibida. List of 'followers' received. Lista de 'seguidores' recibida. List of 'lists' received. Lista de 'listas' recibida. Person list created successfully. Lista de personas creada correctamente. Person list deleted successfully. Lista de personas borrada correctamente. Avatar uploaded. Avatar subido. Ready. Preparado. TimeLine Welcome to Dianara Bienvenido a Dianara Dianara is a <b>pump.io</b> client. Dianara es un cliente <b>pump.io</b>. If you don't have a Pump account yet, you can get one at the following address: Si aún no tienes una cuenta Pump, puedes conseguir una en la siguiente dirección: First, configure your account from the <b>Settings - Account</b> menu. En primer lugar, configura tu cuenta desde el menú <b>Configuración - Cuenta</b>. After the process is done, your profile and timelines should update automatically. Cuando el proceso esté listo, tu perfil y líneas temporales deberían de actualizarse automáticamente. Take a moment to look around the menus and the Configuration window. Tómate un momento para echar un vistazo por los menús y la ventana de Configuración. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. También puedes rellenar tu información de perfil y foto desde el menú <b>Configuración - Editar perfil</b>. There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. Hay descripciones emergentes (tooltips) por todas partes, por lo que si pasas sobre un botón o un campo de texto con el ratón, es probable que veas alguna información adicional. Dianara's blog Blog de Dianara Frequently asked questions about pump.io Preguntas frecuentes sobre pump.io Direct Messages Timeline Línea temporal de mensajes directos Here, you'll see posts specifically directed to you. Aquí verás los mensajes dirigidos específicamente a tí. Activity Timeline Línea temporal de actividad You'll see your own posts here. Aquí verás tus propios mensajes. Favorites Timeline Línea temporal de favoritos Posts and comments you've liked. Mensajes y comentarios que te han gustado. F&irst Page &Primera página &Previous Page Página &anterior &Next Page Página &siguiente Public Público Timestamp Invalid timestamp! ¡Hora/fecha no válida! Less than a minute ago Hace menos de un minuto A minute ago Hace un minuto %1 minutes ago Hace %1 minutos An hour ago Hace una hora %1 hours ago Hace %1 horas A day ago Hace un día %1 days ago Hace %1 días A month ago Hace un mes %1 months ago Hace %1 meses A year ago Hace un año %1 years ago Hace %1 años dianara-v1.1/translations/dianara_ca.ts000644 000764 000764 00000303160 12264101454 017667 0ustar00janjan000000 000000 ASActivity Public Públic AccountDialog Your Pump.io address: La teva adreça pump.io: Webfinger ID, like username@pumpserver.org ID Webfinger, tipus usuari@servidorpump.org Your address, as username@server La teva adreça, com usuari@servidor Get &Verifier Code Obtenir codi de &verificació Verifier code: Codi de verificació: &Save Details &Guardar dades If the browser doesn't open automatically, copy this address manually Si el navegador web no s'obre automàticament, copia aquesta adreça manualment Account Configuration Configuració del compte First, enter your Webfinger ID, your pump.io address. Primer, introdueix el teu identificador Webfinger, la teva adreça pump.io. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. La teva adreça es com usuari@servidorpump.org, i pots trobar-la al teu perfil, a la interfície web. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example Si el teu perfil es troba a https://pump.exemple/elteunom, llavors la teva adreça es elteunom@pump.exemple After clicking this button, a web browser will open, requesting authorization for Dianara Quan premis aquest botó, s'obrirà un navegador web, demanant autorització per Dianara Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. Don't translate the VERIFIER word! Un cop que hagis autoritzat a Dianara des de la interfície web del teu servidor Pump, rebràs un codi anomenat VERIFIER. Copia'l i enganxa'l al camp de sota. Enter the verifier code provided by your Pump server here Introdueix aquí el codi de verificació proporcionat pel teu servidor Pump Paste the verifier here Enganxa el verificador aquí &Authorize Application &Autoritzar l'aplicació &Cancel &Cancel·lar A web browser will start now, where you can get the verifier code Ara s'iniciarà un navegador web, on podràs obtenir el codi de verificació Your Pump address is invalid La teva adreça Pump no es vàlida Verifier code is empty El codi de verificació està buit Dianara is authorized to access your data Dianara te autorització per accedir al teu compte AudienceSelector 'To' List Llista 'Per a' 'CC' List Llista 'CC' &Search: Ce&rcar: Enter a name here to search for it Escriu aquí un nom per buscar-ho &Add to Selected &Afegir a seleccionats All Contacts Tots els contactes Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below. Selecciona gent de la llista de l'esquerra. Pots arrossegar-los amb el ratolí, fer clic o doble clic en ells, o seleccionar-los i fer servir el botó de sota. Clear &List Esborrar &llista &Done &Fet &Cancel &Cancel·lar Selected People Gent seleccionada Comment Like or unlike this comment Dir que t'agrada o que ja no t'agrada aquest comentari Quote This is a verb, infinitive Citar Reply quoting this comment Respondre citant aquest comentari Delete Eliminar Delete this comment Eliminar aquest comentari Unlike Ja no m'agrada Like M'agrada %1 like this comment Plural: %1=list of people like John, Jane, Smith A %1 els hi agrada aquest comentari %1 likes this comment Singular: %1=name of just 1 person A %1 li agrada aquest comentari WARNING: Delete comment? AVÍS: Eliminar comentari? Are you sure you want to delete this comment? Estàs segur de que vols eliminar aquest comentari? &Yes, delete it &Sí, eliminar-ho &No &No CommenterBlock Show All Comments Mostrar tots els comentaris &Comment &Comentar You can press Control+Enter to send the comment with the keyboard Pots premer Control+Enter per enviar el comentari amb el teclat C&ancel C&ancel·lar Press ESC to cancel the comment if there is no text Prem ESC per cancel·lar el comentari si no hi ha text Posting comment failed. Try again. Ha fallat la publicació del comentari. Torna-ho a provar. Sending comment... Enviant comentari... Comment is empty. El comentari està buit. Composer Bold Negreta Italic Cursiva Make a link Fer un enllaç Click here or press Control+N to post a note... Fes clic aquí o prem Control+N per publicar una nota... Type a message here to post it Escriu un missatge aquí per publicar-ho Normal Normal Symbols Símbols Formatting Format Underline Subratllat Strikethrough Barrat Header Títol Preformatted block Bloc preformatat Quote block Bloc de cita Insert an image from a web site Inserir una imatge des d'un lloc web Insert line Inserir línia Cancel Post Cancel·lar el missatge &F F for formatting, translate accordingly &F Text Formatting Options Opcions de format de text Paste Text Without Formatting Enganxar text sense format Insert an image from a URL Inserir una imatge des d'una URL Error: Invalid URL Error: URL no vàlida The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// L'adreça que has introduit (%1) no es vàlida. Les adreces d'imatges han de començar amb http:// o https:// Cancel message? Cancel·lar el missatge? Are you sure you want to cancel this message? Segur que vols cancel·lar aquest missatge? &Yes, cancel it &Sí, cancel·lar-ho &No &No Insert a link Inserir un enllaç &Format Button for text formatting and related options &Format Type a comment here Escriu un comentari aquí Type or paste a web address here. Escriu o enganxa una adreça web aquí. Make a link from selected text Fer un link del text seleccionat Type or paste a web address here. The selected text (%1) will be converted to a link. Escriu o enganxa una adreça web aquí. El text seleccionat (%1) serà convertit en un enllaç. Type or paste the image address here. Escriu o enganxa l'adreça de la imatge aquí. ConfigDialog minutes minuts posts missatges Top Part superior Bottom Part inferior Left side Costat esquerre Right side Costat dret Buttons below Botons a sota Buttons around Botons al voltant Buttons on right side Botons al costat dret Restart program to apply this change Reinicia el programa per aplicar aquest canvi Program Configuration Configuració del programa Timeline &update interval Interval d'&actualització de la línia temporal &Posts per page &Missatges per pàgina &Tabs position Posició de les &pestanyes &Movable tabs Pes&tanyes mòbils C&omposer type Tipus d'&editor posts Goes after a number, as: 25 posts missatges &Posts per page, main timeline &Missatges per pàgina, línia temporal principal posts This goes after a number, like: 10 posts missatges Posts per page, &other timelines Missatges per pàgina, &altres línies temporals Public posts as &default Missatges públics per &defecte As system notifications Com a notificacions del sistema Using own notifications Fent servir notificacions pròpies Don't show notifications No mostrar notificacions Show &notifications Mostrar &notificacions Dianara stores data in this folder: Dianara emmagatzema dades en aquesta carpeta: &Save Configuration &Guardar configuració &Cancel &Cancel·lar ContactCard Hometown Ciutat Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name Bio de %1 This user doesn't have a biography Aquest usuari no té una biografia No biography for %1 %1=contact name No hi ha biografia per %1 Open Profile in Web Browser Obrir el perfil al navegador web In Lists... En llistes... User Options Opcions d'usuari Follow Seguir Stop Following Deixar de seguir Stop following? Deixar de seguir? Are you sure you want to stop following %1? Estàs segur de que vols deixar de seguir a %1? &Yes, stop following &Sí, deixar de seguir &No &No ContactList &Follow &Seguir Reload Following Actualitzar Seguint Reload Followers Actualitzar Seguidors Export Following Exportar Seguint Export Followers Exportar Seguidors username@server.org or https://server.org/username usuari@servidor.org o https://servidor.org/usuari &Enter address to follow: &Introdueix adreça per seguir: Optio&ns Opcio&ns Followin&g &Seguint Follo&wers Se&guidors Reload Lists Actualitzar llistes &Lists &Llistes Export list of 'following' to a file Exportar llista de 'seguint' a un arxiu Export list of 'followers' to a file Exportar llista de 'seguidors' a un arxiu FDNotifications Dianara Notification Notificació de Dianara FilterEditor Filter Editor Editor de filtres Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. Aquí pots establir alguns filtres. Pots filtrar per contingut, autor o aplicació. Per exemple, pots filtrar missatges publicats per la aplicació Open Farm Game, o que contenen la paraula NSFW al missatge. At the moment, this filters only apply to the Meanwhile feed. De moment, aquests filtres només s'apliquen a la línia temporal "Mentrestant". Content Contingut Author Autor Application Aplicació Words to match Paraules per comparar At the moment, these filters only apply to the Meanwhile feed. De moment, aquests filtres només s'apliquen a la línia temporal "Mentrestant". Keywords... Paraules clau... &Add Filter &Afegir filtre Filters in use Filtres en ús &Remove Selected Filter &Eliminar filtre seleccionat &Save Filters &Guardar filtres &Cancel &Cancel·lar &New Filter &Nou filtre C&urrent Filters Filtres &actuals ImageViewer Image Imatge ESC to close, secondary-click for options ESC per tancar, clic secundari per opcions Save Image... Guardar imatge... Close Viewer Tancar visualitzador Save Image As... Guardar imatge com... Image files Arxius d'imatge All files Tots els arxius Error saving image Error guardant la imatge There was a problem while saving %1. Filename should end in .jpg or .png extensions. Hi ha hagut un problema al guardar %1. El nom de l'arxiu hauria d'acabar amb l'extensió .jpg o .png. ListsManager Name Nom Members Membres Add Mem&ber Afegir mem&bre &Remove Member &Eliminar membre &Delete Selected List &Esborrar llista seleccionada Add New &List Afegir nova &llista Create L&ist Crear ll&ista &Add to List &Afegir a llista Are you sure you want to delete %1? 1=Name of a person list Estàs segur de que vols eliminar %1? Remove person from list? Treure persona de la llista? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list Estàs segur de que vols treure a %1 de la llista %2? &Yes &Si Delete Selected List Esborrar llista seleccionada Add New List Afegir nova llista Type a name for the new list... Escriu un nom per la nova llista... Type an optional description here Escriu una descripció opcional aquí Create List Crear llista WARNING: Delete list? AVÍS: Eliminar llista? Are you sure you want to delete this list? Estàs segur de que vols eliminar aquesta llista? &Yes, delete it &Sí, esborrar-la &No &No MainWindow &Messages &Missatges &Contacts &Contactes &Quit &Sortir &Session &Sessió Meanwhile... Mentrestant... Side &Panel Cuadre &lateral Status &Bar &Barra d'estat &Timeline Línia &temporal &Activity &Activitat Initializing... Inicialitzant... Your account is not configured yet. El teu compte no està configurat encara. Mark All as Read Marcar tot com a llegit &Post a Note &Publicar una nota &View &Veure Full &Screen &Pantalla completa S&ettings &Configuració Edit &Profile Editar &perfil &Account &Compte &Filters &Filtres &Configure Dianara &Configurar Dianara &Help Aj&uda Visit &Website Visitar lloc &web &Frequently Asked Questions about Pump.io Preguntes &freqüents sobre pump.io About &Dianara Sobre &Dianara &Show Window &Mostrar finestra Dianara Notification Notificació de Dianara There is 1 new post. Hi ha 1 missatge nou. There are %1 new posts. Hi han %1 missatges nous. Timeline updated. Línia temporal actualitzada. Timeline updated at %1. Línia temporal actualitzada a les %1. Fav&orites Fa&vorits Your Pump.io account is not configured El teu compte Pump.io no està configurat Dianara is a pump.io social networking client. Dianara es un client de xarxa social per pump.io. With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. Amb Dianara pots veure les teves línies temporals, crear nous missatges, penjar fotos, interactuar amb els missatges, gestionar els teus contactes i seguir gent nova. Thanks to all the testers, translators and packagers, who help make Dianara better! Gràcies a tots els 'testers', traductors i empaquetadors, que ajuden a fer Dianara millor! English translation by JanKusanagi. TRANSLATORS: Change this with your language and name ;) Traducció al català per JanKusanagi. Dianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) Dianara es troba llicenciat sota la llicència GNU GPL, i utilitza algunes icones Oxygen: http://www.oxygen-icons.org/ (Llicència LGPL) The main timeline La línia temporal principal Your own posts Els teus propis missatges Your favorited posts Els missatges que t'agraden Messages sent explicitly to you Missatges enviats explícitament a tu The people you follow, and the ones who follow you La gent a la que segueixes, i la que et segueix &Update Main Timeline Act&ualitzar línia temporal principal Update &Messages Timeline Actualitzar línia temporal de &missatges Update &Activity Timeline Actualitzar línia temporal d'&activitat Update Favorites &Timeline Actualitzar línia temporal de &favorits Update Minor &Feed meh... Actualitzar línia temporal meno&r Update All Timelines Actualitzar totes les línies temporals Timeline updated. No new posts. Línia temporal actualitzada. No hi ha missatges nous. About Dianara Sobre Dianara MinorFeed Get More Obtenir més Get older activities Obtenir activitats anteriors There are no activities to show yet. Encara no hi ha activitats per mostrar. MinorFeedItem using %1 Application used to generate this activity utilitzant %1 Open referenced post Obrir el missatge referenciat MiscHelpers bytes bytes PeopleWidget &Search: Ce&rcar: Enter a name here to search for it Escriu aquí un nom per buscar-ho Add a contact to a list Afegir un contacte a una llista &Cancel &Cancel·lar Post Like this post Dir que t'agrada aquest missatge Share this post Compartir aquest missatge Hometown Ciutat Like M'agrada Comment Comentar Post noun, not verb Missatge Shared on %1 Compartit el %1 using %1 utilitzant %1 using %1 1=Program used for posting utilitzant %1 Edited on %1 Editat el %1 In A To Per a CC CC 1 like Li agrada a 1 1 comment 1 comentari Shared %1 times Compartit %1 cops Share Compartir Note Nota Image Imatge Other As in: other type of post Altre Post Noun, not verb Missatge Edit Editar Edit this post Editar aquest missatge %1 likes Li agrada a %1 %1 comments %1 comentaris Delete Eliminar Post Publicar Via %1 Meh... A través de %1 Edited: %1 Editat: %1 Posted on %1 1=Date Publicat el %1 Posted on %1 using %2 1=Date of post, 2=Program used for posting Publicat el %1 utilitzant %2 Comment on this post. Comentar en aquest missatge. If you select some text, it will be quoted. Si selecciones part del text, serà citat. Unshare Deixar de compartir Unshare this post Deixar de compartir aquest missatge Delete this post Eliminar aquest missatge Open %1's profile in web browser Obrir el perfil de %1 al navegador web Open post in web browser Obrir el missatge al navegador web Copy post link to clipboard portapapers? Copiar l'enllaç del missatge al porta-retalls Normalize text colors Normalitzar colors del text Stop following Deixar de seguir Follow Seguir Image is loading... La imatge està carregant... %1 likes this One person A %1 li agrada %1 like this More than one person A %1 els hi agrada %1 shared this %1 = One person name %1 ha compartit això %1 shared this %1 = Names for more than one person %1 han compartit això Shared once Compartit un cop You like this T'agrada això Unlike Ja no m'agrada Share post? Compartir missatge? Do you want to share %1's post? Vols compartir el missatge de %1? &Yes, share it &Sí, compartir-ho &No &No Unshare post? Deixar de compartir el missatge? Do you want to unshare %1's post? Vols deixar de compartir el missatge de %1? &Yes, unshare it &Sí, deixar de compartir-ho WARNING: Delete post? hmm... advertència? AVÍS: Eliminar missatge? Link to: %1 Enllaç a: %1 Stop following? Deixar de seguir? Are you sure you want to stop following %1? Estàs segur de que vols deixar de seguir a %1? &Yes, stop following &Sí, deixar de seguir Are you sure you want to delete this post? Estàs segur de que vols eliminar aquest missatge? &Yes, delete it &Sí, eliminar-ho Click the image to see it in full size Fes clic a la imatge per veure-la a mida completa ProfileEditor Profile Editor Editor de perfil This is your Pump address Aquesta es la teva adreça Pump This is the e-mail address associated with your account, for things such as notifications and password recovery Aquesta és l'adreça d'e-mail associada al teu compte, per coses com ara notificacions i recuperació de la contrasenya Change &Avatar... Canviar &avatar... This is your visible name Aquest es el teu nom visible &Save Profile &Guardar perfil &Cancel &Cancel·lar Webfinger ID ID Webfinger E-mail E-mail Avatar Avatar Full &Name &Nom complet &Hometown Ciuta&t &Bio &Bio Not set In reference to the e-mail not being set for the account Sense definir Select avatar image Selecciona imatge d'avatar Image files Arxius d'imatge All files Tots els arxius Invalid image Imatge no vàlida The selected image is not valid. La imatge seleccionad no es vàlida. Publisher Public Públic Followers Seguidors Select Picture... Seleccionar foto... Title Títol Title for the post. Setting a title helps make the Meanwhile feed more informative. Títol per al missatge. Afegir un títol ajuda a fer el contingut del "Mentrestant" més informatiu. Find the picture in your folders Trobar la foto a les teves carpetes People... Persones... Select who will see this post Selecciona qui veurà aquest missatge To... Per a... Title: Títol: Optional title for the post Títol opcional per al missatge Title for the post Títol per al missatge Lists Llistes CC... CC... Select who will get a copy of this post Selecciona qui rebrà una còpia d'aquest missatge Ad&d Picture &Afegir foto Upload photo Pujar foto Post Publicar Cancel Cancel·lar Cancel the post Cancel·lar el missatge Picture not set No s'ha escollit una foto Error: Already composing Error: Ja s'està redactant You can't edit a post at this time, because a post is already being composed. No pots editar un missatge en aquest moment, perque ja s'està redactant un missatge. Update Actualitzar Editing post Editant missatge Posting failed. Try again. Ha fallat la publicació. Torna-ho a provar. Updating... Actualitzant... Post is empty. El missatge està buit. Select one image Selecciona una imatge Image files Arxius d'imatge All files Tots els arxius Resolution Resolució Size Mida Invalid image Imatge no vàlida The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. No es pot detectar el format de la imatge. Potser l'extensió està equivocada, com una imatge GIF reanomenada a imatge.jpg o semblant. Posting... Publicant... Hit Control+Enter to post with the keyboard Prem Control+Enter per publicar amb el teclat PumpController Getting likes... meh.... Rebent "likes"... Getting comments... Rebent comentaris... Getting minor feed... meh... Rebent línia temporal menor... Error connecting to %1 Error connectant a %1 Unhandled HTTP error code %1 Codi d'error HTTP no gestionat: %1 Post published successfully. Missatge publicat correctament. Comment posted successfully. Comentari publicat correctament. Post shared successfully. Missatge compartit correctament. Minor feed received. meh... Línia temporal menor rebuda. Following successfully. Seguint correctament. Stopped following successfully. S'ha deixat de seguir correctament. List of 'following' completely received. Llista de 'seguint' completament rebuda. Partial list of 'following' received. Part de la llista de 'seguint' rebuda. List of 'followers' completely received. Llista de 'seguidors' completament rebuda. Partial list of 'followers' received. Part de la llista de 'seguidors' rebuda. Person list received. Llista de persones rebuda. Person added to list successfully. Persona afegida a llista correctament. Person removed from list successfully. Persona eliminada de la llista correctament. File uploaded successfully. Posting message... Arxiu penjat correctament. Publicant missatge... Timeline received. Updating post list... S'ha rebut la línia temporal. Actualitzant llista de missatges... Getting list of 'Following'... Rebent llista de 'Seguint'... Getting list of 'Followers'... Rebent llista de 'Seguidors'... Getting list of person lists... Rebent llista de llistes de persones... Creating person list... Creant llista de persones... Deleting person list... Esborrant llista de persones... Getting a person list... Rebent una llista de persones... Adding person to list... Afegint una persona a una llista... Removing person from list... Treient una persona d'una llista... Main timeline update requested, but updates are blocked. Actualització de línia temporal principal sol·licitada, però les actualitzacions estan bloquejades. Getting main timeline... Rebent línia temporal principal... Direct timeline update requested, but updates are blocked. Actualització de línia temporal directa sol·licitada, però les actualitzacions estan bloquejades. Getting direct messages timeline... Rebent línia temporal de missatges directes... Activity timeline update requested, but updates are blocked. Actualització de línia temporal d'activitat sol·licitada, però les actualitzacions estan bloquejades. Getting activity timeline... Rebent línia temporal d'activitat... Favorites timeline update requested, but updates are blocked. Actualització de línia temporal de favorits sol·licitada, però les actualitzacions estan bloquejades. Getting favorites timeline... Rebent línia temporal de favorits... Service Unavailable HTTP 503 error string Servei no disponible Internal Server Error HTTP 500 error string Error intern Gone HTTP 410 error string Ja no disponible Not Found HTTP 404 error string No trobat Forbidden HTTP 403 error string Prohibit Unauthorized HTTP 401 error string No autoritzat Bad Request HTTP 400 error string Sol·licitud incorrecta Moved Temporarily HTTP 302 error string Mogut temporalment Moved Permanently HTTP 301 error string Mogut permanentment Profile received. Perfil rebut. Profile updated. Perfil actualitzat. Avatar published successfully. Avatar publicat correctament. Post updated successfully. Missatge actualitzat correctament. Message liked or unliked successfully. Missatge marcat o desmarcat "M'agrada" correctament. Likes received. meh... "M'agrada" rebuts. Comments received. Comentaris rebuts. Message deleted successfully. Missatge eliminat correctament. List of 'following' received. Llista de 'seguint' rebuda. List of 'followers' received. Llista de 'seguidors' rebuda. List of 'lists' received. Llista de 'llistes' rebuda. Person list created successfully. Llista de persones creada correctament. Person list deleted successfully. Llista de persones esborrada correctament. Avatar uploaded. Avatar penjat. Ready. Preparat. TimeLine Welcome to Dianara Benvingut a Dianara Dianara is a <b>pump.io</b> client. Dianara es un client <b>pump.io</b>. If you don't have a Pump account yet, you can get one at the following address: Si encara no tens un compte Pump, pots aconseguir-ne un a la següent adreça: First, configure your account from the <b>Settings - Account</b> menu. En primer lloc, configura el teu compte des del menú <b>Configuració - Compte</b>. After the process is done, your profile and timelines should update automatically. Quan el procés estigui llest, el teu perfil i línies temporals haurien d'actualitzar-se automàticament. Take a moment to look around the menus and the Configuration window. Pren-te un moment per fer una ullada als menús i la finestra de Configuració. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. També pots omplir la teva informació de perfil i foto des del menú <b>Configuració - Editar perfil</b>. There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. Hi ha indicadors de funció (tooltips) per tot arreu, pel que si passes sobre un botó o un camp de text amb el ratolí, és probable que vegis alguna informació addicional. Dianara's blog Bloc de Dianara Frequently asked questions about pump.io Preguntes freqüents sobre pump.io Direct Messages Timeline Línia temporal de missatges directes Here, you'll see posts specifically directed to you. Aquí veuràs els missatges dirigits específicament a tu. Activity Timeline Línia temporal d'activitat You'll see your own posts here. Aquí veuràs els teus propis missatges. Favorites Timeline Línia temporal de favorits Posts and comments you've liked. Missatges i comentaris que t'han agradat. F&irst Page &Primera pàgina &Previous Page Pàgina &anterior &Next Page Pàgina &següent Public Públic Timestamp Invalid timestamp! Hora/data no vàlida! Less than a minute ago Fa menys d'un minut A minute ago Fa un minut %1 minutes ago Fa %1 minuts An hour ago Fa una hora %1 hours ago Fa %1 hores A day ago Fa un dia %1 days ago Fa %1 dies A month ago Fa un mes %1 months ago Fa %1 mesos A year ago Fa un any %1 years ago Fa %1 anys dianara-v1.1/translations/dianara_ca.qm000664 000764 000764 00000144555 12264101455 017674 0ustar00janjan000000 000000 yT%z>$(hNTc,G; .GI5ҥ!u]<#b.Ntf>2ݣxsDC\DIn-uM$h'RSlB>oc, ,'!>Nu%UB +Wx%1K&`^v!S9zJY^Zjtd 6<!Cƭ>ZϠN؞.%X1nV8Y GJ 8>$1s6p70i^J:JP_i68|sCf{x2z7zW.QP<=B5F[ÌIרB?9_If2nG?7ʳ0A 1tnZ4>g>BP%V&b`3fa$/r):1:j<7z<NE gIz'b=]+HI Q,,lS43g23goBvR;@;zZ7wWKF !@U%,4KT4qKTuORMRV|".fy{s={{{{+ V1WVhpz[LTsT: 7X6r\w600v+ƨI2wonf;^ ^!k+e-^4u{u4u9$<.yX<.2<.om=l2M?$ZPLf;!qp{r7y}Ha/Sb t],1MuMV4ґ2\ڑAڱ>>i.0t1;#4}<C<{*<L?;U\5N~kgNpi{k<@jL} o/ROJ138LxaҕpQiYQqmEf" #N:)2K"2?Ne8e79Hm.<snYZYeUw6'B>e2HrnťUtJ{.^琊 qFR3 -Q 1`#)& 7w hӾI r% |K( ~# 4`|  ` E d o b> ʞN & ~ [ Ad Ax[ ~\ o : !/H@ #K -E ?mC/ Hk TG4# TGuu d 4i& d:u d<k e>I q\ z3. ~ $  I I I( I;P Id Iy )o >pZ LG yo o M ^ '_mH t( 3G6 ֓w T< ؄ De 0m /^t ; c#  "e|W "p 0v 5<0 @u1 Q(n{ bX b{NB mmTh P/v ! eq V j5 ٩ U uO أt  C* 2V (=j - Uy ES{ Yvf ]ӡ^ `u- eBUbk {| F'  VN  #VO ͡{ =: <j tl *q w"] 3M aW ^6 ЕXE #pn G~ V bP g, h13C mCTM |r2 a b| Q* [VF2r+j+/zRwXJlAlyl5mhnc$G8T8TGT8Tg>Q7'F@U)vy4?z&A FTO~T*_} zbu.Rgjt\0u9(n!9/MM؞¹e%yD~<"-ڨdޝr/Gi PblicPublic ASActivity.&Autoritzar l'aplicaci&Authorize Application AccountDialog&Cancellar&Cancel AccountDialog&Guardar dades &Save Details AccountDialogAra s'iniciar un navegador web, on podrs obtenir el codi de verificaciAA web browser will start now, where you can get the verifier code AccountDialog.Configuraci del compteAccount Configuration AccountDialogQuan premis aquest bot, s'obrir un navegador web, demanant autoritzaci per DianaraYAfter clicking this button, a web browser will open, requesting authorization for Dianara AccountDialogbDianara te autoritzaci per accedir al teu compte)Dianara is authorized to access your data AccountDialogIntrodueix aqu el codi de verificaci proporcionat pel teu servidor Pump9Enter the verifier code provided by your Pump server here AccountDialogPrimer, introdueix el teu identificador Webfinger, la teva adrea pump.io.5First, enter your Webfinger ID, your pump.io address. AccountDialog8Obtenir codi de &verificaciGet &Verifier Code AccountDialogSi el navegador web no s'obre automticament, copia aquesta adrea manualmentEIf the browser doesn't open automatically, copy this address manually AccountDialogSi el teu perfil es troba a https://pump.exemple/elteunom, llavors la teva adrea es elteunom@pump.exemple_If your profile is at https://pump.example/yourname, then your address is yourname@pump.example AccountDialog8Un cop que hagis autoritzat a Dianara des de la interfcie web del teu servidor Pump, rebrs un codi anomenat VERIFIER. Copia'l i enganxa'l al camp de sota.Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. AccountDialog6Enganxa el verificador aquPaste the verifier here AccountDialog@El codi de verificaci est buitVerifier code is empty AccountDialog(Codi de verificaci:Verifier code: AccountDialogVID Webfinger, tipus usuari@servidorpump.org*Webfinger ID, like username@pumpserver.org AccountDialog@La teva adrea Pump no es vlidaYour Pump address is invalid AccountDialog.La teva adrea pump.io:Your Pump.io address: AccountDialogLa teva adrea es com usuari@servidorpump.org, i pots trobar-la al teu perfil, a la interfcie web.kYour address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. AccountDialogFLa teva adrea, com usuari@servidor Your address, as username@server AccountDialog,&Afegir a seleccionats&Add to SelectedAudienceSelector&Cancellar&CancelAudienceSelector&Fet&DoneAudienceSelectorLlista 'CC' 'CC' ListAudienceSelectorLlista 'Per a' 'To' ListAudienceSelector$Tots els contactes All ContactsAudienceSelector Esborrar &llista Clear &ListAudienceSelector:Selecciona gent de la llista de l'esquerra. Pots arrossegar-los amb el ratol, fer clic o doble clic en ells, o seleccionar-los i fer servir el bot de sota.Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below.AudienceSelector"Gent seleccionadaSelected PeopleAudienceSelectorFA %1 els hi agrada aquest comentari%1 like this commentComment>A %1 li agrada aquest comentari%1 likes this commentComment&No&NoComment &S, eliminar-ho&Yes, delete itCommentdEsts segur de que vols eliminar aquest comentari?-Are you sure you want to delete this comment?CommentEliminarDeleteComment2Eliminar aquest comentariDelete this commentCommentM'agradaLikeCommentlDir que t'agrada o que ja no t'agrada aquest comentariLike or unlike this commentComment CitarQuoteCommentBRespondre citant aquest comentariReply quoting this commentCommentJa no m'agradaUnlikeComment2AVS: Eliminar comentari?WARNING: Delete comment?Comment&Comentar&CommentCommenterBlockC&ancellarC&ancelCommenterBlock.El comentari est buit.Comment is empty.CommenterBlocktHa fallat la publicaci del comentari. Torna-ho a provar.#Posting comment failed. Try again.CommenterBlockjPrem ESC per cancellar el comentari si no hi ha text3Press ESC to cancel the comment if there is no textCommenterBlock(Enviant comentari...Sending comment...CommenterBlock6Mostrar tots els comentarisShow All CommentsCommenterBlock~Pots premer Control+Enter per enviar el comentari amb el teclatAYou can press Control+Enter to send the comment with the keyboardCommenterBlock&Format&FormatComposer&No&NoComposer$&S, cancellar-ho&Yes, cancel itComposerTSegur que vols cancellar aquest missatge?-Are you sure you want to cancel this message?ComposerNegretaBoldComposer.Cancellar el missatge?Cancel message?ComposernFes clic aqu o prem Control+N per publicar una nota.../Click here or press Control+N to post a note...Composer(Error: URL no vlidaError: Invalid URLComposer Format FormattingComposer TtolHeaderComposer"Inserir un enlla Insert a linkComposer@Inserir una imatge des d'una URLInsert an image from a URLComposerHInserir una imatge des d'un lloc webInsert an image from a web siteComposerInserir lnia Insert lineComposerCursivaItalicComposerFer un enlla Make a linkComposer@Fer un link del text seleccionatMake a link from selected textComposer NormalNormalComposer4Enganxar text sense formatPaste Text Without FormattingComposer Bloc preformatatPreformatted blockComposerBloc de cita Quote blockComposer Barrat StrikethroughComposerSmbolsSymbolsComposer2Opcions de format de textText Formatting OptionsComposerL'adrea que has introduit (%1) no es vlida. Les adreces d'imatges han de comenar amb http:// o https://`The address you entered (%1) is not valid. Image addresses should begin with http:// or https://Composer0Escriu un comentari aquType a comment hereComposerNEscriu un missatge aqu per publicar-hoType a message here to post itComposerJEscriu o enganxa una adrea web aqu."Type or paste a web address here. ComposerEscriu o enganxa una adrea web aqu. El text seleccionat (%1) ser convertit en un enlla.UType or paste a web address here. The selected text (%1) will be converted to a link.ComposerXEscriu o enganxa l'adrea de la imatge aqu.&Type or paste the image address here. ComposerSubratllat UnderlineComposer&Cancellar&Cancel ConfigDialog"Pes&tanyes mbils &Movable tabs ConfigDialog^&Missatges per pgina, lnia temporal principal&Posts per page, main timeline ConfigDialog*&Guardar configuraci&Save Configuration ConfigDialog2Posici de les &pestanyes&Tabs position ConfigDialog>Com a notificacions del sistemaAs system notifications ConfigDialogPart inferiorBottom ConfigDialogZDianara emmagatzema dades en aquesta carpeta:#Dianara stores data in this folder: ConfigDialog0No mostrar notificacionsDon't show notifications ConfigDialogCostat esquerre Left side ConfigDialog\Missatges per pgina, &altres lnies temporals Posts per page, &other timelines ConfigDialog2Configuraci del programaProgram Configuration ConfigDialog<Missatges pblics per &defectePublic posts as &default ConfigDialogCostat dret Right side ConfigDialog,Mostrar &notificacionsShow ¬ifications ConfigDialog\Interval d'&actualitzaci de la lnia temporalTimeline &update interval ConfigDialogPart superiorTop ConfigDialogBFent servir notificacions prpiesUsing own notifications ConfigDialog minutsminutes ConfigDialogmissatges!Goes after a number, as: 25 postsposts ConfigDialogmissatges(This goes after a number, like: 10 postsposts ConfigDialog&No&No ContactCard*&S, deixar de seguir&Yes, stop following ContactCard\Ests segur de que vols deixar de seguir a %1?+Are you sure you want to stop following %1? ContactCardBio de %1 Bio for %1 ContactCard SeguirFollow ContactCard CiutatHometown ContactCardEn llistes... In Lists... ContactCard2No hi ha biografia per %1No biography for %1 ContactCard@Obrir el perfil al navegador webOpen Profile in Web Browser ContactCard Deixar de seguirStop Following ContactCard"Deixar de seguir?Stop following? ContactCardBAquest usuari no t una biografia"This user doesn't have a biography ContactCard Opcions d'usuari User Options ContactCard<&Introdueix adrea per seguir:&Enter address to follow: ContactList&Seguir&Follow ContactList&Llistes&Lists ContactList$Exportar SeguidorsExport Followers ContactList Exportar SeguintExport Following ContactListRExportar llista de 'seguidors' a un arxiu$Export list of 'followers' to a file ContactListNExportar llista de 'seguint' a un arxiu$Export list of 'following' to a file ContactListSe&guidors Follo&wers ContactList&Seguint Followin&g ContactListOpcio&nsOptio&ns ContactList*Actualitzar SeguidorsReload Followers ContactList&Actualitzar SeguintReload Following ContactList&Actualitzar llistes Reload Lists ContactListbusuari@servidor.org o https://servidor.org/usuari2username@server.org or https://server.org/username ContactList,Notificaci de DianaraDianara NotificationFDNotifications&Afegir filtre &Add Filter FilterEditor&Cancellar&Cancel FilterEditor&Nou filtre &New Filter FilterEditor8&Eliminar filtre seleccionat&Remove Selected Filter FilterEditor &Guardar filtres &Save Filters FilterEditorAplicaci Application FilterEditorDe moment, aquests filtres noms s'apliquen a la lnia temporal "Mentrestant".>At the moment, these filters only apply to the Meanwhile feed. FilterEditor AutorAuthor FilterEditor Filtres &actualsC&urrent Filters FilterEditorContingutContent FilterEditor"Editor de filtres Filter Editor FilterEditorFiltres en sFilters in use FilterEditorAqu pots establir alguns filtres. Pots filtrar per contingut, autor o aplicaci. Per exemple, pots filtrar missatges publicats per la aplicaci Open Farm Game, o que contenen la paraula NSFW al missatge.Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. FilterEditor Paraules clau... Keywords... FilterEditorTots els arxius All files ImageViewer(Tancar visualitzador Close Viewer ImageViewerTESC per tancar, clic secundari per opcions)ESC to close, secondary-click for options ImageViewer0Error guardant la imatgeError saving image ImageViewer ImatgeImage ImageViewerArxius d'imatge Image files ImageViewer*Guardar imatge com...Save Image As... ImageViewer"Guardar imatge... Save Image... ImageViewerHi ha hagut un problema al guardar %1. El nom de l'arxiu hauria d'acabar amb l'extensi .jpg o .png.UThere was a problem while saving %1. Filename should end in .jpg or .png extensions. ImageViewer &Afegir a llista &Add to List ListsManager:&Esborrar llista seleccionada&Delete Selected List ListsManager&No&No ListsManager &Eliminar membre&Remove Member ListsManager&Si&Yes ListsManager &S, esborrar-la&Yes, delete it ListsManagerAfegir mem&bre Add Mem&ber ListsManager&Afegir nova &llista Add New &List ListsManagerHEsts segur de que vols eliminar %1?#Are you sure you want to delete %1? ListsManagerhEsts segur de que vols treure a %1 de la llista %2?4Are you sure you want to remove %1 from the %2 list? ListsManagerCrear ll&ista Create L&ist ListsManagerMembresMembers ListsManagerNomName ListsManager8Treure persona de la llista?Remove person from list? ListsManagerFEscriu un nom per la nova llista...Type a name for the new list... ListsManagerFEscriu una descripci opcional aqu!Type an optional description here ListsManager,AVS: Eliminar llista?WARNING: Delete list? ListsManager&Compte&Account MainWindow&Activitat &Activity MainWindow&&Configurar Dianara&Configure Dianara MainWindow&Contactes &Contacts MainWindow&Filtres&Filters MainWindowDPreguntes &freqents sobre pump.io)&Frequently Asked Questions about Pump.io MainWindow Aj&uda&Help MainWindow&Missatges &Messages MainWindow$&Publicar una nota &Post a Note MainWindow&Sortir&Quit MainWindow&Sessi&Session MainWindow"&Mostrar finestra &Show Window MainWindowLnia &temporal &Timeline MainWindowJAct&ualitzar lnia temporal principal&Update Main Timeline MainWindow &Veure&View MainWindowSobre &DianaraAbout &Dianara MainWindowSobre Dianara About Dianara MainWindow,Notificaci de DianaraDianara Notification MainWindowbDianara es un client de xarxa social per pump.io..Dianara is a pump.io social networking client. MainWindow Dianara es troba llicenciat sota la llicncia GNU GPL, i utilitza algunes icones Oxygen: http://www.oxygen-icons.org/ (Llicncia LGPL)wDianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) MainWindowEditar &perfil Edit &Profile MainWindowHTraducci al catal per JanKusanagi.#English translation by JanKusanagi. MainWindowFa&vorits Fav&orites MainWindow$&Pantalla completa Full &Screen MainWindow Inicialitzant...Initializing... MainWindow.Marcar tot com a llegitMark All as Read MainWindowMentrestant... Meanwhile... MainWindowHMissatges enviats explcitament a tuMessages sent explicitly to you MainWindow&Configuraci S&ettings MainWindowCuadre &lateral Side &Panel MainWindow&Barra d'estat Status &Bar MainWindowGrcies a tots els 'testers', traductors i empaquetadors, que ajuden a fer Dianara millor!SThanks to all the testers, translators and packagers, who help make Dianara better! MainWindow6La lnia temporal principalThe main timeline MainWindow^La gent a la que segueixes, i la que et segueix2The people you follow, and the ones who follow you MainWindow2Hi han %1 missatges nous.There are %1 new posts. MainWindow*Hi ha 1 missatge nou.There is 1 new post. MainWindowJLnia temporal actualitzada a les %1.Timeline updated at %1. MainWindow8Lnia temporal actualitzada.Timeline updated. MainWindowjLnia temporal actualitzada. No hi ha missatges nous.Timeline updated. No new posts. MainWindowNActualitzar lnia temporal d'&activitatUpdate &Activity Timeline MainWindowPActualitzar lnia temporal de &missatgesUpdate &Messages Timeline MainWindowLActualitzar totes les lnies temporalsUpdate All Timelines MainWindowNActualitzar lnia temporal de &favoritsUpdate Favorites &Timeline MainWindowBActualitzar lnia temporal meno&rUpdate Minor &Feed MainWindow"Visitar lloc &webVisit &Website MainWindowLAmb Dianara pots veure les teves lnies temporals, crear nous missatges, penjar fotos, interactuar amb els missatges, gestionar els teus contactes i seguir gent nova.With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. MainWindowPEl teu compte Pump.io no est configurat&Your Pump.io account is not configured MainWindowPEl teu compte no est configurat encara.#Your account is not configured yet. MainWindow6Els missatges que t'agradenYour favorited posts MainWindow2Els teus propis missatgesYour own posts MainWindowObtenir msGet More MinorFeed8Obtenir activitats anteriorsGet older activities MinorFeedNEncara no hi ha activitats per mostrar.$There are no activities to show yet. MinorFeed:Obrir el missatge referenciatOpen referenced post MinorFeedItemutilitzant %1using %1 MinorFeedItem bytesbytes MiscHelpers&Cancellar&Cancel PeopleWidgetCe&rcar:&Search: PeopleWidget>Afegir un contacte a una llistaAdd a contact to a list PeopleWidget@Escriu aqu un nom per buscar-ho"Enter a name here to search for it PeopleWidget%1 comentaris %1 commentsPost$A %1 els hi agrada %1 like thisPostLi agrada a %1%1 likesPostA %1 li agrada %1 likes thisPost(%1 ha compartit aix%1 shared thisPost*%1 han compartit aix#%1 = Names for more than one person%1 shared thisPost&No&NoPost &S, eliminar-ho&Yes, delete itPost"&S, compartir-ho&Yes, share itPost*&S, deixar de seguir&Yes, stop followingPost6&S, deixar de compartir-ho&Yes, unshare itPost1 comentari 1 commentPostLi agrada a 11 likePostbEsts segur de que vols eliminar aquest missatge?*Are you sure you want to delete this post?Post\Ests segur de que vols deixar de seguir a %1?+Are you sure you want to stop following %1?PostCCCCPostbFes clic a la imatge per veure-la a mida completa&Click the image to see it in full sizePostComentarCommentPost8Comentar en aquest missatge.Comment on this post.PostZCopiar l'enlla del missatge al porta-retallsCopy post link to clipboardPostEliminarDeletePost0Eliminar aquest missatgeDelete this postPostBVols compartir el missatge de %1?Do you want to share %1's post?PostVVols deixar de compartir el missatge de %1?!Do you want to unshare %1's post?Post EditarEditPost,Editar aquest missatgeEdit this postPostEditat el %1 Edited on %1PostEditat: %1 Edited: %1Post SeguirFollowPost CiutatHometownPostRSi selecciones part del text, ser citat.+If you select some text, it will be quoted.Post ImatgeImagePost6La imatge est carregant...Image is loading...PostAInPostM'agradaLikePost@Dir que t'agrada aquest missatgeLike this postPostEnlla a: %1 Link to: %1Post6Normalitzar colors del textNormalize text colorsPostNotaNotePostLObrir el perfil de %1 al navegador web Open %1's profile in web browserPostDObrir el missatge al navegador webOpen post in web browserPost AltreOtherPostMissatgeNoun, not verbPostPostPublicat el %1 Posted on %1PostCompartirSharePost&Compartir missatge? Share post?Post2Compartir aquest missatgeShare this postPost"Compartit %1 copsShared %1 timesPostCompartit el %1 Shared on %1Post Compartit un cop Shared oncePost Deixar de seguirStop followingPost"Deixar de seguir?Stop following?Post Per aToPostJa no m'agradaUnlikePost&Deixar de compartirUnsharePost@Deixar de compartir el missatge? Unshare post?PostFDeixar de compartir aquest missatgeUnshare this postPostA travs de %1Via %1Post0AVS: Eliminar missatge?WARNING: Delete post?PostT'agrada aix You like thisPostutilitzant %1using %1Postutilitzant %11=Program used for postingusing %1Post&Bio&Bio ProfileEditor&Cancellar&Cancel ProfileEditorCiuta&t &Hometown ProfileEditor&Guardar perfil &Save Profile ProfileEditorTots els arxius All files ProfileEditor AvatarAvatar ProfileEditor$Canviar &avatar...Change &Avatar... ProfileEditor E-mailE-mail ProfileEditor&Nom complet Full &Name ProfileEditorArxius d'imatge Image files ProfileEditor Imatge no vlida Invalid image ProfileEditorSense definirNot set ProfileEditor Editor de perfilProfile Editor ProfileEditor4Selecciona imatge d'avatarSelect avatar image ProfileEditorFLa imatge seleccionad no es vlida. The selected image is not valid. ProfileEditorAquesta s l'adrea d'e-mail associada al teu compte, per coses com ara notificacions i recuperaci de la contrasenyaoThis is the e-mail address associated with your account, for things such as notifications and password recovery ProfileEditor<Aquesta es la teva adrea PumpThis is your Pump address ProfileEditor8Aquest es el teu nom visibleThis is your visible name ProfileEditorID Webfinger Webfinger ID ProfileEditor&Afegir foto Ad&d Picture PublisherTots els arxius All files Publisher CC...CC... PublisherCancellarCancel Publisher,Cancellar el missatgeCancel the post Publisher Editant missatge Editing post Publisher4Error: Ja s'est redactantError: Already composing PublisherFTrobar la foto a les teves carpetes Find the picture in your folders PublisherSeguidors Followers PublisherZPrem Control+Enter per publicar amb el teclat+Hit Control+Enter to post with the keyboard PublisherArxius d'imatge Image files Publisher Imatge no vlida Invalid image PublisherLlistesLists Publisher<Ttol opcional per al missatgeOptional title for the post PublisherPersones... People... Publisher2No s'ha escollit una fotoPicture not set PublisherPublicarPost Publisher,El missatge est buit.Post is empty. PublisherXHa fallat la publicaci. Torna-ho a provar.Posting failed. Try again. PublisherPublicant... Posting... Publisher PblicPublic PublisherResoluci Resolution Publisher&Seleccionar foto...Select Picture... Publisher*Selecciona una imatgeSelect one image Publisher`Selecciona qui rebr una cpia d'aquest missatge'Select who will get a copy of this post PublisherHSelecciona qui veur aquest missatgeSelect who will see this post PublisherMidaSize Publisher No es pot detectar el format de la imatge. Potser l'extensi est equivocada, com una imatge GIF reanomenada a imatge.jpg o semblant.tThe image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Publisher TtolTitle PublisherTtol per al missatge. Afegir un ttol ajuda a fer el contingut del "Mentrestant" ms informatiu.STitle for the post. Setting a title helps make the Meanwhile feed more informative. PublisherPer a...To... PublisherActualitzarUpdate PublisherActualitzant... Updating... PublisherPujar foto Upload photo PublisherNo pots editar un missatge en aquest moment, perque ja s'est redactant un missatge.MYou can't edit a post at this time, because a post is already being composed. PublisherActualitzaci de lnia temporal d'activitat sollicitada, per les actualitzacions estan bloquejades.Esborrant llista de persones...Deleting person list...PumpControllerActualitzaci de lnia temporal directa sollicitada, per les actualitzacions estan bloquejades.:Direct timeline update requested, but updates are blocked.PumpController*Error connectant a %1Error connecting to %1PumpControllerActualitzaci de lnia temporal de favorits sollicitada, per les actualitzacions estan bloquejades.=Favorites timeline update requested, but updates are blocked.PumpController`Arxiu penjat correctament. Publicant missatge....File uploaded successfully. Posting message...PumpController*Seguint correctament.Following successfully.PumpControllerProhibit ForbiddenPumpController@Rebent una llista de persones...Getting a person list...PumpControllerHRebent lnia temporal d'activitat...Getting activity timeline...PumpController(Rebent comentaris...Getting comments...PumpController\Rebent lnia temporal de missatges directes...#Getting direct messages timeline...PumpControllerHRebent lnia temporal de favorits...Getting favorites timeline...PumpController"Rebent "likes"...Getting likes...PumpController>Rebent llista de 'Seguidors'...Getting list of 'Followers'...PumpController:Rebent llista de 'Seguint'...Getting list of 'Following'...PumpControllerNRebent llista de llistes de persones...Getting list of person lists...PumpControllerDRebent lnia temporal principal...Getting main timeline...PumpController<Rebent lnia temporal menor...Getting minor feed...PumpController Ja no disponibleGonePumpControllerError internInternal Server ErrorPumpController$"M'agrada" rebuts.Likes received.PumpControllerTLlista de 'seguidors' completament rebuda.(List of 'followers' completely received.PumpControllerPLlista de 'seguint' completament rebuda.(List of 'following' completely received.PumpController6Llista de 'llistes' rebuda.List of 'lists' received.PumpControllerActualitzaci de lnia temporal principal sollicitada, per les actualitzacions estan bloquejades.8Main timeline update requested, but updates are blocked.PumpController>Missatge eliminat correctament.Message deleted successfully.PumpControllerhMissatge marcat o desmarcat "M'agrada" correctament.&Message liked or unliked successfully.PumpController8Lnia temporal menor rebuda.Minor feed received.PumpController&Mogut permanentmentMoved PermanentlyPumpController$Mogut temporalmentMoved TemporarilyPumpControllerNo trobat Not FoundPumpControllerPPart de la llista de 'seguidors' rebuda.%Partial list of 'followers' received.PumpControllerLPart de la llista de 'seguint' rebuda.%Partial list of 'following' received.PumpControllerLPersona afegida a llista correctament."Person added to list successfully.PumpControllerNLlista de persones creada correctament.!Person list created successfully.PumpControllerTLlista de persones esborrada correctament.!Person list deleted successfully.PumpController4Llista de persones rebuda.Person list received.PumpControllerXPersona eliminada de la llista correctament.&Person removed from list successfully.PumpController>Missatge publicat correctament.Post published successfully.PumpController@Missatge compartit correctament.Post shared successfully.PumpControllerDMissatge actualitzat correctament.Post updated successfully.PumpControllerPerfil rebut.Profile received.PumpController&Perfil actualitzat.Profile updated.PumpControllerPreparat.Ready.PumpControllerFTreient una persona d'una llista...Removing person from list...PumpController(Servei no disponibleService UnavailablePumpControllerFS'ha deixat de seguir correctament.Stopped following successfully.PumpControllerS'ha rebut la lnia temporal. Actualitzant llista de missatges...(Timeline received. Updating post list...PumpControllerNo autoritzat UnauthorizedPumpControllerDCodi d'error HTTP no gestionat: %1Unhandled HTTP error code %1PumpControllerPgina &segent &Next PageTimeLine Pgina &anterior&Previous PageTimeLine4Lnia temporal d'activitatActivity TimelineTimeLineQuan el procs estigui llest, el teu perfil i lnies temporals haurien d'actualitzar-se automticament.RAfter the process is done, your profile and timelines should update automatically.TimeLineHDianara es un client <b>pump.io</b>.#Dianara is a pump.io client.TimeLineBloc de DianaraDianara's blogTimeLineHLnia temporal de missatges directesDirect Messages TimelineTimeLine&Primera pgina F&irst PageTimeLine4Lnia temporal de favoritsFavorites TimelineTimeLineEn primer lloc, configura el teu compte des del men <b>Configuraci - Compte</b>.FFirst, configure your account from the Settings - Account menu.TimeLineBPreguntes freqents sobre pump.io(Frequently asked questions about pump.ioTimeLinenAqu veurs els missatges dirigits especficament a tu.4Here, you'll see posts specifically directed to you.TimeLineSi encara no tens un compte Pump, pots aconseguir-ne un a la segent adrea:OIf you don't have a Pump account yet, you can get one at the following address:TimeLineRMissatges i comentaris que t'han agradat. Posts and comments you've liked.TimeLinePren-te un moment per fer una ullada als mens i la finestra de Configuraci.DTake a moment to look around the menus and the Configuration window.TimeLineRHi ha indicadors de funci (tooltips) per tot arreu, pel que si passes sobre un bot o un camp de text amb el ratol, s probable que vegis alguna informaci addicional.There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information.TimeLine&Benvingut a DianaraWelcome to DianaraTimeLineTamb pots omplir la teva informaci de perfil i foto des del men <b>Configuraci - Editar perfil</b>.\You can also set your profile data and picture from the Settings - Edit Profile menu.TimeLineLAqu veurs els teus propis missatges.You'll see your own posts here.TimeLineFa %1 dies %1 days ago TimestampFa %1 hores %1 hours ago TimestampFa %1 minuts%1 minutes ago TimestampFa %1 mesos %1 months ago TimestampFa %1 anys %1 years ago TimestampFa un dia A day ago TimestampFa un minut A minute ago TimestampFa un mes A month ago TimestampFa un any A year ago TimestampFa una hora An hour ago Timestamp(Hora/data no vlida!Invalid timestamp! Timestamp&Fa menys d'un minutLess than a minute ago TimestampA dianara-v1.1/translations/dianara_es.qm000664 000764 000764 00000145562 12264101455 017717 0ustar00janjan000000 000000 Yt[ %d[!j e+knUs>yT&Uz>$hNvc-PG3|{ݣxsDC]DIn-M$hRSlCod, ;,>Nq%VBe SWyl%3K`^%v!S:zJZ^[3jte 6!!ƭ>ϠN#؞.%Y1oW8Y aJ 8$1ti6:70j$J;JJQ_i69sCgMx2z8Bz.QQ:=B5\ÌJרCX?:D_?Jog nHv?ʳ0 1un[4>>BPEV&`3ga$Qr):2+:k7{<OM hzb>u]+I{ R,,mT4u3g33gpvR<2@;z7xWLF !V3,&4 KT4KTvO RMRV|"fy|s>{{K{V1Vi<{+rT2t0: [X6\w7T00wƨI2xRon+^ ^!-+f-^4u|S4uk9$}<.z"<.3H<.p;=mM?$TZPMf;!p{r7\y~&a/Sbt^,1MuuV84ґ2ڑByڱ?>/ 0t1;#n4}<D<|<?;U]AN~lgPpj[{k<jM} of0IROK39XMxbҕiiQr?Eg" #Nf)2K#2?Ne9e79m.=xsnZtOUw4'B>*Irn(ťUu&{._琊 r{tR4" -QR 1`#) 7w!< hӾ r% |L ~$ 4ar   ` F e o# b?3 ʞO & ~c [ Ad Ay9 ~ of  !/I # -E ?mC Hk TG4 TGvU d 4i d: d<lp e> q\2 z3.8 ~ $  I I I)p I; IeZ Iy )ph >q. MO yo o ? ^ '_n t)8 3G6 ֓ TN ؄ E= 0n /^  c# m "e}1 "$ 0w 5<1H @v Q(oK bXL b{N mmTi P/j ! fA V l j6 ٩W V uP أuz  C+X 2W (=k - Uz[ ET} YwB ]ӡ0 `u eBUcK {} F' ? VO : #Wc ͡ = < tm * w" 3N aX7 _< ЕYg #pn G VQ bQ g- h13 mCUY |s b b| R: \VGx2s+j,/{RXKlBlzlmhnc$H8T8TH,8Th>Q7'GU)y5@?z'iA pFUO~T*_} bu.gt\09=(n9/NU؞¹e%E~=8".Uڨdޝr/GiPblicoPublic ASActivity0&Autorizar la aplicacin&Authorize Application AccountDialog&Cancelar&Cancel AccountDialog&Guardar datos &Save Details AccountDialogAhora se iniciar un navegador web, donde podrs obtener el cdigo de verificacinAA web browser will start now, where you can get the verifier code AccountDialog4Configuracin de la cuentaAccount Configuration AccountDialogCuando pulses este botn, se abrir un navegador web, solicitando autorizacin para DianaraYAfter clicking this button, a web browser will open, requesting authorization for Dianara AccountDialogfDianara tiene autorizacin para acceder a tu cuenta)Dianara is authorized to access your data AccountDialogIntroduce aqu el codigo de verificacin proporcionado por tu servidor Pump9Enter the verifier code provided by your Pump server here AccountDialogPrimero, introduce tu identificador Webfinger, tu direccin pump.io.5First, enter your Webfinger ID, your pump.io address. AccountDialog>Obtener cdigo de &verificacinGet &Verifier Code AccountDialogSi el navegador web no se abre automticamente, copia esta direccin manualmenteEIf the browser doesn't open automatically, copy this address manually AccountDialogSi tu perfil est en https://pump.ejemplo/tunombre, entonces tu direccin es tunombre@pump.ejemplo_If your profile is at https://pump.example/yourname, then your address is yourname@pump.example AccountDialog<Una vez que hayas autorizado a Dianara desde la interfaz web de tu servidor Pump, recibirs un cdigo llamado VERIFIER. Cpialo y pgalo en el campo de abajo.Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. AccountDialog0Pega el verificador aquPaste the verifier here AccountDialogHEl cdigo de verificacin est vacoVerifier code is empty AccountDialog.Cdigo de verificacin:Verifier code: AccountDialogVID Webfinger, tipo usuario@servidorpump.org*Webfinger ID, like username@pumpserver.org AccountDialog<Tu direccin Pump no es vlidaYour Pump address is invalid AccountDialog*Tu direccin pump.io:Your Pump.io address: AccountDialogTu direccin es como usuario@servidorpump.org, y puedes encontrarla en tu perfil, en la interfaz web.kYour address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. AccountDialogFTu direccin, como usuario@servidor Your address, as username@server AccountDialog.&Aadir a seleccionados&Add to SelectedAudienceSelector&Cancelar&CancelAudienceSelector &Hecho&DoneAudienceSelectorLista 'CC' 'CC' ListAudienceSelectorLista 'Para' 'To' ListAudienceSelector&Todos los contactos All ContactsAudienceSelectorBorrar &lista Clear &ListAudienceSelector8Selecciona gente de la lista de la izquierda. Puedes arrastrarlos con el ratn, hacer clic o doble clic en ellos, o seleccionarlos y usar el botn de abajo.Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below.AudienceSelector$Gente seleccionadaSelected PeopleAudienceSelector<A %1 les gusta este comentario%1 like this commentComment:A %1 le gusta este comentario%1 likes this commentComment&No&NoComment&S, borrarlo&Yes, delete itCommentlEsts seguro de que quieres eliminar este comentario?-Are you sure you want to delete this comment?CommentEliminarDeleteComment0Eliminar este comentarioDelete this commentCommentMe gustaLikeCommentnDecir que te gusta o que ya no te gusta este comentarioLike or unlike this commentComment CitarQuoteCommentBResponder citando este comentarioReply quoting this commentCommentYa no me gustaUnlikeCommentDADVERTENCIA: Eliminar comentario?WARNING: Delete comment?Comment&Comentar&CommentCommenterBlockC&ancelarC&ancelCommenterBlock2El comentario est vaco.Comment is empty.CommenterBlockvHa fallado la publicacin del comentario. Prueba de nuevo.#Posting comment failed. Try again.CommenterBlockjPulsa ESC para cancelar el comentario si no hay texto3Press ESC to cancel the comment if there is no textCommenterBlock,Enviando comentario...Sending comment...CommenterBlock:Mostrar todos los comentariosShow All CommentsCommenterBlockPuedes pulsar Control+Enter para enviar el comentario con el tecladoAYou can press Control+Enter to send the comment with the keyboardCommenterBlock&Formato&FormatComposer&No&NoComposer&S, cancelarlo&Yes, cancel itComposerTSeguro que quieres cancelar este mensaje?-Are you sure you want to cancel this message?ComposerNegritaBoldComposer*Cancelar el mensaje?Cancel message?ComposerrHaz clic aqu o pulsa Control+N para publicar una nota.../Click here or press Control+N to post a note...Composer(Error: URL no vlidaError: Invalid URLComposerFormato FormattingComposer TtuloHeaderComposer$Insertar un enlace Insert a linkComposerBInsertar una imagen desde una URLInsert an image from a URLComposerLInsertar una imagen desde un sitio webInsert an image from a web siteComposerInsertar lnea Insert lineComposerCursivaItalicComposerHacer un enlace Make a linkComposerNHacer un link con el texto seleccionadoMake a link from selected textComposer NormalNormalComposer.Pegar texto sin formatoPaste Text Without FormattingComposer(Bloque preformateadoPreformatted blockComposerBloque de cita Quote blockComposerTachado StrikethroughComposerSmbolosSymbolsComposer8Opciones de formato de textoText Formatting OptionsComposerLa direccin que has introducido (%1) no es vlida. Las direcciones de imagenes han de empezar por http:// o https://`The address you entered (%1) is not valid. Image addresses should begin with http:// or https://Composer4Escribe un comentario aquType a comment hereComposerNEscribe un mensaje aqu para publicarloType a message here to post itComposerLEscribe o pega una direccin web aqu."Type or paste a web address here. ComposerEscribe o pega una direccin web aqu. El texto seleccionado (%1) ser convertido en un enlace.UType or paste a web address here. The selected text (%1) will be converted to a link.Composer\Escribe o pega la direccin de la imagen aqu.&Type or paste the image address here. ComposerSubrayado UnderlineComposer&Cancelar&Cancel ConfigDialog$Pes&taas movibles &Movable tabs ConfigDialog\&Mensajes por pgina, lnea temporal principal&Posts per page, main timeline ConfigDialog,&Guardar configuracin&Save Configuration ConfigDialog2Posicin de las &pestaas&Tabs position ConfigDialog>Como notificaciones del sistemaAs system notifications ConfigDialogParte inferiorBottom ConfigDialogJDianara guarda datos en esta carpeta:#Dianara stores data in this folder: ConfigDialog2No mostrar notificacionesDon't show notifications ConfigDialogLado izquierdo Left side ConfigDialogZMensajes por pgina, &otras lneas temporales Posts per page, &other timelines ConfigDialog4Configuracin del programaProgram Configuration ConfigDialog<Mensajes pblicos por &defectoPublic posts as &default ConfigDialogLado derecho Right side ConfigDialog.Mostrar &notificacionesShow ¬ifications ConfigDialog`Intervalo de &actualizacin de la lnea temporalTimeline &update interval ConfigDialogParte superiorTop ConfigDialog:Usando notificaciones propiasUsing own notifications ConfigDialogminutosminutes ConfigDialogmensajes!Goes after a number, as: 25 postsposts ConfigDialogmensajes(This goes after a number, like: 10 postsposts ConfigDialog&No&No ContactCard(&S, dejar de seguir&Yes, stop following ContactCarddEsts seguro de que quieres dejar de seguir a %1?+Are you sure you want to stop following %1? ContactCardBio de %1 Bio for %1 ContactCard SeguirFollow ContactCard CiudadHometown ContactCardEn listas... In Lists... ContactCard0No hay biografa para %1No biography for %1 ContactCardFAbrir el perfil en el navegador webOpen Profile in Web Browser ContactCardDejar de seguirStop Following ContactCard"Dejar de seguir?Stop following? ContactCardFEste usuario no tiene una biografa"This user doesn't have a biography ContactCard&Opciones de usuario User Options ContactCardB&Introduce direccin para seguir:&Enter address to follow: ContactList&Seguir&Follow ContactList&Listas&Lists ContactList&Exportar SeguidoresExport Followers ContactList$Exportar SiguiendoExport Following ContactListVExportar lista de 'seguidores' a un archivo$Export list of 'followers' to a file ContactListTExportar lista de 'siguiendo' a un archivo$Export list of 'following' to a file ContactListSe&guidores Follo&wers ContactList&Siguiendo Followin&g ContactListOpcio&nesOptio&ns ContactList*Actualizar SeguidoresReload Followers ContactList(Actualizar SiguiendoReload Following ContactList"Actualizar listas Reload Lists ContactListfusuario@servidor.org o https://servidor.org/usuario2username@server.org or https://server.org/username ContactList.Notificacin de DianaraDianara NotificationFDNotifications&Aadir filtro &Add Filter FilterEditor&Cancelar&Cancel FilterEditor&Nuevo filtro &New Filter FilterEditor:&Eliminar filtro seleccionado&Remove Selected Filter FilterEditor &Guardar filtros &Save Filters FilterEditorAplicacin Application FilterEditorDe momento, estos filtros solo se aplican a la lnea temporal "Mientras tanto".>At the moment, these filters only apply to the Meanwhile feed. FilterEditor AutorAuthor FilterEditor"Filtros &actualesC&urrent Filters FilterEditorContenidoContent FilterEditor"Editor de filtros Filter Editor FilterEditorFiltros en usoFilters in use FilterEditorAqu puedes establecer algunos filtros. Puedes filtrar por contenido, autor o aplicacin. Por ejemplo, puedes filtrar mensajes publicados por la aplicacin Open Farm Game, o que contienen la palabra NSFW en el mensaje.Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. FilterEditor"Palabras clave... Keywords... FilterEditor$Todos los archivos All files ImageViewerCerrar visor Close Viewer ImageViewer\ESC para cerrar, clic secundario para opciones)ESC to close, secondary-click for options ImageViewer2Error guardando la imagenError saving image ImageViewer ImagenImage ImageViewer$Archivos de imagen Image files ImageViewer,Guardar imagen como...Save Image As... ImageViewer"Guardar imagen... Save Image... ImageViewerHa habido un problema al guardar %1. El nombre del archivo debera acabar con la extensin .jpg o .png.UThere was a problem while saving %1. Filename should end in .jpg or .png extensions. ImageViewer&Aadir a lista &Add to List ListsManager4&Borrar lista seleccionada&Delete Selected List ListsManager&No&No ListsManager"&Eliminar miembro&Remove Member ListsManager&S&Yes ListsManager&S, borrarla&Yes, delete it ListsManagerAadir miem&bro Add Mem&ber ListsManager&Aadir nueva &lista Add New &List ListsManagerREsts seguro de que quieres eliminar %1?#Are you sure you want to delete %1? ListsManagerpEsts seguro de que quieres quitar a %1 de la lista %2?4Are you sure you want to remove %1 from the %2 list? ListsManagerCrear l&ista Create L&ist ListsManagerMiembrosMembers ListsManager NombreName ListsManager8Quitar persona de la lista?Remove person from list? ListsManagerPEscribe un nombre para la nueva lista...Type a name for the new list... ListsManagerJEscribe una descripcin opcional aqu!Type an optional description here ListsManager:ADVERTENCIA: Eliminar lista?WARNING: Delete list? ListsManager&Cuenta&Account MainWindow&Actividad &Activity MainWindow&&Configurar Dianara&Configure Dianara MainWindow&Contactos &Contacts MainWindow&Filtros&Filters MainWindowFPreguntas &frecuentes sobre pump.io)&Frequently Asked Questions about Pump.io MainWindow Ay&uda&Help MainWindow&Mensajes &Messages MainWindow$&Publicar una nota &Post a Note MainWindow &Salir&Quit MainWindow&Sesin&Session MainWindow &Mostrar ventana &Show Window MainWindowLnea &temporal &Timeline MainWindowHAct&ualizar lnea temporal principal&Update Main Timeline MainWindow&Ver&View MainWindow$Acerca de &DianaraAbout &Dianara MainWindow"Acerca de Dianara About Dianara MainWindow.Notificacin de DianaraDianara Notification MainWindowbDianara es un cliente de red social para pump.io..Dianara is a pump.io social networking client. MainWindowDianara est licenciado bajo la licencia GNU GPL, y utiliza algunos iconos Oxygen: http://www.oxygen-icons.org/ (Licencia LGPL)wDianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) MainWindowEditar &perfil Edit &Profile MainWindowRTraduccin al castellano por JanKusanagi.#English translation by JanKusanagi. MainWindowFa&voritos Fav&orites MainWindow$&Pantalla completa Full &Screen MainWindow Inicializando...Initializing... MainWindow,Marcar todo como ledoMark All as Read MainWindow"Mientras tanto... Meanwhile... MainWindowJMensajes enviados explcitamente a tiMessages sent explicitly to you MainWindow&Configuracin S&ettings MainWindowPanel &lateral Side &Panel MainWindow &Barra de estado Status &Bar MainWindowGracias a todos los 'testers', traductores y empaquetadores, que ayudan a hacer Dianara mejor!SThanks to all the testers, translators and packagers, who help make Dianara better! MainWindow6La lnea temporal principalThe main timeline MainWindowVLa gente a la que sigues, y la que te sigue2The people you follow, and the ones who follow you MainWindow.Hay %1 mensajes nuevos.There are %1 new posts. MainWindow(Hay 1 mensaje nuevo.There is 1 new post. MainWindowHLnea temporal actualizada a las %1.Timeline updated at %1. MainWindow6Lnea temporal actualizada.Timeline updated. MainWindowfLnea temporal actualizada. No hay mensajes nuevos.Timeline updated. No new posts. MainWindowNActualizar lnea temporal de &actividadUpdate &Activity Timeline MainWindowLActualizar lnea temporal de &mensajesUpdate &Messages Timeline MainWindowLActualizar todas las lneas temporalesUpdate All Timelines MainWindowNActualizar lnea temporal de &favoritosUpdate Favorites &Timeline MainWindow@Actualizar lnea temporal meno&rUpdate Minor &Feed MainWindow$Visitar sitio &webVisit &Website MainWindow:Con Dianara puedes ver tus lneas temporales, crear nuevos mensajes, subir fotos, interactuar con los mensajes, gestionar tus contactos y seguir gente nueva.With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. MainWindowJTu cuenta Pump.io no est configurada&Your Pump.io account is not configured MainWindowLTu cuenta no est configurada todava.#Your account is not configured yet. MainWindow4Los mensajes que te gustanYour favorited posts MainWindow(Tus propios mensajesYour own posts MainWindowObtener msGet More MinorFeed<Obtener actividades anterioresGet older activities MinorFeedHAn no hay actividades para mostrar.$There are no activities to show yet. MinorFeed:Abrir el mensaje referenciadoOpen referenced post MinorFeedItemusando %1using %1 MinorFeedItem bytesbytes MiscHelpers&Cancelar&Cancel PeopleWidget&Buscar:&Search: PeopleWidget<Aadir un contacto a una listaAdd a contact to a list PeopleWidgetHEscribe aqu un nombre para buscarlo"Enter a name here to search for it PeopleWidget%1 comentarios %1 commentsPostA %1 les gusta %1 like thisPostLes gusta a %1%1 likesPostA %1 le gusta %1 likes thisPost*%1 ha compartido esto%1 shared thisPost,%1 han compartido esto#%1 = Names for more than one person%1 shared thisPost&No&NoPost&S, borrarlo&Yes, delete itPost &S, compartirlo&Yes, share itPost(&S, dejar de seguir&Yes, stop followingPost0&S,dejar de compartirlo&Yes, unshare itPost1 comentario 1 commentPostLe gusta a 11 likePostfEsts seguro de que quieres eliminar este mensaje?*Are you sure you want to delete this post?PostdEsts seguro de que quieres dejar de seguir a %1?+Are you sure you want to stop following %1?PostCCCCPostfHaz clic en la imagen para verla en tamao completo&Click the image to see it in full sizePostComentarCommentPost2Comentar en este mensaje.Comment on this post.PostXCopiar el enlace del mensaje al portapapelesCopy post link to clipboardPostEliminarDeletePost*Eliminar este mensajeDelete this postPostHQuieres compartir el mensaje de %1?Do you want to share %1's post?PostZQuieres dejar de compartir el mensaje de %1?!Do you want to unshare %1's post?Post EditarEditPost&Editar este mensajeEdit this postPostEditado el %1 Edited on %1PostEditado: %1 Edited: %1Post SeguirFollowPost CiudadHometownPostXSi seleccionas parte del texto, ser citado.+If you select some text, it will be quoted.Post ImagenImagePost4La imagen est cargando...Image is loading...PostEnInPostMe gustaLikePost>Decir que te gusta este mensajeLike this postPostEnlace a: %1 Link to: %1Post8Normalizar colores del textoNormalize text colorsPostNotaNotePostRAbrir el perfil de %1 en el navegador web Open %1's profile in web browserPostHAbrir el mensaje en el navegador webOpen post in web browserPostOtroOtherPostMensajeNoun, not verbPostPostPublicado el %1 Posted on %1PostCompartirSharePost&Compartir mensaje? Share post?Post,Compartir este mensajeShare this postPost&Compartido %1 vecesShared %1 timesPost Compartido el %1 Shared on %1Post$Compartido una vez Shared oncePostDejar de seguirStop followingPost"Dejar de seguir?Stop following?PostParaToPostYa no me gustaUnlikePost$Dejar de compartirUnsharePost>Dejar de compartir el mensaje? Unshare post?Post>Dejar de compartir este mensajeUnshare this postPostA travs de %1Via %1Post>ADVERTENCIA: Eliminar mensaje?WARNING: Delete post?PostTe gusta esto You like thisPostusando %1using %1Postusando %11=Program used for postingusing %1Post&Bio&Bio ProfileEditor&Cancelar&Cancel ProfileEditorCiuda&d &Hometown ProfileEditor&Guardar perfil &Save Profile ProfileEditor$Todos los archivos All files ProfileEditor AvatarAvatar ProfileEditor$Cambiar &avatar...Change &Avatar... ProfileEditor E-mailE-mail ProfileEditor &Nombre completo Full &Name ProfileEditor$Archivos de imagen Image files ProfileEditor Imagen no vlida Invalid image ProfileEditorSin definirNot set ProfileEditor Editor de perfilProfile Editor ProfileEditor6Selecciona imagen de avatarSelect avatar image ProfileEditorHLa imagen seleccionada no es vlida. The selected image is not valid. ProfileEditorEsta es la direccin de e-mail asociada con tu cuenta, para cosas como notificaciones y recuperacin de la contraseaoThis is the e-mail address associated with your account, for things such as notifications and password recovery ProfileEditor2Esta es tu direccin PumpThis is your Pump address ProfileEditor2Este es tu nombre visibleThis is your visible name ProfileEditorID Webfinger Webfinger ID ProfileEditor&Aadir foto Ad&d Picture Publisher$Todos los archivos All files Publisher CC...CC... PublisherCancelarCancel Publisher&Cancelar el mensajeCancel the post Publisher Editando mensaje Editing post Publisher8Error: Ya se est redactandoError: Already composing PublisherBEncontrar la foto en tus carpetas Find the picture in your folders PublisherSeguidores Followers Publisher`Pulsa Control+Enter para publicar con el teclado+Hit Control+Enter to post with the keyboard Publisher$Archivos de imagen Image files Publisher Imagen no vlida Invalid image Publisher ListasLists Publisher>Ttulo opcional para el mensajeOptional title for the post PublisherPersonas... People... Publisher(Foto no seleccionadaPicture not set PublisherPublicarPost Publisher,El mensaje est vaco.Post is empty. PublisherXHa fallado la publicacin. Prueba de nuevo.Posting failed. Try again. PublisherPublicando... Posting... PublisherPblicoPublic PublisherResolucin Resolution Publisher&Seleccionar foto...Select Picture... Publisher*Selecciona una imagenSelect one image PublisherfSelecciona quien recibir una copia de este mensaje'Select who will get a copy of this post PublisherDSelecciona quien ver este mensajeSelect who will see this post Publisher TamaoSize PublisherNo se puede detectar el formato de la imagen. La extensin podra estar equivocada, como una imagen GIF renombrada a imagen.jpg o similar.tThe image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Publisher TtuloTitle PublisherTtulo para el mensaje. Aadir un ttulo ayuda a hacer el contenido del "Mientras tanto" ms informativo.STitle for the post. Setting a title helps make the Meanwhile feed more informative. PublisherPara...To... PublisherActualizarUpdate PublisherActualizando... Updating... PublisherSubir foto Upload photo PublisherNo puedes editar un mensaje en este momento, porque ya se est redactando un mensaje.MYou can't edit a post at this time, because a post is already being composed. PublisherActualizacin de lnea temporal de actividad solicitada, pero las actualizaciones estn bloqueadas.Avatar publicado correctamente.Avatar published successfully.PumpControllerAvatar subido.Avatar uploaded.PumpController(Solicitud incorrecta Bad RequestPumpControllerFComentario publicado correctamente.Comment posted successfully.PumpController,Comentarios recibidos.Comments received.PumpController8Creando lista de personas...Creating person list...PumpController:Borrando lista de personas...Deleting person list...PumpControllerActualizacin de lnea temporal directa solicitada, pero las actualizaciones estn bloqueadas.:Direct timeline update requested, but updates are blocked.PumpController*Error conectando a %1Error connecting to %1PumpControllerActualizacin de lnea temporal de favoritos solicitada, pero las actualizaciones estn bloqueadas.=Favorites timeline update requested, but updates are blocked.PumpControllerfArchivo subido correctamente. Publicando mensaje....File uploaded successfully. Posting message...PumpController0Siguiendo correctamente.Following successfully.PumpControllerProhibido ForbiddenPumpControllerFRecibiendo una lista de personas...Getting a person list...PumpControllerRRecibiendo lnea temporal de actividad...Getting activity timeline...PumpController2Recibiendo comentarios...Getting comments...PumpControllerbRecibiendo lnea temporal de mensajes directos...#Getting direct messages timeline...PumpControllerRRecibiendo lnea temporal de favoritos...Getting favorites timeline...PumpController*Recibiendo "likes"...Getting likes...PumpControllerFRecibiendo lista de 'Seguidores'...Getting list of 'Followers'...PumpControllerDRecibiendo lista de 'Siguiendo'...Getting list of 'Following'...PumpControllerRRecibiendo lista de listas de personas...Getting list of person lists...PumpControllerLRecibiendo lnea temporal principal...Getting main timeline...PumpControllerDRecibiendo lnea temporal menor...Getting minor feed...PumpController Ya no disponibleGonePumpControllerError internoInternal Server ErrorPumpController*"Me gusta" recibidos.Likes received.PumpControllerZLista de 'seguidores' completamente recibida.(List of 'followers' completely received.PumpControllerXLista de 'siguiendo' completamente recibida.(List of 'following' completely received.PumpController6Lista de 'listas' recibida.List of 'lists' received.PumpControllerActualizacin de lnea temporal principal solicitada, pero las actualizaciones estn bloqueadas.8Main timeline update requested, but updates are blocked.PumpController@Mensaje eliminado correctamente.Message deleted successfully.PumpControllerlMensaje marcado o desmarcado "Me gusta" correctamente.&Message liked or unliked successfully.PumpController<Lnea temporal menor recibida.Minor feed received.PumpController,Movido permanentementeMoved PermanentlyPumpController(Movido temporalmenteMoved TemporarilyPumpControllerNo encontrado Not FoundPumpControllerVParte de la lista de 'seguidores' recibida.%Partial list of 'followers' received.PumpControllerTParte de la lista de 'siguiendo' recibida.%Partial list of 'following' received.PumpControllerLPersona aadida a lista correctamente."Person added to list successfully.PumpControllerNLista de personas creada correctamente.!Person list created successfully.PumpControllerPLista de personas borrada correctamente.!Person list deleted successfully.PumpController6Lista de personas recibida.Person list received.PumpControllerXPersona eliminada de la lista correctamente.&Person removed from list successfully.PumpController@Mensaje publicado correctamente.Post published successfully.PumpControllerBMensaje compartido correctamente.Post shared successfully.PumpControllerDMensaje actualizado correctamente.Post updated successfully.PumpController Perfil recibido.Profile received.PumpController&Perfil actualizado.Profile updated.PumpControllerPreparado.Ready.PumpControllerLQuitando a una persona de una lista...Removing person from list...PumpController,Servicio no disponibleService UnavailablePumpControllerJSe ha dejado de seguir correctamente.Stopped following successfully.PumpControllertLnea temporal recibida. Actualizando lista de mensajes...(Timeline received. Updating post list...PumpControllerNo autorizado UnauthorizedPumpControllerLCdigo de error HTTP no gestionado: %1Unhandled HTTP error code %1PumpController"Pgina &siguiente &Next PageTimeLine Pgina &anterior&Previous PageTimeLine6Lnea temporal de actividadActivity TimelineTimeLineCuando el proceso est listo, tu perfil y lneas temporales deberan de actualizarse automticamente.RAfter the process is done, your profile and timelines should update automatically.TimeLineJDianara es un cliente <b>pump.io</b>.#Dianara is a pump.io client.TimeLineBlog de DianaraDianara's blogTimeLineFLnea temporal de mensajes directosDirect Messages TimelineTimeLine&Primera pgina F&irst PageTimeLine6Lnea temporal de favoritosFavorites TimelineTimeLineEn primer lugar, configura tu cuenta desde el men <b>Configuracin - Cuenta</b>.FFirst, configure your account from the Settings - Account menu.TimeLineDPreguntas frecuentes sobre pump.io(Frequently asked questions about pump.ioTimeLinenAqu vers los mensajes dirigidos especficamente a t.4Here, you'll see posts specifically directed to you.TimeLineSi an no tienes una cuenta Pump, puedes conseguir una en la siguiente direccin:OIf you don't have a Pump account yet, you can get one at the following address:TimeLineTMensajes y comentarios que te han gustado. Posts and comments you've liked.TimeLineTmate un momento para echar un vistazo por los mens y la ventana de Configuracin.DTake a moment to look around the menus and the Configuration window.TimeLinebHay descripciones emergentes (tooltips) por todas partes, por lo que si pasas sobre un botn o un campo de texto con el ratn, es probable que veas alguna informacin adicional.There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information.TimeLine(Bienvenido a DianaraWelcome to DianaraTimeLineTambin puedes rellenar tu informacin de perfil y foto desde el men <b>Configuracin - Editar perfil</b>.\You can also set your profile data and picture from the Settings - Edit Profile menu.TimeLine@Aqu vers tus propios mensajes.You'll see your own posts here.TimeLineHace %1 das %1 days ago TimestampHace %1 horas %1 hours ago TimestampHace %1 minutos%1 minutes ago TimestampHace %1 meses %1 months ago TimestampHace %1 aos %1 years ago TimestampHace un da A day ago TimestampHace un minuto A minute ago TimestampHace un mes A month ago TimestampHace un ao A year ago TimestampHace una hora An hour ago Timestamp,Hora/fecha no vlida!Invalid timestamp! Timestamp.Hace menos de un minutoLess than a minute ago Timestampdianara-v1.1/translations/dianara_fr.ts000644 000764 000764 00000260217 12264101305 017713 0ustar00janjan000000 000000 ASActivity Public AccountDialog Your Pump.io address: Webfinger ID, like username@pumpserver.org Your address, as username@server Get &Verifier Code Verifier code: &Save Details If the browser doesn't open automatically, copy this address manually Account Configuration First, enter your Webfinger ID, your pump.io address. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example After clicking this button, a web browser will open, requesting authorization for Dianara Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. Don't translate the VERIFIER word! Enter the verifier code provided by your Pump server here Paste the verifier here &Authorize Application &Cancel A web browser will start now, where you can get the verifier code Your Pump address is invalid Verifier code is empty Dianara is authorized to access your data AudienceSelector 'To' List 'CC' List &Add to Selected All Contacts Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below. Clear &List &Done &Cancel Selected People Comment Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Delete Delete this comment Unlike Like %1 like this comment Plural: %1=list of people like John, Jane, Smith %1 likes this comment Singular: %1=name of just 1 person WARNING: Delete comment? Are you sure you want to delete this comment? &Yes, delete it &No CommenterBlock Show All Comments &Comment You can press Control+Enter to send the comment with the keyboard C&ancel Press ESC to cancel the comment if there is no text Posting comment failed. Try again. Sending comment... Comment is empty. Composer Type a message here to post it Click here or press Control+N to post a note... Symbols Formatting Normal Bold Italic Underline Strikethrough Header Preformatted block Quote block Make a link Insert an image from a web site Insert line &Format Button for text formatting and related options Text Formatting Options Paste Text Without Formatting Type a comment here Insert a link Type or paste a web address here. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Insert an image from a URL Type or paste the image address here. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog minutes Top Bottom Left side Right side Program Configuration Timeline &update interval posts Goes after a number, as: 25 posts &Posts per page, main timeline posts This goes after a number, like: 10 posts Posts per page, &other timelines &Tabs position &Movable tabs Public posts as &default As system notifications Using own notifications Don't show notifications Show &notifications Dianara stores data in this folder: &Save Configuration &Cancel ContactCard Hometown Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name This user doesn't have a biography No biography for %1 %1=contact name Open Profile in Web Browser In Lists... User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList &Follow Reload Following Reload Followers Export Following Export Followers username@server.org or https://server.org/username &Enter address to follow: Optio&ns Followin&g Follo&wers Reload Lists &Lists Export list of 'following' to a file Export list of 'followers' to a file FDNotifications Dianara Notification FilterEditor Filter Editor Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. At the moment, these filters only apply to the Meanwhile feed. Content Author Application Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel &New Filter C&urrent Filters ImageViewer Image ESC to close, secondary-click for options Save Image... Close Viewer Save Image As... Image files All files Error saving image There was a problem while saving %1. Filename should end in .jpg or .png extensions. ListsManager Name Members Add Mem&ber &Remove Member &Delete Selected List Add New &List Create L&ist &Add to List Are you sure you want to delete %1? 1=Name of a person list Remove person from list? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list &Yes Type a name for the new list... Type an optional description here WARNING: Delete list? &Yes, delete it &No MainWindow Meanwhile... Side &Panel Status &Bar Dianara Notification &Timeline The main timeline &Activity Your own posts Your favorited posts &Messages Messages sent explicitly to you &Contacts The people you follow, and the ones who follow you Initializing... Your account is not configured yet. &Session &Update Main Timeline Update &Messages Timeline Update &Activity Timeline Update Favorites &Timeline Update Minor &Feed Update All Timelines Mark All as Read &Post a Note &Quit &View Full &Screen S&ettings Edit &Profile &Account &Filters &Frequently Asked Questions about Pump.io Timeline updated at %1. Your Pump.io account is not configured Dianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) &Configure Dianara &Help Visit &Website About &Dianara &Show Window There is 1 new post. There are %1 new posts. Timeline updated. Timeline updated. No new posts. Fav&orites About Dianara Dianara is a pump.io social networking client. With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. Thanks to all the testers, translators and packagers, who help make Dianara better! English translation by JanKusanagi. TRANSLATORS: Change this with your language and name ;) MinorFeed Get More Get older activities There are no activities to show yet. MinorFeedItem using %1 Application used to generate this activity Open referenced post MiscHelpers bytes PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Hometown Like Like this post 1 like 1 comment Shared %1 times Comment Shared on %1 using %1 using %1 1=Program used for posting Edited: %1 Edited on %1 In Share Share this post Edit Edit this post %1 likes %1 comments Delete Note Image Other As in: other type of post Post Noun, not verb Via %1 Posted on %1 1=Date To CC Comment on this post. If you select some text, it will be quoted. Unshare Unshare this post Delete this post Open %1's profile in web browser Open post in web browser Copy post link to clipboard Normalize text colors Stop following Follow Image is loading... %1 likes this One person %1 like this More than one person %1 shared this %1 = One person name %1 shared this %1 = Names for more than one person Shared once You like this Unlike Share post? Do you want to share %1's post? &Yes, share it &No Unshare post? Do you want to unshare %1's post? &Yes, unshare it WARNING: Delete post? Link to: %1 Stop following? Are you sure you want to stop following %1? &Yes, stop following Are you sure you want to delete this post? &Yes, delete it Click the image to see it in full size ProfileEditor Profile Editor This is your Pump address This is the e-mail address associated with your account, for things such as notifications and password recovery Change &Avatar... This is your visible name &Save Profile &Cancel Webfinger ID E-mail Avatar Full &Name &Hometown &Bio Not set In reference to the e-mail not being set for the account Select avatar image Image files All files Invalid image The selected image is not valid. Publisher Title Title for the post. Setting a title helps make the Meanwhile feed more informative. Select Picture... Find the picture in your folders Public Followers Lists To... CC... Select who will get a copy of this post Ad&d Picture Upload photo Post Cancel Cancel the post Picture not set Error: Already composing You can't edit a post at this time, because a post is already being composed. Update Editing post Posting failed. Try again. Updating... Post is empty. Select one image Image files All files Resolution Size Invalid image The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Posting... Optional title for the post People... Select who will see this post Hit Control+Enter to post with the keyboard PumpController Creating person list... Deleting person list... Getting a person list... Adding person to list... Removing person from list... Main timeline update requested, but updates are blocked. Direct timeline update requested, but updates are blocked. Getting direct messages timeline... Activity timeline update requested, but updates are blocked. Favorites timeline update requested, but updates are blocked. Getting favorites timeline... Getting likes... Getting comments... Getting minor feed... Service Unavailable HTTP 503 error string Internal Server Error HTTP 500 error string Gone HTTP 410 error string Not Found HTTP 404 error string Forbidden HTTP 403 error string Unauthorized HTTP 401 error string Bad Request HTTP 400 error string Moved Temporarily HTTP 302 error string Moved Permanently HTTP 301 error string Error connecting to %1 Unhandled HTTP error code %1 Profile received. Profile updated. Post published successfully. Avatar published successfully. Post updated successfully. Comment posted successfully. Post shared successfully. Minor feed received. Following successfully. Stopped following successfully. List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. Person list created successfully. Person list deleted successfully. Person list received. Person added to list successfully. Person removed from list successfully. File uploaded successfully. Posting message... Timeline received. Updating post list... Getting list of 'Following'... Getting list of 'Followers'... Getting list of person lists... Getting main timeline... Getting activity timeline... Message liked or unliked successfully. Likes received. Comments received. Message deleted successfully. List of 'lists' received. Avatar uploaded. Ready. TimeLine Welcome to Dianara Dianara is a <b>pump.io</b> client. If you don't have a Pump account yet, you can get one at the following address: First, configure your account from the <b>Settings - Account</b> menu. After the process is done, your profile and timelines should update automatically. Take a moment to look around the menus and the Configuration window. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Frequently asked questions about pump.io Direct Messages Timeline Here, you'll see posts specifically directed to you. Activity Timeline You'll see your own posts here. Favorites Timeline Posts and comments you've liked. F&irst Page &Previous Page &Next Page Timestamp Invalid timestamp! Less than a minute ago A minute ago %1 minutes ago An hour ago %1 hours ago A day ago %1 days ago A month ago %1 months ago A year ago %1 years ago dianara-v1.1/translations/dianara_de.ts000644 000764 000764 00000260217 12264101305 017674 0ustar00janjan000000 000000 ASActivity Public AccountDialog Your Pump.io address: Webfinger ID, like username@pumpserver.org Your address, as username@server Get &Verifier Code Verifier code: &Save Details If the browser doesn't open automatically, copy this address manually Account Configuration First, enter your Webfinger ID, your pump.io address. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example After clicking this button, a web browser will open, requesting authorization for Dianara Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. Don't translate the VERIFIER word! Enter the verifier code provided by your Pump server here Paste the verifier here &Authorize Application &Cancel A web browser will start now, where you can get the verifier code Your Pump address is invalid Verifier code is empty Dianara is authorized to access your data AudienceSelector 'To' List 'CC' List &Add to Selected All Contacts Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below. Clear &List &Done &Cancel Selected People Comment Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Delete Delete this comment Unlike Like %1 like this comment Plural: %1=list of people like John, Jane, Smith %1 likes this comment Singular: %1=name of just 1 person WARNING: Delete comment? Are you sure you want to delete this comment? &Yes, delete it &No CommenterBlock Show All Comments &Comment You can press Control+Enter to send the comment with the keyboard C&ancel Press ESC to cancel the comment if there is no text Posting comment failed. Try again. Sending comment... Comment is empty. Composer Type a message here to post it Click here or press Control+N to post a note... Symbols Formatting Normal Bold Italic Underline Strikethrough Header Preformatted block Quote block Make a link Insert an image from a web site Insert line &Format Button for text formatting and related options Text Formatting Options Paste Text Without Formatting Type a comment here Insert a link Type or paste a web address here. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Insert an image from a URL Type or paste the image address here. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog minutes Top Bottom Left side Right side Program Configuration Timeline &update interval posts Goes after a number, as: 25 posts &Posts per page, main timeline posts This goes after a number, like: 10 posts Posts per page, &other timelines &Tabs position &Movable tabs Public posts as &default As system notifications Using own notifications Don't show notifications Show &notifications Dianara stores data in this folder: &Save Configuration &Cancel ContactCard Hometown Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name This user doesn't have a biography No biography for %1 %1=contact name Open Profile in Web Browser In Lists... User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList &Follow Reload Following Reload Followers Export Following Export Followers username@server.org or https://server.org/username &Enter address to follow: Optio&ns Followin&g Follo&wers Reload Lists &Lists Export list of 'following' to a file Export list of 'followers' to a file FDNotifications Dianara Notification FilterEditor Filter Editor Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. At the moment, these filters only apply to the Meanwhile feed. Content Author Application Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel &New Filter C&urrent Filters ImageViewer Image ESC to close, secondary-click for options Save Image... Close Viewer Save Image As... Image files All files Error saving image There was a problem while saving %1. Filename should end in .jpg or .png extensions. ListsManager Name Members Add Mem&ber &Remove Member &Delete Selected List Add New &List Create L&ist &Add to List Are you sure you want to delete %1? 1=Name of a person list Remove person from list? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list &Yes Type a name for the new list... Type an optional description here WARNING: Delete list? &Yes, delete it &No MainWindow Meanwhile... Side &Panel Status &Bar Dianara Notification &Timeline The main timeline &Activity Your own posts Your favorited posts &Messages Messages sent explicitly to you &Contacts The people you follow, and the ones who follow you Initializing... Your account is not configured yet. &Session &Update Main Timeline Update &Messages Timeline Update &Activity Timeline Update Favorites &Timeline Update Minor &Feed Update All Timelines Mark All as Read &Post a Note &Quit &View Full &Screen S&ettings Edit &Profile &Account &Filters &Frequently Asked Questions about Pump.io Timeline updated at %1. Your Pump.io account is not configured Dianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) &Configure Dianara &Help Visit &Website About &Dianara &Show Window There is 1 new post. There are %1 new posts. Timeline updated. Timeline updated. No new posts. Fav&orites About Dianara Dianara is a pump.io social networking client. With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. Thanks to all the testers, translators and packagers, who help make Dianara better! English translation by JanKusanagi. TRANSLATORS: Change this with your language and name ;) MinorFeed Get More Get older activities There are no activities to show yet. MinorFeedItem using %1 Application used to generate this activity Open referenced post MiscHelpers bytes PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Hometown Like Like this post 1 like 1 comment Shared %1 times Comment Shared on %1 using %1 using %1 1=Program used for posting Edited: %1 Edited on %1 In Share Share this post Edit Edit this post %1 likes %1 comments Delete Note Image Other As in: other type of post Post Noun, not verb Via %1 Posted on %1 1=Date To CC Comment on this post. If you select some text, it will be quoted. Unshare Unshare this post Delete this post Open %1's profile in web browser Open post in web browser Copy post link to clipboard Normalize text colors Stop following Follow Image is loading... %1 likes this One person %1 like this More than one person %1 shared this %1 = One person name %1 shared this %1 = Names for more than one person Shared once You like this Unlike Share post? Do you want to share %1's post? &Yes, share it &No Unshare post? Do you want to unshare %1's post? &Yes, unshare it WARNING: Delete post? Link to: %1 Stop following? Are you sure you want to stop following %1? &Yes, stop following Are you sure you want to delete this post? &Yes, delete it Click the image to see it in full size ProfileEditor Profile Editor This is your Pump address This is the e-mail address associated with your account, for things such as notifications and password recovery Change &Avatar... This is your visible name &Save Profile &Cancel Webfinger ID E-mail Avatar Full &Name &Hometown &Bio Not set In reference to the e-mail not being set for the account Select avatar image Image files All files Invalid image The selected image is not valid. Publisher Title Title for the post. Setting a title helps make the Meanwhile feed more informative. Select Picture... Find the picture in your folders Public Followers Lists To... CC... Select who will get a copy of this post Ad&d Picture Upload photo Post Cancel Cancel the post Picture not set Error: Already composing You can't edit a post at this time, because a post is already being composed. Update Editing post Posting failed. Try again. Updating... Post is empty. Select one image Image files All files Resolution Size Invalid image The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Posting... Optional title for the post People... Select who will see this post Hit Control+Enter to post with the keyboard PumpController Creating person list... Deleting person list... Getting a person list... Adding person to list... Removing person from list... Main timeline update requested, but updates are blocked. Direct timeline update requested, but updates are blocked. Getting direct messages timeline... Activity timeline update requested, but updates are blocked. Favorites timeline update requested, but updates are blocked. Getting favorites timeline... Getting likes... Getting comments... Getting minor feed... Service Unavailable HTTP 503 error string Internal Server Error HTTP 500 error string Gone HTTP 410 error string Not Found HTTP 404 error string Forbidden HTTP 403 error string Unauthorized HTTP 401 error string Bad Request HTTP 400 error string Moved Temporarily HTTP 302 error string Moved Permanently HTTP 301 error string Error connecting to %1 Unhandled HTTP error code %1 Profile received. Profile updated. Post published successfully. Avatar published successfully. Post updated successfully. Comment posted successfully. Post shared successfully. Minor feed received. Following successfully. Stopped following successfully. List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. Person list created successfully. Person list deleted successfully. Person list received. Person added to list successfully. Person removed from list successfully. File uploaded successfully. Posting message... Timeline received. Updating post list... Getting list of 'Following'... Getting list of 'Followers'... Getting list of person lists... Getting main timeline... Getting activity timeline... Message liked or unliked successfully. Likes received. Comments received. Message deleted successfully. List of 'lists' received. Avatar uploaded. Ready. TimeLine Welcome to Dianara Dianara is a <b>pump.io</b> client. If you don't have a Pump account yet, you can get one at the following address: First, configure your account from the <b>Settings - Account</b> menu. After the process is done, your profile and timelines should update automatically. Take a moment to look around the menus and the Configuration window. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Frequently asked questions about pump.io Direct Messages Timeline Here, you'll see posts specifically directed to you. Activity Timeline You'll see your own posts here. Favorites Timeline Posts and comments you've liked. F&irst Page &Previous Page &Next Page Timestamp Invalid timestamp! Less than a minute ago A minute ago %1 minutes ago An hour ago %1 hours ago A day ago %1 days ago A month ago %1 months ago A year ago %1 years ago dianara-v1.1/translations/dianara_it.ts000644 000764 000764 00000301576 12264101444 017730 0ustar00janjan000000 000000 ASActivity Public Pubblico AccountDialog Your Pump.io address: Il tuo indirizzo Pump.io: Webfinger ID, like username@pumpserver.org Webfinger ID, esempio nomeutente@pumpserver.org Your address, as username@server Il tuo indirizzo, come nomeutente@server Get &Verifier Code Ottieni il Codice di &Verifica Verifier code: Codice di Verifica: &Save Details &Salva i Dati If the browser doesn't open automatically, copy this address manually Se il browser non si apre automaticamente, copia questo indirizzo manualmente Account Configuration Configurazione Account First, enter your Webfinger ID, your pump.io address. Prima inserisci il tuo Webfinger ID, il tuo indirizzo Pump.io. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. Il tuo indirizzo assomiglia a nomeutente@pumpserver.org, e lo puoi trovare sul tuo profilo, nell'interfaccia web. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example Se il tuo profilo, per esempio, è https://pumpserver.org/nomeutente, di conseguenza il tuo indirizzo è nomeutente@pumpserver.org After clicking this button, a web browser will open, requesting authorization for Dianara Dopo aver cliccato questo bottone, il browser web si dovrebbe aprire, richiedendo l'autorizzazione per Dianara Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. Don't translate the VERIFIER word! Una volta autorizzato Dianara dall'interfaccia web del tuo server Pump.io, riceverai un codice chiamato VERIFIER. Copialo e incollalo nel campo qui sotto. Enter the verifier code provided by your Pump server here Inserisci il codice di verifica ottenuto dal tuo server Pump.io qui Paste the verifier here Incolla il codice di verifica qui &Authorize Application &Autorizza Applicazione &Cancel &Cancella A web browser will start now, where you can get the verifier code Ora si avviera il browser web, dove potrai ottenere il codice di verifica Your Pump address is invalid Il tuo indirizzo Pump.io non è valido Verifier code is empty Il campo del codice di verifica è vuoto Dianara is authorized to access your data Dianara è autorizzato ad acceder ai tuoi dati AudienceSelector 'To' List Lista 'A' 'CC' List Lista 'CC' &Search: &Cerca: Enter a name here to search for it Inserisci qui un nome per cercarlo &Add to Selected &Aggiungi ai Selezionati All Contacts Tutti i Contatti Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below. Seleziona le persone dalla lista a sinistra. Puoi trascinarle con il mouse, click o doppio click su di esse, o selezionarle e usare il bottone qui sotto. Clear &List Pulisci la &Lista &Done &Fatto &Cancel &Cancella Selected People Persone Selezionate Comment Like or unlike this comment Decidi se ti piace o non ti piace questo commento Quote This is a verb, infinitive Quotare Reply quoting this comment Rispondi quotando questo commento Delete Elimina Delete this comment Elimina questo commento Unlike Non mi piace Like Mi piace %1 like this comment Plural: %1=list of people like John, Jane, Smith A %1 piace questo commento %1 likes this comment Singular: %1=name of just 1 person A %1 piace questo commento WARNING: Delete comment? ATTENZIONE: Cancellare il commento? Are you sure you want to delete this comment? Sei sicuro di voler cancellare questo commento? &Yes, delete it &Si, cancellalo &No &No CommenterBlock Show All Comments Mostra Tutti i Commenti &Comment &Commenta You can press Control+Enter to send the comment with the keyboard Premi Control+Invio per inviare il commento con la tastiera C&ancel &Annulla Press ESC to cancel the comment if there is no text Premi ESC per annullare il commento, se non c'è testo Posting comment failed. Try again. Invio del commento fallito. Prova di nuovo. Sending comment... Inviando il commento... Comment is empty. Il commento è vuoto. Composer Type a message here to post it Scrivi qui un messaggio per pubblicarlo Click here or press Control+N to post a note... Clicca qui o premi Control+N per pubblicare una nota... Symbols Simboli Formatting Formattazione Normal Normale Bold Grassetto Italic Corsivo Underline Sottolineato Strikethrough Barrato Header Titolo Preformatted block Blocco preformattato Quote block Blocco citazioni Make a link Crea un link Insert an image from a web site Inserisci un'immagine da un sito web Insert line Inserisci una linea Cancel Post Cancella la nota &Format Button for text formatting and related options &Formato &F F for formatting, translate accordingly &F Text Formatting Options Opzioni di formattazione del testo Paste Text Without Formatting Incolla testo senza formattazione Type a comment here Scrivi un commento qui Insert a link Inserisci un link Type or paste a web address here. Digita o incolla un indirizzo web qui. Make a link from selected text Crea un link con il testo selezionato Type or paste a web address here. The selected text (%1) will be converted to a link. Digita o incolla un indirizzo web qui. Il testo selezionato (%1) sarà convertito in un link. Insert an image from a URL Inserisci un'immagine da un URL Type or paste the image address here. Digita o incolla l'indirizzo di un'immagine qui. Error: Invalid URL Errore: URL non valido The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// L'indirizzo inserito (%1) non è valido. Gli indirizzi di un'immagine dovrebbero iniziare con http:// o https:// Cancel message? Cancellare il messaggio? Are you sure you want to cancel this message? Sei sicuro di voler cancellare questo messaggio? &Yes, cancel it &Si, cancellalo &No &No ConfigDialog minutes minuti posts messaggi Top In alto Bottom In basso Left side A sinistra Right side A destra Buttons below Bottoni di sotto Buttons around Bottoni intorno Buttons on right side Bottoni a destra Restart program to apply this change Riavvia il programma per applicare le modifiche Program Configuration Configurazione del programma Timeline &update interval Intervallo di &aggiornamento della timeline &Posts per page &Messaggi per pagina &Tabs position Posizione delle &tab &Movable tabs &Tab mobili C&omposer type Tipo di &editor posts Goes after a number, as: 25 posts elementi &Posts per page, main timeline &Elementi per pagina, timeline principale posts This goes after a number, like: 10 posts elementi Posts per page, &other timelines Elementi per pagina, &altre timeline Public posts as &default Messaggi pubblici come &default As system notifications Come notifiche di sistema Using own notifications Usando proprie notifiche Don't show notifications Non mostrare notifiche Show &notifications Mostra &notifiche Dianara stores data in this folder: Dianara salva i dati in questa cartella: &Save Configuration &Salva Configurazione &Cancel &Cancella ContactCard Hometown Città Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name Bio di %1 This user doesn't have a biography Questo utente non ha una biografia No biography for %1 %1=contact name Nessuna biografia per %1 Open Profile in Web Browser Apri il profilo nel browser web In Lists... Nelle liste... User Options Opzioni utente Follow Segui Stop Following Smetti di seguire Stop following? Smettere di seguire? Are you sure you want to stop following %1? Sei sicuro di voler smettere di seguire %1? &Yes, stop following &Si, smetti di seguire &No &No ContactList &Follow &Segui Reload Following Ricarica Following Reload Followers Ricarica Followers Export Following Esporta Following Export Followers Esporta Followers username@server.org or https://server.org/username nomeutente@pumpserver.org o https://pumpserver.org/nomeutente &Enter address to follow: &Inserisci l'indirizzo da seguire: Optio&ns Op&zioni Followin&g Foll&owing Follo&wers Follo&wers Reload Lists Ricarica Liste &Lists &Liste Export list of 'following' to a file Esporta la lista dei 'Following' in un file Export list of 'followers' to a file Esporta la lista dei 'Followers' in un file FDNotifications Dianara Notification Notifiche di Dianara FilterEditor Filter Editor Modifica Filtri Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. Qui puoi impostare alcuni filtri. Puoi filtrare gli elementi per contenuto, autore o applicazione. Per esempio, puoi nascondere i messaggi postati dall'applicazione Open Farm Game, o quelli che contengono NSFW nel messaggio. Content Contenuto Author Autore Application Applicazione Words to match Parole da controllare At the moment, these filters only apply to the Meanwhile feed. Al momento, questi filtri si possono applicare solo alla timeline "Nel Frattempo...". Keywords... Parole chiave... &Add Filter &Aggiungi Filtro Filters in use Filtri in uso &Remove Selected Filter &Rimuovi i Filtri Selezionati &Save Filters &Salva Filtri &Cancel &Annulla &New Filter &Nuovo Filtro C&urrent Filters Filt&ri Salvati ImageViewer Image Immagine ESC to close, secondary-click for options ESC per chiudere, click destro per opzioni Save Image... Salvataggio Immagine... Close Viewer Chiudi Visualizzatore Save Image As... Salva Immagine Come... Image files Files Immagine All files Tutti i Files Error saving image Errore durante il salvataggio dell'immagine There was a problem while saving %1. Filename should end in .jpg or .png extensions. C'è stato un problema salvando %1. L'estensione del file dovrebbe essere .jpg o .png. ListsManager Name Nome Members Membri Add Mem&ber Aggiungi Mem&bro &Remove Member &Rimuovi Membro &Delete Selected List &Cancella la Lista Selezionata Add New &List Aggiungi Nuova &Lista Create L&ist Crea L&ista &Add to List &Aggiungi alla Lista Are you sure you want to delete %1? 1=Name of a person list Sei sicuro di voler cancellare %1? Remove person from list? Rimuovere la persona dalla lista? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list Sei sicuro di voler rimuovere %1 dalla lista %2? &Yes &Si Delete Selected List Elimina la Lista Selezionata Add New List Aggiungi una Nuova Lista Type a name for the new list... Digita un nome per la nuova lista... Type an optional description here Digita una descrizione opzionale qui Create List Crea Lista WARNING: Delete list? ATTENZIONE: Cancellare la lista? Are you sure you want to delete this list? Sei sicuro di voler cancellare questa lista? &Yes, delete it &Si, cancellala &No &No MainWindow Meanwhile... Nel frattempo... Side &Panel Pannello &laterale Status &Bar &Barra di stato Dianara Notification Notifiche di Dianara &Timeline T&imeline The main timeline Timeline principale &Activity &Attività Your own posts I tuoi messaggi Your favorited posts I tuoi messaggi preferiti &Messages &Messaggi Messages sent explicitly to you Messaggi inviati esplicitamente a te &Contacts &Contatti The people you follow, and the ones who follow you Persone che segui e che ti seguono Initializing... Inizializzando... Your account is not configured yet. Il tuo account non è ancora stato configurato. &Session &Sessione &Update Main Timeline Agg&iorna la timeline principale Update &Messages Timeline Aggiorna la timeline dei &messaggi Update &Activity Timeline Aggiorna la timeline delle &attività Update Favorites &Timeline Aggiorna la timeline dei pre&feriti Update Minor &Feed Aggiorna la timeline late&rale Update All Timelines Aggiorna tutte le timeline Mark All as Read Segna tutti come letti &Post a Note &Pubblica una nota &Quit &E&sci &View &Visualizzazione Full &Screen Sc&hermo Intero S&ettings Configura&zione Edit &Profile Modifica &profilo &Account A&ccount &Filters &Filtri &Frequently Asked Questions about Pump.io Domande &frequenti su pump.io Timeline updated at %1. Timeline aggiornata alle %1. Your Pump.io account is not configured Il tuo account Pump.io non è configurato Dianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) Dianara è distribuita su licenza GNU GPL, ed utilizza alcune icone Oxygen: http://www.oxygen-icons.org/ (licenza LGPL) &Configure Dianara Configura &Dianara &Help Ai&uto Visit &Website Visita il sito &web About &Dianara Informazioni su &Dianara &Show Window &Mostra la finestra There is 1 new post. C'è 1 nuovo messaggio. There are %1 new posts. Ci sono %1 nuovi messaggi. Timeline updated. Timeline aggiornata. Timeline updated. No new posts. Timeline aggiornata. Nessun nuovo messaggio. Fav&orites Pref&eriti About Dianara Informazioni su Dianara Dianara is a pump.io social networking client. Dianara è un client per il social network pump.io. With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. Con Dianara puoi vedere le tue timelines, creare nuove note, caricare immagini, interagire con messaggi, gestire i tuoi contatti e seguire nuove persone. Thanks to all the testers, translators and packagers, who help make Dianara better! Grazie a tutti i tester, i traduttori e i manutentori dei pacchetti, che hanno aiutato Dianara a migliorare! English translation by JanKusanagi. TRANSLATORS: Change this with your language and name ;) Traduzione italiana a cura di Metal Biker (howcanuhavemyusername@microca.st). MinorFeed Get More Più Elementi Get older activities Guarda attività meno recenti There are no activities to show yet. Non ci sono ancora attività da mostrare. MinorFeedItem using %1 Application used to generate this activity via %1 Open referenced post Apri l'elemento relativo MiscHelpers bytes bytes PeopleWidget &Search: &Cerca: Enter a name here to search for it Inserisci qui un nome per cercarlo Add a contact to a list Aggiungi un contatto ad una lista &Cancel &Cancella Post Hometown Città Like Mi piace Like this post Dì che ti piace questo messaggio Note Nota Image Immagine Other As in: other type of post Altro Post Noun, not verb Pubblica 1 like 1 mi piace 1 comment 1 commento Shared %1 times Condiviso %1 volte Comment Commentare Post noun, not verb Pubblica Shared on %1 Condiviso su %1 using %1 via %1 using %1 1=Program used for posting via %1 Edited: %1 Modificato: %1 Edited on %1 Modificato su %1 In In Share Condividi Share this post Condividi questo messaggio Edit Modifica Edit this post Modifica questo messaggio %1 likes %1 mi piace %1 comments %1 commenti Delete Elimina Post Pubblica Via %1 Via %1 Posted on %1 1=Date Pubblicato il %1 Posted on %1 using %2 1=Date of post, 2=Program used for posting Pubblicato il %1 utilizzando %2 To A CC CC Comment on this post. Commenta questo messaggio. If you select some text, it will be quoted. Se selezioni del testo, verrà quotato. Unshare Rimuovi condivisione Unshare this post Rimuovi la condivisione di questo elemento Delete this post Elimina questo messaggio Open %1's profile in web browser Apri il profilo di %1 nel browser web Open post in web browser Apri il messaggio nel browser web Copy post link to clipboard Copia il link al messaggio negli appunti Normalize text colors Normalizza i colori del testo Stop following Smetti di seguire Follow Segui Image is loading... Immagine in caricamento... %1 likes this One person A %1 piace questo elemento %1 like this More than one person A %1 piace questo elemento %1 shared this %1 = One person name %1 ha condiviso questo elemento %1 shared this %1 = Names for more than one person %1 hanno condiviso questo elemento Shared once Condiviso una volta You like this Ti piace Unlike Non mi piace più Share post? Condividi il messaggio? Do you want to share %1's post? Vuoi condividere il messaggio di %1? &Yes, share it &Si, condividilo &No &No Unshare post? Rimuovere la condivisione di questo elemento? Do you want to unshare %1's post? Vuoi rimuovere la condivisione dell'elemento di %1? &Yes, unshare it &Si, non condividerlo WARNING: Delete post? ATTENZIONE: Eliminare il messaggio? Link to: %1 Link a: %1 Stop following? Smettere di seguire? Are you sure you want to stop following %1? Sei sicuro di voler smettere di seguire %1? &Yes, stop following &Si, smetti di seguire Are you sure you want to delete this post? Sei sicuro di voler eliminare questo messaggio? &Yes, delete it &Si, eliminalo Click the image to see it in full size Clicca l'immagine per vederla nelle dimensioni originali ProfileEditor Profile Editor Editor del profilo This is your Pump address Questo è il tuo indirizzo Pump.io This is the e-mail address associated with your account, for things such as notifications and password recovery Questa è l'indirizzo e-mail associato al tuo accout, per inviare notifiche e recuperare la password Change &Avatar... Cambia &avatar... This is your visible name Questo è il tuo nome visibile &Save Profile &Salva profilo &Cancel &Cancella Webfinger ID Webfinger ID E-mail E-mail Avatar Avatar Full &Name &Nome Completo &Hometown Ci&ttà &Bio &Bio Not set In reference to the e-mail not being set for the account Non impostata Select avatar image Scegli l'immagine per l'avatar Image files Files immagine All files Tutti i files Invalid image Immagine non valida The selected image is not valid. L'immagine selezionata non è valida. Publisher Title Titolo Title for the post. Setting a title helps make the Meanwhile feed more informative. Titolo dell'elemento. Scrivere un titolo aiuta a rendere più immediato il feed della timeline laterale "nel frattempo...". Select Picture... Selezionare l'immagine... Find the picture in your folders Trova l'immagine nelle tue cartelle Public Pubblico Followers Followers Lists Liste To... A... CC... CC... Select who will get a copy of this post Scegli chi riceverà una copia di questo messaggio Ad&d Picture A&ggiungi un'immagine Upload photo Carica un'immagine Post Pubblica Cancel Cancella Cancel the post Cancella il messaggio Picture not set Immagine non selezionata Error: Already composing Errore: stai già scrivendo You can't edit a post at this time, because a post is already being composed. Non puoi modificare il messaggio ora, perchè stai già scrivendo un messaggio. Update Aggiorna Editing post Modifica il messaggio Posting failed. Try again. Invio fallito. Prova di nuovo. Updating... Aggiornando... Post is empty. Il messaggio è vuoto. Select one image Scegli un'immagine Image files Files immagine All files Tutti i files Resolution Risoluzione Size Dimensioni Invalid image Immagine non valida The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Il formato dell'immagine non è stato riconosciuto. L'estensione può essere sbagliata, come un'immagine GIF rinominata in immagine.jpg o altro. Posting... Pubblicando... Title: Titolo: Optional title for the post Titolo opzionale per il messaggio Title for the post Titolo per il messaggio People... Persone... Select who will see this post Scegli chi potrà vedere il messaggio Hit Control+Enter to post with the keyboard Premi Control+Invio per pubblicare con la tastiera PumpController Creating person list... Creando la lista delle persone... Deleting person list... Eliminando la lista delle persone... Getting a person list... Ottenendo la lista della persona... Adding person to list... Aggiungendo una persona alla lista... Removing person from list... Rimuovendo una persona dalla lista... Main timeline update requested, but updates are blocked. Richiesto un aggiornamento della timeline principale, ma gli aggiornamenti sono bloccati. Direct timeline update requested, but updates are blocked. Richiesto un aggiornamento dei messaggi diretti, ma gli aggiornamenti sono bloccati. Getting direct messages timeline... Ricevendo la timeline dei messaggi diretti... Activity timeline update requested, but updates are blocked. Richiesto un aggiornamento della timeline delle attività, ma gli aggiornamenti sono bloccati. Favorites timeline update requested, but updates are blocked. Richiesto un aggiornamento dei preferiti, ma gli aggiornamenti sono bloccati. Getting favorites timeline... Ricevendo la timeline dei preferiti... Getting likes... Ricevendo i "mi piace"... Getting comments... Ricevendo i commenti... Getting minor feed... Ricevendo la timeline laterale... Service Unavailable HTTP 503 error string Servizio non Disponibile (Errore 503) Internal Server Error HTTP 500 error string Errore Interno del Server (Errore 500) Gone HTTP 410 error string Sparito (Errore 410) Not Found HTTP 404 error string Non Trovato (Errore 404) Forbidden HTTP 403 error string Proibito (Errore 403) Unauthorized HTTP 401 error string Non Autorizzato (Errore 401) Bad Request HTTP 400 error string Richiesta Errata (Errore 400) Moved Temporarily HTTP 302 error string Spostato Temporaneamente (Codice 302) Moved Permanently HTTP 301 error string Spostato Permanentemente (Codice 301) Error connecting to %1 Errore durante la connessione a %1 Unhandled HTTP error code %1 Codice di errore HTTP non gestito: %1 Profile received. Profilo ricevuto. Profile updated. Profilo aggiornato. Post published successfully. Messaggio pubblicato con successo. Avatar published successfully. Avatar pubblicato con successo. Post updated successfully. Messaggio aggiornato con successo. Comment posted successfully. Commento pubblicato con successo. Post shared successfully. Messaggio condiviso con successo. Minor feed received. Timeline laterale ricevuta. Following successfully. Following riuscito. Stopped following successfully. Rimozione following riuscita. List of 'following' completely received. Lista dei 'following' ricevuta completamente. Partial list of 'following' received. Lista dei 'following' ricevuta parzialmente. List of 'followers' completely received. Lista dei 'followers' ricevuta completamente. Partial list of 'followers' received. Lista dei 'followers' ricevuta parzialmente. Person list created successfully. Lista di persone creata correttamente. Person list deleted successfully. Lista di persone eliminata correttamente. Person list received. Lista di persone ricevuta. Person added to list successfully. Persona aggiunta alla lista con successo. Person removed from list successfully. Persona rimossa dalla lista con successo. File uploaded successfully. Posting message... File caricato con successo. Pubblicando il messaggio... Timeline received. Updating post list... Timeline ricevuta. Aggiornando la lista dei messaggi... Getting list of 'Following'... Ricevendo la lista dei 'Following'... Getting list of 'Followers'... Ricevendo la lista dei 'Followers'... Getting list of person lists... Rocevendo le liste delle persone... Getting main timeline... Ricevendo la timeline principale... Getting activity timeline... Ricevendo la timeline delle attività... Message liked or unliked successfully. Messaggio aggiunto o rimosso dai "Mi piace" correttamente. Likes received. "Mi piace" ricevuto. Comments received. Commento ricevuto. Message deleted successfully. Messaggio cancellato correttamente. List of 'following' received. Lista dei 'Following' ricevuta. List of 'followers' received. Lista dei 'Followers' ricevuta. List of 'lists' received. Lista delle 'Liste' ricevuta. Avatar uploaded. Avatar caricato. Ready. Pronto. TimeLine Welcome to Dianara Benvenuto in Dianara Dianara is a <b>pump.io</b> client. Dianara è un client <b>pump.io</b>. If you don't have a Pump account yet, you can get one at the following address: Se non hai ancora un account Pump.io, ne puoi ottenere uno al seguente indirizzo: First, configure your account from the <b>Settings - Account</b> menu. Prima di tutto, configura il tuo account nel menù <b>Configurazione - Account</b>. After the process is done, your profile and timelines should update automatically. Completato il processo, il tuo profilo e le timelines dovrebbero aggiornarsi automaticamente. Take a moment to look around the menus and the Configuration window. Prenditi un momento per dare uno sguardo ai menù ed alla finestra di Configurazione. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. Puoi anche impostare i dettagli e la foto del tuo profilo nel menu <b>Configurazione - Modifica profilo</b>. There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. Ci sono tooltips (popup descrittivi) ovunque, quindi se ti porti sopra un bottone con il mouse otterrai informazioni aggiuntive. Dianara's blog Blog di Dianara Frequently asked questions about pump.io Domande frequenti (FAQ) su pump.io Direct Messages Timeline Timeline dei messaggi diretti Here, you'll see posts specifically directed to you. Qui potrai vedere messaggi indirizzati specificatamente a te. Activity Timeline Timeline delle Attività You'll see your own posts here. Qui vedrai i tuoi messaggi. Favorites Timeline Timeline dei Preferiti Posts and comments you've liked. Messaggi e commenti che ti sono piaciuti. F&irst Page &Prima pagina &Previous Page P&agina Precedente &Next Page Pro&ssima Pagina Public Pubblico Timestamp Invalid timestamp! Timestamp non Valida! Less than a minute ago Meno di un minuto fa A minute ago Un minuto fa %1 minutes ago %1 minuti fa An hour ago Un'ora fa %1 hours ago %1 ore fa A day ago Un giorno fa %1 days ago %1 giorni fa A month ago Un mese fa %1 months ago %1 mesi fa A year ago Un anno fa %1 years ago %1 anni fa dianara-v1.1/translations/dianara_ru.ts000644 000764 000764 00000260217 12264101305 017732 0ustar00janjan000000 000000 ASActivity Public AccountDialog Your Pump.io address: Webfinger ID, like username@pumpserver.org Your address, as username@server Get &Verifier Code Verifier code: &Save Details If the browser doesn't open automatically, copy this address manually Account Configuration First, enter your Webfinger ID, your pump.io address. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example After clicking this button, a web browser will open, requesting authorization for Dianara Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. Don't translate the VERIFIER word! Enter the verifier code provided by your Pump server here Paste the verifier here &Authorize Application &Cancel A web browser will start now, where you can get the verifier code Your Pump address is invalid Verifier code is empty Dianara is authorized to access your data AudienceSelector 'To' List 'CC' List &Add to Selected All Contacts Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below. Clear &List &Done &Cancel Selected People Comment Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Delete Delete this comment Unlike Like %1 like this comment Plural: %1=list of people like John, Jane, Smith %1 likes this comment Singular: %1=name of just 1 person WARNING: Delete comment? Are you sure you want to delete this comment? &Yes, delete it &No CommenterBlock Show All Comments &Comment You can press Control+Enter to send the comment with the keyboard C&ancel Press ESC to cancel the comment if there is no text Posting comment failed. Try again. Sending comment... Comment is empty. Composer Type a message here to post it Click here or press Control+N to post a note... Symbols Formatting Normal Bold Italic Underline Strikethrough Header Preformatted block Quote block Make a link Insert an image from a web site Insert line &Format Button for text formatting and related options Text Formatting Options Paste Text Without Formatting Type a comment here Insert a link Type or paste a web address here. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Insert an image from a URL Type or paste the image address here. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog minutes Top Bottom Left side Right side Program Configuration Timeline &update interval posts Goes after a number, as: 25 posts &Posts per page, main timeline posts This goes after a number, like: 10 posts Posts per page, &other timelines &Tabs position &Movable tabs Public posts as &default As system notifications Using own notifications Don't show notifications Show &notifications Dianara stores data in this folder: &Save Configuration &Cancel ContactCard Hometown Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name This user doesn't have a biography No biography for %1 %1=contact name Open Profile in Web Browser In Lists... User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList &Follow Reload Following Reload Followers Export Following Export Followers username@server.org or https://server.org/username &Enter address to follow: Optio&ns Followin&g Follo&wers Reload Lists &Lists Export list of 'following' to a file Export list of 'followers' to a file FDNotifications Dianara Notification FilterEditor Filter Editor Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. At the moment, these filters only apply to the Meanwhile feed. Content Author Application Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel &New Filter C&urrent Filters ImageViewer Image ESC to close, secondary-click for options Save Image... Close Viewer Save Image As... Image files All files Error saving image There was a problem while saving %1. Filename should end in .jpg or .png extensions. ListsManager Name Members Add Mem&ber &Remove Member &Delete Selected List Add New &List Create L&ist &Add to List Are you sure you want to delete %1? 1=Name of a person list Remove person from list? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list &Yes Type a name for the new list... Type an optional description here WARNING: Delete list? &Yes, delete it &No MainWindow Meanwhile... Side &Panel Status &Bar Dianara Notification &Timeline The main timeline &Activity Your own posts Your favorited posts &Messages Messages sent explicitly to you &Contacts The people you follow, and the ones who follow you Initializing... Your account is not configured yet. &Session &Update Main Timeline Update &Messages Timeline Update &Activity Timeline Update Favorites &Timeline Update Minor &Feed Update All Timelines Mark All as Read &Post a Note &Quit &View Full &Screen S&ettings Edit &Profile &Account &Filters &Frequently Asked Questions about Pump.io Timeline updated at %1. Your Pump.io account is not configured Dianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) &Configure Dianara &Help Visit &Website About &Dianara &Show Window There is 1 new post. There are %1 new posts. Timeline updated. Timeline updated. No new posts. Fav&orites About Dianara Dianara is a pump.io social networking client. With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. Thanks to all the testers, translators and packagers, who help make Dianara better! English translation by JanKusanagi. TRANSLATORS: Change this with your language and name ;) MinorFeed Get More Get older activities There are no activities to show yet. MinorFeedItem using %1 Application used to generate this activity Open referenced post MiscHelpers bytes PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Hometown Like Like this post 1 like 1 comment Shared %1 times Comment Shared on %1 using %1 using %1 1=Program used for posting Edited: %1 Edited on %1 In Share Share this post Edit Edit this post %1 likes %1 comments Delete Note Image Other As in: other type of post Post Noun, not verb Via %1 Posted on %1 1=Date To CC Comment on this post. If you select some text, it will be quoted. Unshare Unshare this post Delete this post Open %1's profile in web browser Open post in web browser Copy post link to clipboard Normalize text colors Stop following Follow Image is loading... %1 likes this One person %1 like this More than one person %1 shared this %1 = One person name %1 shared this %1 = Names for more than one person Shared once You like this Unlike Share post? Do you want to share %1's post? &Yes, share it &No Unshare post? Do you want to unshare %1's post? &Yes, unshare it WARNING: Delete post? Link to: %1 Stop following? Are you sure you want to stop following %1? &Yes, stop following Are you sure you want to delete this post? &Yes, delete it Click the image to see it in full size ProfileEditor Profile Editor This is your Pump address This is the e-mail address associated with your account, for things such as notifications and password recovery Change &Avatar... This is your visible name &Save Profile &Cancel Webfinger ID E-mail Avatar Full &Name &Hometown &Bio Not set In reference to the e-mail not being set for the account Select avatar image Image files All files Invalid image The selected image is not valid. Publisher Title Title for the post. Setting a title helps make the Meanwhile feed more informative. Select Picture... Find the picture in your folders Public Followers Lists To... CC... Select who will get a copy of this post Ad&d Picture Upload photo Post Cancel Cancel the post Picture not set Error: Already composing You can't edit a post at this time, because a post is already being composed. Update Editing post Posting failed. Try again. Updating... Post is empty. Select one image Image files All files Resolution Size Invalid image The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Posting... Optional title for the post People... Select who will see this post Hit Control+Enter to post with the keyboard PumpController Creating person list... Deleting person list... Getting a person list... Adding person to list... Removing person from list... Main timeline update requested, but updates are blocked. Direct timeline update requested, but updates are blocked. Getting direct messages timeline... Activity timeline update requested, but updates are blocked. Favorites timeline update requested, but updates are blocked. Getting favorites timeline... Getting likes... Getting comments... Getting minor feed... Service Unavailable HTTP 503 error string Internal Server Error HTTP 500 error string Gone HTTP 410 error string Not Found HTTP 404 error string Forbidden HTTP 403 error string Unauthorized HTTP 401 error string Bad Request HTTP 400 error string Moved Temporarily HTTP 302 error string Moved Permanently HTTP 301 error string Error connecting to %1 Unhandled HTTP error code %1 Profile received. Profile updated. Post published successfully. Avatar published successfully. Post updated successfully. Comment posted successfully. Post shared successfully. Minor feed received. Following successfully. Stopped following successfully. List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. Person list created successfully. Person list deleted successfully. Person list received. Person added to list successfully. Person removed from list successfully. File uploaded successfully. Posting message... Timeline received. Updating post list... Getting list of 'Following'... Getting list of 'Followers'... Getting list of person lists... Getting main timeline... Getting activity timeline... Message liked or unliked successfully. Likes received. Comments received. Message deleted successfully. List of 'lists' received. Avatar uploaded. Ready. TimeLine Welcome to Dianara Dianara is a <b>pump.io</b> client. If you don't have a Pump account yet, you can get one at the following address: First, configure your account from the <b>Settings - Account</b> menu. After the process is done, your profile and timelines should update automatically. Take a moment to look around the menus and the Configuration window. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Frequently asked questions about pump.io Direct Messages Timeline Here, you'll see posts specifically directed to you. Activity Timeline You'll see your own posts here. Favorites Timeline Posts and comments you've liked. F&irst Page &Previous Page &Next Page Timestamp Invalid timestamp! Less than a minute ago A minute ago %1 minutes ago An hour ago %1 hours ago A day ago %1 days ago A month ago %1 months ago A year ago %1 years ago dianara-v1.1/translations/dianara_gl.ts000644 000764 000764 00000260217 12264101304 017705 0ustar00janjan000000 000000 ASActivity Public AccountDialog Your Pump.io address: Webfinger ID, like username@pumpserver.org Your address, as username@server Get &Verifier Code Verifier code: &Save Details If the browser doesn't open automatically, copy this address manually Account Configuration First, enter your Webfinger ID, your pump.io address. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example After clicking this button, a web browser will open, requesting authorization for Dianara Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. Don't translate the VERIFIER word! Enter the verifier code provided by your Pump server here Paste the verifier here &Authorize Application &Cancel A web browser will start now, where you can get the verifier code Your Pump address is invalid Verifier code is empty Dianara is authorized to access your data AudienceSelector 'To' List 'CC' List &Add to Selected All Contacts Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below. Clear &List &Done &Cancel Selected People Comment Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Delete Delete this comment Unlike Like %1 like this comment Plural: %1=list of people like John, Jane, Smith %1 likes this comment Singular: %1=name of just 1 person WARNING: Delete comment? Are you sure you want to delete this comment? &Yes, delete it &No CommenterBlock Show All Comments &Comment You can press Control+Enter to send the comment with the keyboard C&ancel Press ESC to cancel the comment if there is no text Posting comment failed. Try again. Sending comment... Comment is empty. Composer Type a message here to post it Click here or press Control+N to post a note... Symbols Formatting Normal Bold Italic Underline Strikethrough Header Preformatted block Quote block Make a link Insert an image from a web site Insert line &Format Button for text formatting and related options Text Formatting Options Paste Text Without Formatting Type a comment here Insert a link Type or paste a web address here. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Insert an image from a URL Type or paste the image address here. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog minutes Top Bottom Left side Right side Program Configuration Timeline &update interval posts Goes after a number, as: 25 posts &Posts per page, main timeline posts This goes after a number, like: 10 posts Posts per page, &other timelines &Tabs position &Movable tabs Public posts as &default As system notifications Using own notifications Don't show notifications Show &notifications Dianara stores data in this folder: &Save Configuration &Cancel ContactCard Hometown Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name This user doesn't have a biography No biography for %1 %1=contact name Open Profile in Web Browser In Lists... User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList &Follow Reload Following Reload Followers Export Following Export Followers username@server.org or https://server.org/username &Enter address to follow: Optio&ns Followin&g Follo&wers Reload Lists &Lists Export list of 'following' to a file Export list of 'followers' to a file FDNotifications Dianara Notification FilterEditor Filter Editor Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. At the moment, these filters only apply to the Meanwhile feed. Content Author Application Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel &New Filter C&urrent Filters ImageViewer Image ESC to close, secondary-click for options Save Image... Close Viewer Save Image As... Image files All files Error saving image There was a problem while saving %1. Filename should end in .jpg or .png extensions. ListsManager Name Members Add Mem&ber &Remove Member &Delete Selected List Add New &List Create L&ist &Add to List Are you sure you want to delete %1? 1=Name of a person list Remove person from list? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list &Yes Type a name for the new list... Type an optional description here WARNING: Delete list? &Yes, delete it &No MainWindow Meanwhile... Side &Panel Status &Bar Dianara Notification &Timeline The main timeline &Messages Messages sent explicitly to you &Activity Your own posts Your favorited posts &Contacts The people you follow, and the ones who follow you Initializing... Your account is not configured yet. &Session &Update Main Timeline Update &Messages Timeline Update &Activity Timeline Update Favorites &Timeline Update Minor &Feed Update All Timelines Mark All as Read &Post a Note &Quit &View Full &Screen S&ettings Edit &Profile &Account &Filters &Frequently Asked Questions about Pump.io Timeline updated at %1. Your Pump.io account is not configured Dianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) &Configure Dianara &Help Visit &Website About &Dianara &Show Window There is 1 new post. There are %1 new posts. Timeline updated. Timeline updated. No new posts. Fav&orites About Dianara Dianara is a pump.io social networking client. With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. Thanks to all the testers, translators and packagers, who help make Dianara better! English translation by JanKusanagi. TRANSLATORS: Change this with your language and name ;) MinorFeed Get More Get older activities There are no activities to show yet. MinorFeedItem using %1 Application used to generate this activity Open referenced post MiscHelpers bytes PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Via %1 Shared on %1 using %1 Hometown using %1 1=Program used for posting Edited: %1 Posted on %1 1=Date Edited on %1 In To CC Comment Note Image Other As in: other type of post Post Noun, not verb Comment on this post. If you select some text, it will be quoted. Share Share this post Unshare Unshare this post Edit Edit this post Delete Delete this post Open %1's profile in web browser Open post in web browser Copy post link to clipboard Normalize text colors Stop following Follow Image is loading... %1 likes this One person %1 like this More than one person 1 like %1 likes 1 comment %1 comments %1 shared this %1 = One person name %1 shared this %1 = Names for more than one person Shared once Shared %1 times You like this Unlike Like this post Like Share post? Do you want to share %1's post? &Yes, share it &No Unshare post? Do you want to unshare %1's post? &Yes, unshare it WARNING: Delete post? Are you sure you want to delete this post? &Yes, delete it Click the image to see it in full size Link to: %1 Stop following? Are you sure you want to stop following %1? &Yes, stop following ProfileEditor Profile Editor This is your Pump address This is the e-mail address associated with your account, for things such as notifications and password recovery Change &Avatar... This is your visible name &Save Profile &Cancel Webfinger ID E-mail Avatar Full &Name &Hometown &Bio Not set In reference to the e-mail not being set for the account Select avatar image Image files All files Invalid image The selected image is not valid. Publisher Select Picture... Find the picture in your folders Public Followers People... Select who will see this post Upload photo To... Title Optional title for the post Title for the post. Setting a title helps make the Meanwhile feed more informative. Lists CC... Select who will get a copy of this post Ad&d Picture Post Hit Control+Enter to post with the keyboard Cancel Cancel the post Picture not set Error: Already composing You can't edit a post at this time, because a post is already being composed. Update Editing post Posting failed. Try again. Updating... Post is empty. Select one image Image files All files Resolution Size Invalid image The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Posting... PumpController Creating person list... Deleting person list... Getting main timeline... Getting direct messages timeline... Getting activity timeline... Getting favorites timeline... Getting likes... Getting comments... Getting minor feed... Error connecting to %1 Unhandled HTTP error code %1 Timeline received. Updating post list... Post published successfully. Post updated successfully. Message liked or unliked successfully. Message deleted successfully. Likes received. Getting list of 'Following'... Getting list of 'Followers'... Getting list of person lists... Getting a person list... Adding person to list... Removing person from list... Main timeline update requested, but updates are blocked. Direct timeline update requested, but updates are blocked. Activity timeline update requested, but updates are blocked. Favorites timeline update requested, but updates are blocked. Service Unavailable HTTP 503 error string Internal Server Error HTTP 500 error string Gone HTTP 410 error string Not Found HTTP 404 error string Forbidden HTTP 403 error string Unauthorized HTTP 401 error string Bad Request HTTP 400 error string Moved Temporarily HTTP 302 error string Moved Permanently HTTP 301 error string Profile received. Profile updated. Avatar published successfully. Comment posted successfully. Comments received. Post shared successfully. Minor feed received. Following successfully. Stopped following successfully. List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. List of 'lists' received. Person list created successfully. Person list deleted successfully. Person list received. Person added to list successfully. Person removed from list successfully. File uploaded successfully. Posting message... Avatar uploaded. Ready. TimeLine Welcome to Dianara Dianara is a <b>pump.io</b> client. If you don't have a Pump account yet, you can get one at the following address: First, configure your account from the <b>Settings - Account</b> menu. After the process is done, your profile and timelines should update automatically. Take a moment to look around the menus and the Configuration window. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Frequently asked questions about pump.io Direct Messages Timeline Here, you'll see posts specifically directed to you. Activity Timeline You'll see your own posts here. Favorites Timeline Posts and comments you've liked. F&irst Page &Previous Page &Next Page Timestamp Invalid timestamp! Less than a minute ago A minute ago %1 minutes ago An hour ago %1 hours ago A day ago %1 days ago A month ago %1 months ago A year ago %1 years ago dianara-v1.1/translations/dianara_eu.ts000644 000764 000764 00000260217 12264101304 017714 0ustar00janjan000000 000000 ASActivity Public AccountDialog Your Pump.io address: Webfinger ID, like username@pumpserver.org Your address, as username@server Get &Verifier Code Verifier code: &Save Details If the browser doesn't open automatically, copy this address manually Account Configuration First, enter your Webfinger ID, your pump.io address. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example After clicking this button, a web browser will open, requesting authorization for Dianara Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. Don't translate the VERIFIER word! Enter the verifier code provided by your Pump server here Paste the verifier here &Authorize Application &Cancel A web browser will start now, where you can get the verifier code Your Pump address is invalid Verifier code is empty Dianara is authorized to access your data AudienceSelector 'To' List 'CC' List &Add to Selected All Contacts Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below. Clear &List &Done &Cancel Selected People Comment Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Delete Delete this comment Unlike Like %1 like this comment Plural: %1=list of people like John, Jane, Smith %1 likes this comment Singular: %1=name of just 1 person WARNING: Delete comment? Are you sure you want to delete this comment? &Yes, delete it &No CommenterBlock Show All Comments &Comment You can press Control+Enter to send the comment with the keyboard C&ancel Press ESC to cancel the comment if there is no text Posting comment failed. Try again. Sending comment... Comment is empty. Composer Type a message here to post it Click here or press Control+N to post a note... Symbols Formatting Normal Bold Italic Underline Strikethrough Header Preformatted block Quote block Make a link Insert an image from a web site Insert line &Format Button for text formatting and related options Text Formatting Options Paste Text Without Formatting Type a comment here Insert a link Type or paste a web address here. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Insert an image from a URL Type or paste the image address here. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog minutes Top Bottom Left side Right side Program Configuration Timeline &update interval posts Goes after a number, as: 25 posts &Posts per page, main timeline posts This goes after a number, like: 10 posts Posts per page, &other timelines &Tabs position &Movable tabs Public posts as &default As system notifications Using own notifications Don't show notifications Show &notifications Dianara stores data in this folder: &Save Configuration &Cancel ContactCard Hometown Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name This user doesn't have a biography No biography for %1 %1=contact name Open Profile in Web Browser In Lists... User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList &Follow Reload Following Reload Followers Export Following Export Followers username@server.org or https://server.org/username &Enter address to follow: Optio&ns Followin&g Follo&wers Reload Lists &Lists Export list of 'following' to a file Export list of 'followers' to a file FDNotifications Dianara Notification FilterEditor Filter Editor Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. At the moment, these filters only apply to the Meanwhile feed. Content Author Application Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel &New Filter C&urrent Filters ImageViewer Image ESC to close, secondary-click for options Save Image... Close Viewer Save Image As... Image files All files Error saving image There was a problem while saving %1. Filename should end in .jpg or .png extensions. ListsManager Name Members Add Mem&ber &Remove Member &Delete Selected List Add New &List Create L&ist &Add to List Are you sure you want to delete %1? 1=Name of a person list Remove person from list? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list &Yes Type a name for the new list... Type an optional description here WARNING: Delete list? &Yes, delete it &No MainWindow Meanwhile... Side &Panel Status &Bar Dianara Notification &Timeline The main timeline &Messages Messages sent explicitly to you &Activity Your own posts Your favorited posts &Contacts The people you follow, and the ones who follow you Initializing... Your account is not configured yet. &Session &Update Main Timeline Update &Messages Timeline Update &Activity Timeline Update Favorites &Timeline Update Minor &Feed Update All Timelines Mark All as Read &Post a Note &Quit &View Full &Screen S&ettings Edit &Profile &Account &Filters &Frequently Asked Questions about Pump.io Timeline updated at %1. Your Pump.io account is not configured Dianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) &Configure Dianara &Help Visit &Website About &Dianara &Show Window There is 1 new post. There are %1 new posts. Timeline updated. Timeline updated. No new posts. Fav&orites About Dianara Dianara is a pump.io social networking client. With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. Thanks to all the testers, translators and packagers, who help make Dianara better! English translation by JanKusanagi. TRANSLATORS: Change this with your language and name ;) MinorFeed Get More Get older activities There are no activities to show yet. MinorFeedItem using %1 Application used to generate this activity Open referenced post MiscHelpers bytes PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Via %1 Shared on %1 using %1 Hometown using %1 1=Program used for posting Edited: %1 Posted on %1 1=Date Edited on %1 In To CC Comment Note Image Other As in: other type of post Post Noun, not verb Comment on this post. If you select some text, it will be quoted. Share Share this post Unshare Unshare this post Edit Edit this post Delete Delete this post Open %1's profile in web browser Open post in web browser Copy post link to clipboard Normalize text colors Stop following Follow Image is loading... %1 likes this One person %1 like this More than one person 1 like %1 likes 1 comment %1 comments %1 shared this %1 = One person name %1 shared this %1 = Names for more than one person Shared once Shared %1 times You like this Unlike Like this post Like Share post? Do you want to share %1's post? &Yes, share it &No Unshare post? Do you want to unshare %1's post? &Yes, unshare it WARNING: Delete post? Are you sure you want to delete this post? &Yes, delete it Click the image to see it in full size Link to: %1 Stop following? Are you sure you want to stop following %1? &Yes, stop following ProfileEditor Profile Editor This is your Pump address This is the e-mail address associated with your account, for things such as notifications and password recovery Change &Avatar... This is your visible name &Save Profile &Cancel Webfinger ID E-mail Avatar Full &Name &Hometown &Bio Not set In reference to the e-mail not being set for the account Select avatar image Image files All files Invalid image The selected image is not valid. Publisher Select Picture... Find the picture in your folders Public Followers People... Select who will see this post Upload photo To... Title Optional title for the post Title for the post. Setting a title helps make the Meanwhile feed more informative. Lists CC... Select who will get a copy of this post Ad&d Picture Post Hit Control+Enter to post with the keyboard Cancel Cancel the post Picture not set Error: Already composing You can't edit a post at this time, because a post is already being composed. Update Editing post Posting failed. Try again. Updating... Post is empty. Select one image Image files All files Resolution Size Invalid image The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Posting... PumpController Creating person list... Deleting person list... Getting main timeline... Getting direct messages timeline... Getting activity timeline... Getting favorites timeline... Getting likes... Getting comments... Getting minor feed... Error connecting to %1 Unhandled HTTP error code %1 Timeline received. Updating post list... Post published successfully. Post updated successfully. Message liked or unliked successfully. Message deleted successfully. Likes received. Getting list of 'Following'... Getting list of 'Followers'... Getting list of person lists... Getting a person list... Adding person to list... Removing person from list... Main timeline update requested, but updates are blocked. Direct timeline update requested, but updates are blocked. Activity timeline update requested, but updates are blocked. Favorites timeline update requested, but updates are blocked. Service Unavailable HTTP 503 error string Internal Server Error HTTP 500 error string Gone HTTP 410 error string Not Found HTTP 404 error string Forbidden HTTP 403 error string Unauthorized HTTP 401 error string Bad Request HTTP 400 error string Moved Temporarily HTTP 302 error string Moved Permanently HTTP 301 error string Profile received. Profile updated. Avatar published successfully. Comment posted successfully. Comments received. Post shared successfully. Minor feed received. Following successfully. Stopped following successfully. List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. List of 'lists' received. Person list created successfully. Person list deleted successfully. Person list received. Person added to list successfully. Person removed from list successfully. File uploaded successfully. Posting message... Avatar uploaded. Ready. TimeLine Welcome to Dianara Dianara is a <b>pump.io</b> client. If you don't have a Pump account yet, you can get one at the following address: First, configure your account from the <b>Settings - Account</b> menu. After the process is done, your profile and timelines should update automatically. Take a moment to look around the menus and the Configuration window. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Frequently asked questions about pump.io Direct Messages Timeline Here, you'll see posts specifically directed to you. Activity Timeline You'll see your own posts here. Favorites Timeline Posts and comments you've liked. F&irst Page &Previous Page &Next Page Timestamp Invalid timestamp! Less than a minute ago A minute ago %1 minutes ago An hour ago %1 hours ago A day ago %1 days ago A month ago %1 months ago A year ago %1 years ago dianara-v1.1/translations/dianara_pt.ts000644 000764 000764 00000260217 12264101305 017727 0ustar00janjan000000 000000 ASActivity Public AccountDialog Your Pump.io address: Webfinger ID, like username@pumpserver.org Your address, as username@server Get &Verifier Code Verifier code: &Save Details If the browser doesn't open automatically, copy this address manually Account Configuration First, enter your Webfinger ID, your pump.io address. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example After clicking this button, a web browser will open, requesting authorization for Dianara Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. Don't translate the VERIFIER word! Enter the verifier code provided by your Pump server here Paste the verifier here &Authorize Application &Cancel A web browser will start now, where you can get the verifier code Your Pump address is invalid Verifier code is empty Dianara is authorized to access your data AudienceSelector 'To' List 'CC' List &Add to Selected All Contacts Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below. Clear &List &Done &Cancel Selected People Comment Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Delete Delete this comment Unlike Like %1 like this comment Plural: %1=list of people like John, Jane, Smith %1 likes this comment Singular: %1=name of just 1 person WARNING: Delete comment? Are you sure you want to delete this comment? &Yes, delete it &No CommenterBlock Show All Comments &Comment You can press Control+Enter to send the comment with the keyboard C&ancel Press ESC to cancel the comment if there is no text Posting comment failed. Try again. Sending comment... Comment is empty. Composer Type a message here to post it Click here or press Control+N to post a note... Symbols Formatting Normal Bold Italic Underline Strikethrough Header Preformatted block Quote block Make a link Insert an image from a web site Insert line &Format Button for text formatting and related options Text Formatting Options Paste Text Without Formatting Type a comment here Insert a link Type or paste a web address here. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Insert an image from a URL Type or paste the image address here. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog minutes Top Bottom Left side Right side Program Configuration Timeline &update interval posts Goes after a number, as: 25 posts &Posts per page, main timeline posts This goes after a number, like: 10 posts Posts per page, &other timelines &Tabs position &Movable tabs Public posts as &default As system notifications Using own notifications Don't show notifications Show &notifications Dianara stores data in this folder: &Save Configuration &Cancel ContactCard Hometown Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name This user doesn't have a biography No biography for %1 %1=contact name Open Profile in Web Browser In Lists... User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList &Follow Reload Following Reload Followers Export Following Export Followers username@server.org or https://server.org/username &Enter address to follow: Optio&ns Followin&g Follo&wers Reload Lists &Lists Export list of 'following' to a file Export list of 'followers' to a file FDNotifications Dianara Notification FilterEditor Filter Editor Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. At the moment, these filters only apply to the Meanwhile feed. Content Author Application Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel &New Filter C&urrent Filters ImageViewer Image ESC to close, secondary-click for options Save Image... Close Viewer Save Image As... Image files All files Error saving image There was a problem while saving %1. Filename should end in .jpg or .png extensions. ListsManager Name Members Add Mem&ber &Remove Member &Delete Selected List Add New &List Create L&ist &Add to List Are you sure you want to delete %1? 1=Name of a person list Remove person from list? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list &Yes Type a name for the new list... Type an optional description here WARNING: Delete list? &Yes, delete it &No MainWindow Meanwhile... Side &Panel Status &Bar Dianara Notification &Timeline The main timeline &Messages Messages sent explicitly to you &Activity Your own posts Your favorited posts &Contacts The people you follow, and the ones who follow you Initializing... Your account is not configured yet. &Session &Update Main Timeline Update &Messages Timeline Update &Activity Timeline Update Favorites &Timeline Update Minor &Feed Update All Timelines Mark All as Read &Post a Note &Quit &View Full &Screen S&ettings Edit &Profile &Account &Filters &Frequently Asked Questions about Pump.io Timeline updated at %1. Your Pump.io account is not configured Dianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) &Configure Dianara &Help Visit &Website About &Dianara &Show Window There is 1 new post. There are %1 new posts. Timeline updated. Timeline updated. No new posts. Fav&orites About Dianara Dianara is a pump.io social networking client. With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. Thanks to all the testers, translators and packagers, who help make Dianara better! English translation by JanKusanagi. TRANSLATORS: Change this with your language and name ;) MinorFeed Get More Get older activities There are no activities to show yet. MinorFeedItem using %1 Application used to generate this activity Open referenced post MiscHelpers bytes PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Via %1 Shared on %1 using %1 Hometown using %1 1=Program used for posting Edited: %1 Posted on %1 1=Date Edited on %1 In To CC Comment Note Image Other As in: other type of post Post Noun, not verb Comment on this post. If you select some text, it will be quoted. Share Share this post Unshare Unshare this post Edit Edit this post Delete Delete this post Open %1's profile in web browser Open post in web browser Copy post link to clipboard Normalize text colors Stop following Follow Image is loading... %1 likes this One person %1 like this More than one person 1 like %1 likes 1 comment %1 comments %1 shared this %1 = One person name %1 shared this %1 = Names for more than one person Shared once Shared %1 times You like this Unlike Like this post Like Share post? Do you want to share %1's post? &Yes, share it &No Unshare post? Do you want to unshare %1's post? &Yes, unshare it WARNING: Delete post? Are you sure you want to delete this post? &Yes, delete it Click the image to see it in full size Link to: %1 Stop following? Are you sure you want to stop following %1? &Yes, stop following ProfileEditor Profile Editor This is your Pump address This is the e-mail address associated with your account, for things such as notifications and password recovery Change &Avatar... This is your visible name &Save Profile &Cancel Webfinger ID E-mail Avatar Full &Name &Hometown &Bio Not set In reference to the e-mail not being set for the account Select avatar image Image files All files Invalid image The selected image is not valid. Publisher Select Picture... Find the picture in your folders Public Followers People... Select who will see this post Upload photo To... Title Optional title for the post Title for the post. Setting a title helps make the Meanwhile feed more informative. Lists CC... Select who will get a copy of this post Ad&d Picture Post Hit Control+Enter to post with the keyboard Cancel Cancel the post Picture not set Error: Already composing You can't edit a post at this time, because a post is already being composed. Update Editing post Posting failed. Try again. Updating... Post is empty. Select one image Image files All files Resolution Size Invalid image The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Posting... PumpController Creating person list... Deleting person list... Getting main timeline... Getting direct messages timeline... Getting activity timeline... Getting favorites timeline... Getting likes... Getting comments... Getting minor feed... Error connecting to %1 Unhandled HTTP error code %1 Timeline received. Updating post list... Post published successfully. Post updated successfully. Message liked or unliked successfully. Message deleted successfully. Likes received. Getting list of 'Following'... Getting list of 'Followers'... Getting list of person lists... Getting a person list... Adding person to list... Removing person from list... Main timeline update requested, but updates are blocked. Direct timeline update requested, but updates are blocked. Activity timeline update requested, but updates are blocked. Favorites timeline update requested, but updates are blocked. Service Unavailable HTTP 503 error string Internal Server Error HTTP 500 error string Gone HTTP 410 error string Not Found HTTP 404 error string Forbidden HTTP 403 error string Unauthorized HTTP 401 error string Bad Request HTTP 400 error string Moved Temporarily HTTP 302 error string Moved Permanently HTTP 301 error string Profile received. Profile updated. Avatar published successfully. Comment posted successfully. Comments received. Post shared successfully. Minor feed received. Following successfully. Stopped following successfully. List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. List of 'lists' received. Person list created successfully. Person list deleted successfully. Person list received. Person added to list successfully. Person removed from list successfully. File uploaded successfully. Posting message... Avatar uploaded. Ready. TimeLine Welcome to Dianara Dianara is a <b>pump.io</b> client. If you don't have a Pump account yet, you can get one at the following address: First, configure your account from the <b>Settings - Account</b> menu. After the process is done, your profile and timelines should update automatically. Take a moment to look around the menus and the Configuration window. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Frequently asked questions about pump.io Direct Messages Timeline Here, you'll see posts specifically directed to you. Activity Timeline You'll see your own posts here. Favorites Timeline Posts and comments you've liked. F&irst Page &Previous Page &Next Page Timestamp Invalid timestamp! Less than a minute ago A minute ago %1 minutes ago An hour ago %1 hours ago A day ago %1 days ago A month ago %1 months ago A year ago %1 years ago dianara-v1.1/translations/dianara_pl.ts000644 000764 000764 00000260217 12264101305 017717 0ustar00janjan000000 000000 ASActivity Public AccountDialog Account Configuration First, enter your Webfinger ID, your pump.io address. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example Your Pump.io address: Webfinger ID, like username@pumpserver.org Your address, as username@server Get &Verifier Code After clicking this button, a web browser will open, requesting authorization for Dianara Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. Don't translate the VERIFIER word! Verifier code: Enter the verifier code provided by your Pump server here Paste the verifier here &Authorize Application &Save Details If the browser doesn't open automatically, copy this address manually &Cancel A web browser will start now, where you can get the verifier code Your Pump address is invalid Verifier code is empty Dianara is authorized to access your data AudienceSelector 'To' List 'CC' List &Add to Selected All Contacts Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below. Clear &List &Done &Cancel Selected People Comment Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Delete Delete this comment Unlike Like %1 like this comment Plural: %1=list of people like John, Jane, Smith %1 likes this comment Singular: %1=name of just 1 person WARNING: Delete comment? Are you sure you want to delete this comment? &Yes, delete it &No CommenterBlock Show All Comments &Comment You can press Control+Enter to send the comment with the keyboard C&ancel Press ESC to cancel the comment if there is no text Posting comment failed. Try again. Sending comment... Comment is empty. Composer Click here or press Control+N to post a note... Symbols Formatting Normal Bold Italic Underline Strikethrough Header Preformatted block Quote block Make a link Insert an image from a web site Insert line &Format Button for text formatting and related options Text Formatting Options Paste Text Without Formatting Type a message here to post it Type a comment here Insert a link Type or paste a web address here. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Insert an image from a URL Type or paste the image address here. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog Program Configuration minutes Timeline &update interval posts Goes after a number, as: 25 posts &Posts per page, main timeline posts This goes after a number, like: 10 posts Posts per page, &other timelines Top Bottom Left side Right side &Tabs position &Movable tabs Public posts as &default As system notifications Using own notifications Don't show notifications Show &notifications Dianara stores data in this folder: &Save Configuration &Cancel ContactCard Hometown Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name This user doesn't have a biography No biography for %1 %1=contact name Open Profile in Web Browser In Lists... User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList &Follow Reload Following Reload Followers Export Following Export Followers username@server.org or https://server.org/username &Enter address to follow: Optio&ns Followin&g Follo&wers Reload Lists &Lists Export list of 'following' to a file Export list of 'followers' to a file FDNotifications Dianara Notification FilterEditor Filter Editor Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. At the moment, these filters only apply to the Meanwhile feed. Content Author Application Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel &New Filter C&urrent Filters ImageViewer Image ESC to close, secondary-click for options Save Image... Close Viewer Save Image As... Image files All files Error saving image There was a problem while saving %1. Filename should end in .jpg or .png extensions. ListsManager Name Members Add Mem&ber &Remove Member &Delete Selected List Add New &List Create L&ist &Add to List Are you sure you want to delete %1? 1=Name of a person list Remove person from list? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list &Yes Type a name for the new list... Type an optional description here WARNING: Delete list? &Yes, delete it &No MainWindow Meanwhile... The main timeline Messages sent explicitly to you Your own posts Your favorited posts &Contacts The people you follow, and the ones who follow you Initializing... Your account is not configured yet. &Session &Update Main Timeline Update &Messages Timeline Update &Activity Timeline Update Favorites &Timeline Update Minor &Feed Update All Timelines Mark All as Read &Post a Note &Quit &View Side &Panel Status &Bar Full &Screen S&ettings Edit &Profile &Filters &Frequently Asked Questions about Pump.io Your Pump.io account is not configured Dianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) &Configure Dianara &Account &Help Visit &Website About &Dianara &Show Window Dianara Notification There is 1 new post. There are %1 new posts. Timeline updated. Timeline updated at %1. Timeline updated. No new posts. &Timeline &Messages &Activity Fav&orites About Dianara Dianara is a pump.io social networking client. With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. Thanks to all the testers, translators and packagers, who help make Dianara better! English translation by JanKusanagi. TRANSLATORS: Change this with your language and name ;) MinorFeed Get More Get older activities There are no activities to show yet. MinorFeedItem using %1 Application used to generate this activity Open referenced post MiscHelpers bytes PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Via %1 Shared on %1 using %1 Hometown using %1 1=Program used for posting Edited: %1 Posted on %1 1=Date Edited on %1 In To CC Comment Note Image Other As in: other type of post Post Noun, not verb Comment on this post. If you select some text, it will be quoted. Share Share this post Unshare Unshare this post Edit Edit this post Delete Delete this post Open %1's profile in web browser Open post in web browser Copy post link to clipboard Normalize text colors Stop following Follow Image is loading... %1 likes this One person %1 like this More than one person 1 like %1 likes 1 comment %1 comments %1 shared this %1 = One person name %1 shared this %1 = Names for more than one person Shared once Shared %1 times You like this Unlike Like this post Like Share post? Do you want to share %1's post? &Yes, share it &No Unshare post? Do you want to unshare %1's post? &Yes, unshare it WARNING: Delete post? Are you sure you want to delete this post? &Yes, delete it Click the image to see it in full size Link to: %1 Stop following? Are you sure you want to stop following %1? &Yes, stop following ProfileEditor Profile Editor This is your Pump address This is the e-mail address associated with your account, for things such as notifications and password recovery Change &Avatar... This is your visible name &Save Profile &Cancel Webfinger ID E-mail Avatar Full &Name &Hometown &Bio Not set In reference to the e-mail not being set for the account Select avatar image Image files All files Invalid image The selected image is not valid. Publisher Title Optional title for the post Title for the post. Setting a title helps make the Meanwhile feed more informative. Select Picture... Find the picture in your folders Public Followers Lists People... To... Select who will see this post CC... Select who will get a copy of this post Ad&d Picture Upload photo Post Hit Control+Enter to post with the keyboard Cancel Cancel the post Picture not set Error: Already composing You can't edit a post at this time, because a post is already being composed. Update Editing post Posting failed. Try again. Updating... Post is empty. Select one image Image files All files Resolution Size Invalid image The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Posting... PumpController Creating person list... Deleting person list... Getting main timeline... Getting direct messages timeline... Getting activity timeline... Getting favorites timeline... Getting likes... Getting comments... Getting minor feed... Error connecting to %1 Unhandled HTTP error code %1 Timeline received. Updating post list... Post published successfully. Post updated successfully. Message liked or unliked successfully. Message deleted successfully. Likes received. Getting list of 'Following'... Getting list of 'Followers'... Getting list of person lists... Getting a person list... Adding person to list... Removing person from list... Main timeline update requested, but updates are blocked. Direct timeline update requested, but updates are blocked. Activity timeline update requested, but updates are blocked. Favorites timeline update requested, but updates are blocked. Service Unavailable HTTP 503 error string Internal Server Error HTTP 500 error string Gone HTTP 410 error string Not Found HTTP 404 error string Forbidden HTTP 403 error string Unauthorized HTTP 401 error string Bad Request HTTP 400 error string Moved Temporarily HTTP 302 error string Moved Permanently HTTP 301 error string Profile received. Profile updated. Avatar published successfully. Comment posted successfully. Comments received. Post shared successfully. Minor feed received. Following successfully. Stopped following successfully. List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. List of 'lists' received. Person list created successfully. Person list deleted successfully. Person list received. Person added to list successfully. Person removed from list successfully. File uploaded successfully. Posting message... Avatar uploaded. Ready. TimeLine Welcome to Dianara Dianara is a <b>pump.io</b> client. If you don't have a Pump account yet, you can get one at the following address: First, configure your account from the <b>Settings - Account</b> menu. After the process is done, your profile and timelines should update automatically. Take a moment to look around the menus and the Configuration window. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Frequently asked questions about pump.io Direct Messages Timeline Here, you'll see posts specifically directed to you. Activity Timeline You'll see your own posts here. Favorites Timeline Posts and comments you've liked. F&irst Page &Previous Page &Next Page Timestamp Invalid timestamp! Less than a minute ago A minute ago %1 minutes ago An hour ago %1 hours ago A day ago %1 days ago A month ago %1 months ago A year ago %1 years ago dianara-v1.1/translations/dianara_pl.qm000664 000764 000764 00000000037 12166513354 017714 0ustar00janjan000000 000000 yT&iz>M$hHNc-$G; .GgI5ҥu]#b.Nf>2QݣxsDC]WDIn-M$hRSlBoc, G,]>N%VB O]Wy\%5K`^]v!S9zJZ^Zjte3 6F!/ƭ>ϠN%؞T.%Y1o\Wk8Y J 8P$1t#6&70iJ:JQk_i68sCfx2Rz7z.QP=B5N[ÌJ2רB?9_IfnnH?ʳ0M 1un[m4> >BP'V&`3fa$r):1:j7{\<N gzb=]+IY Rw,,m0TG4_3g23govR;@; z7xWL FB !U,"4KT4qKTvhOHRMRV|"fy|vs>${{%{W&V1_Vh{T:s: X6x\w600vƨI2xZon-^ ^!+f4-^4u|!4uQ9$Y<.z <.2<.p =lM?$ZPM\f;!p{r7Jy~a/Sbrt^=,1MusV4ґ2`ڑAڱ>>.0t1;#4} <D8<{< ?;U\N~lJgOopj {k<\jL} oJ/ROJ38MxaoҕiQrEgX" #NT)2K#12?Ne9e79Xm.<snZ6SUw.'B><I:rnťUt{.^琊 r=vR3 -Qn 1`#) 7w!@ hӾ r% |K ~$: 4`   ` FW d oq b> ʞO6 & ~ [ Ad] Ay/ ~ on  !/H # -E ?mC{ Hk3 TG4! TGv d 4i d: d<l e>s q\0 z3. ~ $  I I^ I) I;l Id Iy )p4 >p L yo o@ K ^ '_m t)J 3G6 ֓ T ؄ D 0n6 /^  c$ o "e} " 0w\ 5<0 @u Q(o bX8 b{Nb mmTiQ P/j ! e V" j5 ٩ VT uO أuF  C+L 2W (=kg - UzC ES Yw ]ӡT `uW eBUb {}~ F' ] VN}  #W% ͡w = < tm] * w" 3M aX ^ ЕYK #pn G_ V= bQ g, h13E mCU# |r b b| Q \[VGHj2sM+j+/{RXKlBElzlmhnce$HZ8T(8TG8Thm>:Q7'FU*y4?Bz'}A FU`O~'T*_} bu.gt\09(n9/M؞¹e& E6~<".ڨdޝ/;iPubblicoPublic ASActivity.&Autorizza Applicazione&Authorize Application AccountDialog&Cancella&Cancel AccountDialog&Salva i Dati &Save Details AccountDialogOra si avviera il browser web, dove potrai ottenere il codice di verificaAA web browser will start now, where you can get the verifier code AccountDialog,Configurazione AccountAccount Configuration AccountDialogDopo aver cliccato questo bottone, il browser web si dovrebbe aprire, richiedendo l'autorizzazione per DianaraYAfter clicking this button, a web browser will open, requesting authorization for Dianara AccountDialogZDianara autorizzato ad acceder ai tuoi dati)Dianara is authorized to access your data AccountDialogInserisci il codice di verifica ottenuto dal tuo server Pump.io qui9Enter the verifier code provided by your Pump server here AccountDialog|Prima inserisci il tuo Webfinger ID, il tuo indirizzo Pump.io.5First, enter your Webfinger ID, your pump.io address. AccountDialog<Ottieni il Codice di &VerificaGet &Verifier Code AccountDialogSe il browser non si apre automaticamente, copia questo indirizzo manualmenteEIf the browser doesn't open automatically, copy this address manually AccountDialogSe il tuo profilo, per esempio, https://pumpserver.org/nomeutente, di conseguenza il tuo indirizzo nomeutente@pumpserver.org_If your profile is at https://pump.example/yourname, then your address is yourname@pump.example AccountDialog4Una volta autorizzato Dianara dall'interfaccia web del tuo server Pump.io, riceverai un codice chiamato VERIFIER. Copialo e incollalo nel campo qui sotto.Once you have authorized Dianara from your Pump server web interface, you'll receive a code called VERIFIER. Copy it and paste it into the field below. AccountDialogBIncolla il codice di verifica quiPaste the verifier here AccountDialogNIl campo del codice di verifica vuotoVerifier code is empty AccountDialog&Codice di Verifica:Verifier code: AccountDialog^Webfinger ID, esempio nomeutente@pumpserver.org*Webfinger ID, like username@pumpserver.org AccountDialogJIl tuo indirizzo Pump.io non validoYour Pump address is invalid AccountDialog2Il tuo indirizzo Pump.io:Your Pump.io address: AccountDialogIl tuo indirizzo assomiglia a nomeutente@pumpserver.org, e lo puoi trovare sul tuo profilo, nell'interfaccia web.kYour address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. AccountDialogPIl tuo indirizzo, come nomeutente@server Your address, as username@server AccountDialog0&Aggiungi ai Selezionati&Add to SelectedAudienceSelector&Cancella&CancelAudienceSelector &Fatto&DoneAudienceSelectorLista 'CC' 'CC' ListAudienceSelectorLista 'A' 'To' ListAudienceSelector Tutti i Contatti All ContactsAudienceSelector"Pulisci la &Lista Clear &ListAudienceSelector2Seleziona le persone dalla lista a sinistra. Puoi trascinarle con il mouse, click o doppio click su di esse, o selezionarle e usare il bottone qui sotto.Select people from the list on the left. You can drag them with the mouse, click or double-click on them, or select them and use the button below.AudienceSelector&Persone SelezionateSelected PeopleAudienceSelector4A %1 piace questo commento%1 like this commentComment4A %1 piace questo commento%1 likes this commentComment&No&NoComment&Si, cancellalo&Yes, delete itComment^Sei sicuro di voler cancellare questo commento?-Are you sure you want to delete this comment?CommentEliminaDeleteComment.Elimina questo commentoDelete this commentCommentMi piaceLikeCommentbDecidi se ti piace o non ti piace questo commentoLike or unlike this commentCommentQuotareQuoteCommentBRispondi quotando questo commentoReply quoting this commentCommentNon mi piaceUnlikeCommentFATTENZIONE: Cancellare il commento?WARNING: Delete comment?Comment&Commenta&CommentCommenterBlock&AnnullaC&ancelCommenterBlock(Il commento vuoto.Comment is empty.CommenterBlockXInvio del commento fallito. Prova di nuovo.#Posting comment failed. Try again.CommenterBlockjPremi ESC per annullare il commento, se non c' testo3Press ESC to cancel the comment if there is no textCommenterBlock.Inviando il commento...Sending comment...CommenterBlock.Mostra Tutti i CommentiShow All CommentsCommenterBlockvPremi Control+Invio per inviare il commento con la tastieraAYou can press Control+Enter to send the comment with the keyboardCommenterBlock&Formato&FormatComposer&No&NoComposer&Si, cancellalo&Yes, cancel itComposer`Sei sicuro di voler cancellare questo messaggio?-Are you sure you want to cancel this message?ComposerGrassettoBoldComposer0Cancellare il messaggio?Cancel message?ComposernClicca qui o premi Control+N per pubblicare una nota.../Click here or press Control+N to post a note...Composer,Errore: URL non validoError: Invalid URLComposerFormattazione FormattingComposer TitoloHeaderComposer"Inserisci un link Insert a linkComposer>Inserisci un'immagine da un URLInsert an image from a URLComposerHInserisci un'immagine da un sito webInsert an image from a web siteComposer&Inserisci una linea Insert lineComposerCorsivoItalicComposerCrea un link Make a linkComposerJCrea un link con il testo selezionatoMake a link from selected textComposerNormaleNormalComposerBIncolla testo senza formattazionePaste Text Without FormattingComposer(Blocco preformattatoPreformatted blockComposer Blocco citazioni Quote blockComposerBarrato StrikethroughComposerSimboliSymbolsComposerDOpzioni di formattazione del testoText Formatting OptionsComposerL'indirizzo inserito (%1) non valido. Gli indirizzi di un'immagine dovrebbero iniziare con http:// o https://`The address you entered (%1) is not valid. Image addresses should begin with http:// or https://Composer,Scrivi un commento quiType a comment hereComposerNScrivi qui un messaggio per pubblicarloType a message here to post itComposerLDigita o incolla un indirizzo web qui."Type or paste a web address here. ComposerDigita o incolla un indirizzo web qui. Il testo selezionato (%1) sar convertito in un link.UType or paste a web address here. The selected text (%1) will be converted to a link.Composer`Digita o incolla l'indirizzo di un'immagine qui.&Type or paste the image address here. ComposerSottolineato UnderlineComposer&Cancella&Cancel ConfigDialog&Tab mobili &Movable tabs ConfigDialogR&Elementi per pagina, timeline principale&Posts per page, main timeline ConfigDialog*&Salva Configurazione&Save Configuration ConfigDialog(Posizione delle &tab&Tabs position ConfigDialog2Come notifiche di sistemaAs system notifications ConfigDialogIn bassoBottom ConfigDialogPDianara salva i dati in questa cartella:#Dianara stores data in this folder: ConfigDialog,Non mostrare notificheDon't show notifications ConfigDialogA sinistra Left side ConfigDialogHElementi per pagina, &altre timeline Posts per page, &other timelines ConfigDialog8Configurazione del programmaProgram Configuration ConfigDialog>Messaggi pubblici come &defaultPublic posts as &default ConfigDialogA destra Right side ConfigDialog"Mostra &notificheShow ¬ifications ConfigDialogVIntervallo di &aggiornamento della timelineTimeline &update interval ConfigDialogIn altoTop ConfigDialog0Usando proprie notificheUsing own notifications ConfigDialog minutiminutes ConfigDialogelementi!Goes after a number, as: 25 postsposts ConfigDialogelementi(This goes after a number, like: 10 postsposts ConfigDialog&No&No ContactCard,&Si, smetti di seguire&Yes, stop following ContactCardVSei sicuro di voler smettere di seguire %1?+Are you sure you want to stop following %1? ContactCardBio di %1 Bio for %1 ContactCard SeguiFollow ContactCard CittHometown ContactCardNelle liste... In Lists... ContactCard0Nessuna biografia per %1No biography for %1 ContactCard>Apri il profilo nel browser webOpen Profile in Web Browser ContactCard"Smetti di seguireStop Following ContactCard(Smettere di seguire?Stop following? ContactCardDQuesto utente non ha una biografia"This user doesn't have a biography ContactCardOpzioni utente User Options ContactCardD&Inserisci l'indirizzo da seguire:&Enter address to follow: ContactList &Segui&Follow ContactList &Liste&Lists ContactList"Esporta FollowersExport Followers ContactList"Esporta FollowingExport Following ContactListVEsporta la lista dei 'Followers' in un file$Export list of 'followers' to a file ContactListVEsporta la lista dei 'Following' in un file$Export list of 'following' to a file ContactListFollo&wers Follo&wers ContactListFoll&owing Followin&g ContactListOp&zioniOptio&ns ContactList$Ricarica FollowersReload Followers ContactList$Ricarica FollowingReload Following ContactListRicarica Liste Reload Lists ContactListznomeutente@pumpserver.org o https://pumpserver.org/nomeutente2username@server.org or https://server.org/username ContactList(Notifiche di DianaraDianara NotificationFDNotifications &Aggiungi Filtro &Add Filter FilterEditor&Annulla&Cancel FilterEditor&Nuovo Filtro &New Filter FilterEditor:&Rimuovi i Filtri Selezionati&Remove Selected Filter FilterEditor&Salva Filtri &Save Filters FilterEditorApplicazione Application FilterEditorAl momento, questi filtri si possono applicare solo alla timeline "Nel Frattempo...".>At the moment, these filters only apply to the Meanwhile feed. FilterEditor AutoreAuthor FilterEditorFilt&ri SalvatiC&urrent Filters FilterEditorContenutoContent FilterEditorModifica Filtri Filter Editor FilterEditorFiltri in usoFilters in use FilterEditorQui puoi impostare alcuni filtri. Puoi filtrare gli elementi per contenuto, autore o applicazione. Per esempio, puoi nascondere i messaggi postati dall'applicazione Open Farm Game, o quelli che contengono NSFW nel messaggio.Here you can set some filters. You can filter by content, author or application. For instance, you can filter out messages posted by the application Open Farm Game, or which contain the word NSFW in the message. FilterEditor Parole chiave... Keywords... FilterEditorTutti i Files All files ImageViewer*Chiudi Visualizzatore Close Viewer ImageViewerTESC per chiudere, click destro per opzioni)ESC to close, secondary-click for options ImageViewerVErrore durante il salvataggio dell'immagineError saving image ImageViewerImmagineImage ImageViewerFiles Immagine Image files ImageViewer,Salva Immagine Come...Save Image As... ImageViewer.Salvataggio Immagine... Save Image... ImageViewerC' stato un problema salvando %1. L'estensione del file dovrebbe essere .jpg o .png.UThere was a problem while saving %1. Filename should end in .jpg or .png extensions. ImageViewer(&Aggiungi alla Lista &Add to List ListsManager<&Cancella la Lista Selezionata&Delete Selected List ListsManager&No&No ListsManager&Rimuovi Membro&Remove Member ListsManager&Si&Yes ListsManager&Si, cancellala&Yes, delete it ListsManager Aggiungi Mem&bro Add Mem&ber ListsManager*Aggiungi Nuova &Lista Add New &List ListsManagerDSei sicuro di voler cancellare %1?#Are you sure you want to delete %1? ListsManager`Sei sicuro di voler rimuovere %1 dalla lista %2?4Are you sure you want to remove %1 from the %2 list? ListsManagerCrea L&ista Create L&ist ListsManager MembriMembers ListsManagerNomeName ListsManagerBRimuovere la persona dalla lista?Remove person from list? ListsManagerHDigita un nome per la nuova lista...Type a name for the new list... ListsManagerHDigita una descrizione opzionale qui!Type an optional description here ListsManager@ATTENZIONE: Cancellare la lista?WARNING: Delete list? ListsManagerA&ccount&Account MainWindow&Attivit &Activity MainWindow$Configura &Dianara&Configure Dianara MainWindow&Contatti &Contacts MainWindow&Filtri&Filters MainWindow:Domande &frequenti su pump.io)&Frequently Asked Questions about Pump.io MainWindow Ai&uto&Help MainWindow&Messaggi &Messages MainWindow$&Pubblica una nota &Post a Note MainWindow &E&sci&Quit MainWindow&Sessione&Session MainWindow&&Mostra la finestra &Show Window MainWindowT&imeline &Timeline MainWindow@Agg&iorna la timeline principale&Update Main Timeline MainWindow &Visualizzazione&View MainWindow2Informazioni su &Dianara About &Dianara MainWindow.Informazioni su Dianara About Dianara MainWindow(Notifiche di DianaraDianara Notification MainWindowdDianara un client per il social network pump.io..Dianara is a pump.io social networking client. MainWindowDianara distribuita su licenza GNU GPL, ed utilizza alcune icone Oxygen: http://www.oxygen-icons.org/ (licenza LGPL)wDianara is licensed under the GNU GPL license, and uses some Oxygen icons: http://www.oxygen-icons.org/ (LGPL licensed) MainWindow"Modifica &profilo Edit &Profile MainWindowTraduzione italiana a cura di Metal Biker (howcanuhavemyusername@microca.st).#English translation by JanKusanagi. MainWindowPref&eriti Fav&orites MainWindowSc&hermo Intero Full &Screen MainWindow"Inizializzando...Initializing... MainWindow,Segna tutti come lettiMark All as Read MainWindow Nel frattempo... Meanwhile... MainWindowHMessaggi inviati esplicitamente a teMessages sent explicitly to you MainWindowConfigura&zione S&ettings MainWindow$Pannello &laterale Side &Panel MainWindow&Barra di stato Status &Bar MainWindowGrazie a tutti i tester, i traduttori e i manutentori dei pacchetti, che hanno aiutato Dianara a migliorare!SThanks to all the testers, translators and packagers, who help make Dianara better! MainWindow&Timeline principaleThe main timeline MainWindowDPersone che segui e che ti seguono2The people you follow, and the ones who follow you MainWindow4Ci sono %1 nuovi messaggi.There are %1 new posts. MainWindow,C' 1 nuovo messaggio.There is 1 new post. MainWindow8Timeline aggiornata alle %1.Timeline updated at %1. MainWindow(Timeline aggiornata.Timeline updated. MainWindowXTimeline aggiornata. Nessun nuovo messaggio.Timeline updated. No new posts. MainWindowHAggiorna la timeline delle &attivitUpdate &Activity Timeline MainWindowDAggiorna la timeline dei &messaggiUpdate &Messages Timeline MainWindow4Aggiorna tutte le timelineUpdate All Timelines MainWindowFAggiorna la timeline dei pre&feritiUpdate Favorites &Timeline MainWindow<Aggiorna la timeline late&raleUpdate Minor &Feed MainWindow&Visita il sito &webVisit &Website MainWindow2Con Dianara puoi vedere le tue timelines, creare nuove note, caricare immagini, interagire con messaggi, gestire i tuoi contatti e seguire nuove persone.With Dianara you can see your timelines, create new posts, upload pictures, interact with posts, manage your contacts and follow new people. MainWindowPIl tuo account Pump.io non configurato&Your Pump.io account is not configured MainWindow\Il tuo account non ancora stato configurato.#Your account is not configured yet. MainWindow2I tuoi messaggi preferitiYour favorited posts MainWindowI tuoi messaggiYour own posts MainWindowPi ElementiGet More MinorFeed8Guarda attivit meno recentiGet older activities MinorFeedPNon ci sono ancora attivit da mostrare.$There are no activities to show yet. MinorFeed0Apri l'elemento relativoOpen referenced post MinorFeedItem via %1using %1 MinorFeedItem bytesbytes MiscHelpers&Cancella&Cancel PeopleWidget&Cerca:&Search: PeopleWidgetBAggiungi un contatto ad una listaAdd a contact to a list PeopleWidgetDInserisci qui un nome per cercarlo"Enter a name here to search for it PeopleWidget%1 commenti %1 commentsPost4A %1 piace questo elemento %1 like thisPost%1 mi piace%1 likesPost4A %1 piace questo elemento %1 likes thisPost>%1 ha condiviso questo elemento%1 shared thisPostD%1 hanno condiviso questo elemento#%1 = Names for more than one person%1 shared thisPost&No&NoPost&Si, eliminalo&Yes, delete itPost &Si, condividilo&Yes, share itPost,&Si, smetti di seguire&Yes, stop followingPost*&Si, non condividerlo&Yes, unshare itPost1 commento 1 commentPost1 mi piace1 likePost^Sei sicuro di voler eliminare questo messaggio?*Are you sure you want to delete this post?PostVSei sicuro di voler smettere di seguire %1?+Are you sure you want to stop following %1?PostCCCCPostpClicca l'immagine per vederla nelle dimensioni originali&Click the image to see it in full sizePostCommentareCommentPost4Commenta questo messaggio.Comment on this post.PostPCopia il link al messaggio negli appuntiCopy post link to clipboardPostEliminaDeletePost0Elimina questo messaggioDelete this postPostHVuoi condividere il messaggio di %1?Do you want to share %1's post?PostfVuoi rimuovere la condivisione dell'elemento di %1?!Do you want to unshare %1's post?PostModificaEditPost2Modifica questo messaggioEdit this postPost Modificato su %1 Edited on %1PostModificato: %1 Edited: %1Post SeguiFollowPost CittHometownPostLSe selezioni del testo, verr quotato.+If you select some text, it will be quoted.PostImmagineImagePost4Immagine in caricamento...Image is loading...PostInInPostMi piaceLikePost@D che ti piace questo messaggioLike this postPostLink a: %1 Link to: %1Post:Normalizza i colori del testoNormalize text colorsPostNotaNotePostJApri il profilo di %1 nel browser web Open %1's profile in web browserPostBApri il messaggio nel browser webOpen post in web browserPost AltroOtherPostPubblicaNoun, not verbPostPost Pubblicato il %1 Posted on %1PostCondividiSharePost.Condividi il messaggio? Share post?Post4Condividi questo messaggioShare this postPost$Condiviso %1 volteShared %1 timesPostCondiviso su %1 Shared on %1Post&Condiviso una volta Shared oncePost"Smetti di seguireStop followingPost(Smettere di seguire?Stop following?PostAToPost Non mi piace piUnlikePost(Rimuovi condivisioneUnsharePostZRimuovere la condivisione di questo elemento? Unshare post?PostTRimuovi la condivisione di questo elementoUnshare this postPost Via %1Via %1PostFATTENZIONE: Eliminare il messaggio?WARNING: Delete post?PostTi piace You like thisPost via %1using %1Post via %11=Program used for postingusing %1Post&Bio&Bio ProfileEditor&Cancella&Cancel ProfileEditor Ci&tt &Hometown ProfileEditor&Salva profilo &Save Profile ProfileEditorTutti i files All files ProfileEditor AvatarAvatar ProfileEditor"Cambia &avatar...Change &Avatar... ProfileEditor E-mailE-mail ProfileEditor&Nome Completo Full &Name ProfileEditorFiles immagine Image files ProfileEditor&Immagine non valida Invalid image ProfileEditorNon impostataNot set ProfileEditor$Editor del profiloProfile Editor ProfileEditor<Scegli l'immagine per l'avatarSelect avatar image ProfileEditorHL'immagine selezionata non valida. The selected image is not valid. ProfileEditorQuesta l'indirizzo e-mail associato al tuo accout, per inviare notifiche e recuperare la passwordoThis is the e-mail address associated with your account, for things such as notifications and password recovery ProfileEditorBQuesto il tuo indirizzo Pump.ioThis is your Pump address ProfileEditor:Questo il tuo nome visibileThis is your visible name ProfileEditorWebfinger ID Webfinger ID ProfileEditor*A&ggiungi un'immagine Ad&d Picture PublisherTutti i files All files Publisher CC...CC... PublisherCancellaCancel Publisher*Cancella il messaggioCancel the post Publisher*Modifica il messaggio Editing post Publisher4Errore: stai gi scrivendoError: Already composing PublisherFTrova l'immagine nelle tue cartelle Find the picture in your folders PublisherFollowers Followers PublisherdPremi Control+Invio per pubblicare con la tastiera+Hit Control+Enter to post with the keyboard PublisherFiles immagine Image files Publisher&Immagine non valida Invalid image Publisher ListeLists PublisherBTitolo opzionale per il messaggioOptional title for the post PublisherPersone... People... Publisher0Immagine non selezionataPicture not set PublisherPubblicaPost Publisher*Il messaggio vuoto.Post is empty. Publisher>Invio fallito. Prova di nuovo.Posting failed. Try again. PublisherPubblicando... Posting... PublisherPubblicoPublic PublisherRisoluzione Resolution Publisher2Selezionare l'immagine...Select Picture... Publisher$Scegli un'immagineSelect one image PublisherbScegli chi ricever una copia di questo messaggio'Select who will get a copy of this post PublisherHScegli chi potr vedere il messaggioSelect who will see this post PublisherDimensioniSize PublisherIl formato dell'immagine non stato riconosciuto. L'estensione pu essere sbagliata, come un'immagine GIF rinominata in immagine.jpg o altro.tThe image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Publisher TitoloTitle PublisherTitolo dell'elemento. Scrivere un titolo aiuta a rendere pi immediato il feed della timeline laterale "nel frattempo...".STitle for the post. Setting a title helps make the Meanwhile feed more informative. PublisherA...To... PublisherAggiornaUpdate PublisherAggiornando... Updating... Publisher$Carica un'immagine Upload photo PublisherNon puoi modificare il messaggio ora, perch stai gi scrivendo un messaggio.MYou can't edit a post at this time, because a post is already being composed. PublisherRichiesto un aggiornamento della timeline delle attivit, ma gli aggiornamenti sono bloccati.Avatar pubblicato con successo.Avatar published successfully.PumpController Avatar caricato.Avatar uploaded.PumpController:Richiesta Errata (Errore 400) Bad RequestPumpControllerBCommento pubblicato con successo.Comment posted successfully.PumpController$Commento ricevuto.Comments received.PumpControllerBCreando la lista delle persone...Creating person list...PumpControllerHEliminando la lista delle persone...Deleting person list...PumpControllerRichiesto un aggiornamento dei messaggi diretti, ma gli aggiornamenti sono bloccati.:Direct timeline update requested, but updates are blocked.PumpControllerDErrore durante la connessione a %1Error connecting to %1PumpControllerRichiesto un aggiornamento dei preferiti, ma gli aggiornamenti sono bloccati.=Favorites timeline update requested, but updates are blocked.PumpControllernFile caricato con successo. Pubblicando il messaggio....File uploaded successfully. Posting message...PumpController&Following riuscito.Following successfully.PumpController*Proibito (Errore 403) ForbiddenPumpControllerFOttenendo la lista della persona...Getting a person list...PumpControllerNRicevendo la timeline delle attivit...Getting activity timeline...PumpController.Ricevendo i commenti...Getting comments...PumpControllerZRicevendo la timeline dei messaggi diretti...#Getting direct messages timeline...PumpControllerLRicevendo la timeline dei preferiti...Getting favorites timeline...PumpController2Ricevendo i "mi piace"...Getting likes...PumpControllerJRicevendo la lista dei 'Followers'...Getting list of 'Followers'...PumpControllerJRicevendo la lista dei 'Following'...Getting list of 'Following'...PumpControllerFRocevendo le liste delle persone...Getting list of person lists...PumpControllerFRicevendo la timeline principale...Getting main timeline...PumpControllerBRicevendo la timeline laterale...Getting minor feed...PumpController(Sparito (Errore 410)GonePumpControllerLErrore Interno del Server (Errore 500)Internal Server ErrorPumpController("Mi piace" ricevuto.Likes received.PumpControllerZLista dei 'followers' ricevuta completamente.(List of 'followers' completely received.PumpControllerZLista dei 'following' ricevuta completamente.(List of 'following' completely received.PumpController:Lista delle 'Liste' ricevuta.List of 'lists' received.PumpControllerRichiesto un aggiornamento della timeline principale, ma gli aggiornamenti sono bloccati.8Main timeline update requested, but updates are blocked.PumpControllerFMessaggio cancellato correttamente.Message deleted successfully.PumpControllertMessaggio aggiunto o rimosso dai "Mi piace" correttamente.&Message liked or unliked successfully.PumpController6Timeline laterale ricevuta.Minor feed received.PumpControllerJSpostato Permanentemente (Codice 301)Moved PermanentlyPumpControllerJSpostato Temporaneamente (Codice 302)Moved TemporarilyPumpController0Non Trovato (Errore 404) Not FoundPumpControllerXLista dei 'followers' ricevuta parzialmente.%Partial list of 'followers' received.PumpControllerXLista dei 'following' ricevuta parzialmente.%Partial list of 'following' received.PumpControllerRPersona aggiunta alla lista con successo."Person added to list successfully.PumpControllerLLista di persone creata correttamente.!Person list created successfully.PumpControllerRLista di persone eliminata correttamente.!Person list deleted successfully.PumpController4Lista di persone ricevuta.Person list received.PumpControllerRPersona rimossa dalla lista con successo.&Person removed from list successfully.PumpControllerDMessaggio pubblicato con successo.Post published successfully.PumpControllerBMessaggio condiviso con successo.Post shared successfully.PumpControllerDMessaggio aggiornato con successo.Post updated successfully.PumpController"Profilo ricevuto.Profile received.PumpController&Profilo aggiornato.Profile updated.PumpControllerPronto.Ready.PumpControllerJRimuovendo una persona dalla lista...Removing person from list...PumpControllerJServizio non Disponibile (Errore 503)Service UnavailablePumpController:Rimozione following riuscita.Stopped following successfully.PumpControllernTimeline ricevuta. Aggiornando la lista dei messaggi...(Timeline received. Updating post list...PumpController8Non Autorizzato (Errore 401) UnauthorizedPumpControllerJCodice di errore HTTP non gestito: %1Unhandled HTTP error code %1PumpController Pro&ssima Pagina &Next PageTimeLine$P&agina Precedente&Previous PageTimeLine.Timeline delle AttivitActivity TimelineTimeLineCompletato il processo, il tuo profilo e le timelines dovrebbero aggiornarsi automaticamente.RAfter the process is done, your profile and timelines should update automatically.TimeLineFDianara un client <b>pump.io</b>.#Dianara is a pump.io client.TimeLineBlog di DianaraDianara's blogTimeLine:Timeline dei messaggi direttiDirect Messages TimelineTimeLine&Prima pagina F&irst PageTimeLine,Timeline dei PreferitiFavorites TimelineTimeLinePrima di tutto, configura il tuo account nel men <b>Configurazione - Account</b>.FFirst, configure your account from the Settings - Account menu.TimeLineDDomande frequenti (FAQ) su pump.io(Frequently asked questions about pump.ioTimeLinezQui potrai vedere messaggi indirizzati specificatamente a te.4Here, you'll see posts specifically directed to you.TimeLineSe non hai ancora un account Pump.io, ne puoi ottenere uno al seguente indirizzo:OIf you don't have a Pump account yet, you can get one at the following address:TimeLineRMessaggi e commenti che ti sono piaciuti. Posts and comments you've liked.TimeLinePrenditi un momento per dare uno sguardo ai men ed alla finestra di Configurazione.DTake a moment to look around the menus and the Configuration window.TimeLineCi sono tooltips (popup descrittivi) ovunque, quindi se ti porti sopra un bottone con il mouse otterrai informazioni aggiuntive.There are tooltips everywhere, so if you hover a button or a text field with your mouse, you'll probably see some extra information.TimeLine(Benvenuto in DianaraWelcome to DianaraTimeLinePuoi anche impostare i dettagli e la foto del tuo profilo nel menu <b>Configurazione - Modifica profilo</b>.\You can also set your profile data and picture from the Settings - Edit Profile menu.TimeLine6Qui vedrai i tuoi messaggi.You'll see your own posts here.TimeLine%1 giorni fa %1 days ago Timestamp%1 ore fa %1 hours ago Timestamp%1 minuti fa%1 minutes ago Timestamp%1 mesi fa %1 months ago Timestamp%1 anni fa %1 years ago TimestampUn giorno fa A day ago TimestampUn minuto fa A minute ago TimestampUn mese fa A month ago TimestampUn anno fa A year ago TimestampUn'ora fa An hour ago Timestamp*Timestamp non Valida!Invalid timestamp! Timestamp(Meno di un minuto faLess than a minute ago Timestamp