dianara-v1.4.1/0000775000175000017500000000000013221736412011407 5ustar janjandianara-v1.4.1/packaging/0000755000175000017500000000000012607735617013346 5ustar janjandianara-v1.4.1/packaging/mga-mdv-rpm/0000755000175000017500000000000012202277507015460 5ustar janjandianara-v1.4.1/packaging/mga-mdv-rpm/dianara.spec0000644000175000017500000000243312202277507017735 0ustar janjanSummary: 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.4.1/packaging/salix-slkbuild/0000755000175000017500000000000012607763123016266 5ustar janjandianara-v1.4.1/packaging/salix-slkbuild/SLKBUILD0000644000175000017500000000250512607735667017440 0ustar janjan# Maintainer: Your Name pkgname=dianara pkgver=1.3.1 pkgrel=1salix url="http://dianara.nongnu.org/" source=(http://download-mirror.savannah.gnu.org/releases/$pkgname/$pkgname-v$pkgver.tar.gz) docs=("BUGS" "CHANGELOG" "INSTALL" "LICENSE" "README" "TODO" "TRANSLATING") slackdesc=\ ( #|-----handy-ruler------------------------------------------------------| "dianara (A Pump.io client)" "Dianara is a Pump.io application for the desktop." "With it, you can access your Pump.io account without using a web" "browser." ) build() { cd $startdir/src/$pkgname-v$pkgver qmake PREFIX=/usr make || return 1 # bin install -Dm755 $pkgname \ "$startdir/pkg/usr/bin/$pkgname" # desktop file install -Dm644 $pkgname.desktop \ "$startdir/pkg/usr/share/applications/$pkgname.desktop" # icons install -Dm644 icon/32x32/$pkgname.png \ "$startdir/pkg/usr/share/icons/hicolor/32x32/apps/$pkgname.png" install -Dm644 icon/64x64/$pkgname.png \ "$startdir/pkg/usr/share/icons/hicolor/64x64/apps/$pkgname.png" # translations install -d "$startdir/pkg/usr/share/$pkgname/translations" install -Dm644 translations/*.qm \ "$startdir/pkg/usr/share/$pkgname/translations" # man install -Dm644 manual/$pkgname.1 \ "$startdir/pkg/usr/share/man/man1/$pkgname.1" } } dianara-v1.4.1/packaging/fedora-rpm/0000755000175000017500000000000012573333701015370 5ustar janjandianara-v1.4.1/packaging/fedora-rpm/dianara.spec0000644000175000017500000000311412573333701017642 0ustar janjanName: dianara Version: 1.3.1 Release: 1%{?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 BuildRequires: file-devel 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 * Tue Aug 18 2015 Roman Yepishev 1.3.1-1 - Upgrade to 1.3.1 * 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.4.1/appdata/0000755000175000017500000000000013032057073013016 5ustar janjandianara-v1.4.1/appdata/dianara.appdata.xml0000644000175000017500000000256413032057072016556 0ustar janjan dianara.desktop CC0-1.0 GPL-2.0+ Dianara A social networking client for the Pump.io network

Dianara is a Pump.io client, a desktop application to manage a user's account on the Pump.io distributed social network.

It allows all the usual things, like creating new posts, commenting on other people's posts, sharing them, following people, editing your profile, etc. It can also do many things that the web interface can't, such as:

  • Editing posts and comments
  • Adding titles to regular notes
  • Uploading audio and video
  • Following people, from any server, with a click on their avatar
  • Changing the e-mail address associated with your account
http://dianara.nongnu.org/images/dianara-v0.9-release-main.png The main window, while replying to a post https://jancoding.wordpress.com/dianara jancoding@gmx.com
dianara-v1.4.1/manual/0000755000175000017500000000000013066005303012655 5ustar janjandianara-v1.4.1/manual/dianara.10000644000175000017500000001104613066005303014340 0ustar janjan.TH "Dianara" "1" "September 19, 2016" "dianara" "Dianara Pump.io Client" .SH NAME Dianara \- A Pump.io client for the desktop .SH SYNOPSIS .B dianara [\-\-debug] [\-\-config [name]] .SH DESCRIPTION Dianara is a Pump.io application for the desktop. With it, you can manage your Pump.io account without using a web browser. You can also do things that the web interface can't do at this point, like editing posts and comments, or uploading audio and video. What it does: - Posting text (with some HTML formatting), to Followers, Public, custom person lists created by you, or specific people, in any combination. - Uploading pictures, audio, video, and other files. - Showing different timelines, with configurable number of posts per page, moving forward and backward in pages, or jumping to any page directly. - Liking, commenting, sharing, editing and deleting posts. - Liking, editing and deleting comments. - Contact listing, following and unfollowing, either by entering their webfinger address, through buttons in the lists, or via avatar menus. - Editing your profile, changing your avatar, setting your e-mail address. - Watching the "Meanwhile" feed and interacting with posts from it. Also the "Mentions" and "Actions" feeds, showing only activities related to you, or done by you. - Filter out unwanted posts, or highlight them according to some rules. - Popup notifications when there are new posts, or new "Meanwhile" activities. - And more! .SH USAGE TIPS - Press F1 in the program to read the general help. - You can set "public posting" as the default in the Configuration window. - There are tooltips everywhere, so if you hover over a button or a text field with your mouse, you'll probably see some extra information. This is specially useful in the "Meanwhile" feed. - When publishing a note or posting a comment, you can send using the keyboard, by pressing Control+Enter. - You can use Control+Up/Down/PgUp/PgDown/Home/End to scroll the timeline. Use Control+Left/Right to go forward and backwards in the timeline pages. Press Control+G to jump to any page. - You can click on the avatar of a post's author to get some options. This also works in the comments, and in the Meanwhile feed. - If a post is of "image" type, you can click on the image to see it full size. - You can hide/show the side panel by pressing F9. Most items in the "Meanwhile" column have a "+" button to open the referenced post. - Adding titles to your posts will make the "Meanwhile" feed and the e-mail notifications a lot more useful! - You can temporarily stop (and restart) the auto-updating of timelines by clicking on the "state" icon in the status bar, or by using the Session > Auto-update Timelines option. - If you connect to the Internet through a proxy, you can set it up in the program settings. - Dianara offers a D-Bus interface that allows some control from other applications. The interface is at org.nongnu.dianara, and you can access it with tools such as qdbus or dbus-send. It offers methods like 'toggle' and 'post'. .SH COMMAND LINE PARAMETERS - You can use the \-\-config (or \-c) command line parameter to specify that a different configuration file be used. For example, if you run "dianara \-\-config myotheraccount", the config file in use will be "Dianara_myotheraccount.conf" instead of the usual "Dianara.conf". This way, you can switch between different accounts easily. You can even run two instances of Dianara at the same time. You can use any name you wish to identify the configuration, but it must be one word, no spaces. - Use the \-\-debug (or \-d) command line parameter to have detailed debugging information on what the program is doing. - If your server does not support HTTPS, you can use the \-\-nohttps parameter. - If you need to connect to a server with a invalid SSL configuration, such as a self-signed certificate or an expired certificate, you can use the \-\-ignoresslerrors parameter. This is not recommended. .SH FILES .TP ~/.config/JanCoding/Dianara.conf Stores program configuration, for default profile. Other profiles used via \-\-config option will use Dianara_yourconfigname.conf .TP ~/.local/share/JanCoding/ Stores cached program data, such as user avatars and post meta-information. .SH AUTHOR Dianara was written by JanKusanagi .SH BUG REPORTS You can look for information on known bugs and report any new ones you find at the issue tracker: https://gitlab.com/dianara/dianara-dev/issues You should also check the BUGS file included with Dianara. dianara-v1.4.1/BUGS0000644000175000017500000001413513163750603012077 0ustar janjan This is a list of problems you might experience with Dianara. Some of them are bugs in the Pump.io core, but are collected here for reference. As a general rule, if Dianara reports some sort of problem, check the status bar (at the bottom) and/or the Log window for details. If you find a bug not listed here, you can report it at the issue tracker: https://gitlab.com/dianara/dianara-dev/issues 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 an issue with distribution of comments by the Pump servers, not something Dianara-specific. It is being worked on by the Pump.io developers. This happens specially in non-public posts, the ones that are to "Followers" only. Until this issue is resolved, it is recommended to post to "Public". - The "+" button to open related posts from the Meanwhile feed will open the referenced posts, but you won't always be able to see the comments. Basically it will fail if the activity is about "something in reply to something else", and "something else" is in a server different from yours. When that happens, you'll see a message about it in the status bar. It should always work for activities such as "JohnDoe favorited a note" or "Jane updated an image", and also if the post was already available in the major timelines. This is due to some data not being present in what's provided by the server. **See pump.io issue: https://github.com/pump-io/pump.io/issues/873 As of Dianara v1.3.0, this should work for any post that has been previously seen in the timelines, so it will work much more often. - Sometimes you might not be able to comment on a post. If an error appears next to the comment composing box, check the status bar. You'll be probably getting the "No original post" error. This is a bug in the Pump.io core. It usually happens with posts from people you weren't following at the time they created the post, but you're seeing due to someone else sharing them. **See Pump.io issue: https://github.com/pump-io/pump.io/issues/1027 - Sometimes, a comment you just posted doesn't appear in the comments right away. When that happens, you can click on "Reload Comments" to reload them and verify that your comment was indeed posted. However, the box where you type the comment should never disappear until the server confirms your comment has been correctly posted, so your comment should never be lost. - SSL errors can only be either blocked (default), ignored for embedded images, or completely ignored. - Several settings (such as colors) don't apply to previous content. They take effect on posts received after the change to the setting was done, after going to a different page in the timeline, or after a program restart. - 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. - Animated GIFs are not animated in some cases. They are animated when viewed in the separate image viewer, not in the post itself. That's why it only works in 'image' type of posts, not in images embedded from URLs. - Sometimes, actions such as posting a comment to a note, will generate a duplicated activity in the "Meanwhile" feed. This is a Pump.io core issue, but don't worry, your contacts won't (usually) see this duplication. **See pump.io issue: https://github.com/pump-io/pump.io/issues/1016 - Related to the previous issue: If the newest Meanwhile activity was duplicated, updating that feed might add yet another duplicate of that activity if there is nothing new. - "Comments to comments" are not supported, nor displayed. You will, however, be notified of a comment to one of your comments in the Meanwhile feed, and can jump to the parent post if you see a shared comment in your timeline, and comment there. - When uploading media, the whole file will be loaded into memory first. (Not much of an issue unless you want to upload huge files) - Downloading attachments of type "file" will fail if the post is in a server different from yours, and not posted to Public. **See pump.io issue: https://github.com/pump-io/pump.io/issues/1014 - The Meanwhile feed items are always in English. That's because those texts come from the server (english only, at the moment), and they are not part of Dianara. - The status bar icons are sometimes quite big in certain environments, depending on the visual theme you're using for Qt applications. - System notifications are broken under Xfce (up to 4.12), showing only the title and no message, due to line breaks. **See Xfce issue: https://bugzilla.xfce.org/show_bug.cgi?id=11706 - Small regression on v1.3.0: the "likes" on comments are not updated from the info received on the "Meanwhile" feed, but it can be updated by clicking on "Reload comments". This will be restored on later versions. - The "browse messages" option in the avatar menus and in the contact lists will only work for contacts in your same server. This is due to a current Pump.io API limitation. - The site users (Neighbors) list has very limited information about the listed users. This is due to a lack of information (currently) provided by the Pump.io API for this. - Following someone from your Followers tab will change that contact to "Stop following" immediately, even if their server is down and you answer "don't follow" when Dianara asks for confirmation. - Some text labels (like "Shared 7 times") are highlighted when hovered with the mouse. This highlighting color can make links unreadable for certain color configurations. - When using the Oxygen theme for Qt widgets, and after running the program for a semi-long period of time, and receiving many new posts, the oldest posts in the timeline start showing graphic glitches which make the contents hard, or impossible, to read. They also stop responding to mouse clicks and general input. This does not seem to happen with other Qt themes. dianara-v1.4.1/TRANSLATING0000644000175000017500000001546513147602606013135 0ustar janjan Dianara - A Pump.io client Copyright 2012-2017 JanKusanagi JRR =============================================================================== 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 translated (from its original English) to a few languages, mainly Spanish, Catalan, Italian, German, Hebrew and Galician. There is a partial Polish translation, but it could use some help. The file /translations/translation-status has more detailed info on this. If you wish to contribute a new language, or help updating an existing one, let me know via Pump.io (jankusanagi@datamost.com) or via e-mail, at the address at the top of the document, so I can make sure that two people aren't working on the same thing. Here's a basic explanation of how the translation process is done: Getting started =============================================================================== If you want to translate Dianara to your language, you'll need to edit the corresponging TS file from the /translations folder. These .ts files can be edited with any plain text editor, but the best way to edit them is probably using the Qt Linguist program. (Qt Linguist manual: http://doc.qt.io/qt-5/linguist-translators.html) For instance, if you wish to translate the program into French, you'll have to open dianara_fr.ts, included with the source code of Dianara, in Qt Linguist. The language code used is often the same as your country's Internet domain. If you start Dianara from a terminal, you'll see something like: "Using translation file: :/translations/dianara_fr_FR.UTF8" which indicates that your system uses fr_FR.UTF8 as language locale, and the file you should edit is dianara_fr.ts, and generate a "compiled" language file dianara_fr.qm. Dianara should load the language configured in your operating system. If you want to force it to use a specifc language, you can run it with a specific LANG variable, like this: LANG=fr ./dianara If there's no file for your language, you can make a copy of dianara_EMPTY.ts and rename it to your language's code, such as dianara_ja.ts for Japanese. If you do this, make sure your file is not overwritten by mine when I add your new language to the project; mine would be empty. This mainly applies to people who clone the code using git. *** If you have any doubts, or if the file for your language doesn't exist yet, just contact me on Pump.io (jankusanagi@datamost.com) or via e-mail, at the address at the top of this document. Translating =============================================================================== The translation itself is quite simple. There are a lot of "strings" (sentences or words), grouped together based (roughly) on what widget uses them. So, some are used in the Configuration dialog, some are used in the Publisher widget (the part of the main window where you compose new posts), etc. You just need to go through those lists, translating each word or sentence, while keeping in mind a few simple rules: - If a string contains %1, %2, etc, those will be replaced by something when used in the program. The translated string should contain those same values even if they're in different order due to language differences. There are sometimes "developer comments" visible in Qt Linguist for these, where there's some clarification of what will replace %1, %2, etc. when the program is running. Ex: "Do you want to share %1's post?" would translate to Spanish as "¿Quieres compartir el mensaje de %1?". - Qt Linguist will warn you if the original string ends with a period and the translated string doesn't, or the other way around. - Strings used in buttons and menus might include an '&' symbol. Those are used to mark which letter is used as keyboard accelerator (or shortcut), and should be included in the translated version, even if they're in front of a different letter. Qt Linguist will warn you if an accelerator is missing. Ex: The "&Help" menu might be translated into Spanish as "A&yuda", since there is no "H" in the translated string. When running in English, users will be able to access that menu by pressing Alt+H. When running in Spanish, that menu will be accessed by pressing Alt+Y. One thing to keep in mind is that accelerators that are visible at the same time on screen shouldn't use the same letter. For instance, "&Session" and "&Settings" shouldn't be in the same menu. "&Session" and "S&ettings" would work. There's no warning about this, so if the accelerators are duplicated in the translation, you'll see that using them in the program doesn't work as expected. This is a small problem, but try to avoid it. Note that the 'HelpWidget' class is quite big and full of very long strings. It's mostly inline documentation, not user interface, so it could probably be left for last. Besides the translation of the program itself, there are a couple of strings in the dianara.desktop file that should be translated. Look for the GenericName[yourlanguage] and Comment[yourlanguage] lines. If they're not there, you can add them yourself using your language code. Testing it =============================================================================== Testing your translation is optional, and it requires building Dianara from source. There are instructions about that in the INSTALL file. To test your updated translation, first use the "File > Release" option in Qt Linguist. That will generate a .qm file. Make sure it's saved to the same /translations/ folder as the .ts file. Then rebuild the Dianara binary as usual. To ensure the newest changes are applied, delete the qrc_dianara.* files in the /build/ folder (if they exist), before rebuilding. When done =============================================================================== Once you're done, or when you want to submit your translation even if it's not yet complete (which is perfectly fine), you can send the .ts file via e-mail to the address at the top, or you can create a Merge Request in gitlab.com, if you're comfortable using git. Thank you in advance! dianara-v1.4.1/images/0000755000175000017500000000000013216303601012644 5ustar janjandianara-v1.4.1/images/button-configure.png0000644000175000017500000000265211472551664016671 0ustar janjanPNG  IHDR szzsRGB pHYs^tIME)%FUbKGD*IDATxڽV[PSW5h"be4"2EBCx"Ũ!!ommVU:-XEZ|ؖ)Y?3k}]{a#\]gX+Tx+ߴ;_[{~6c wX]ߏALxxNdɉZVr{zLx^PkT֕exl%0j^S]x%hA%pX+˷ogx\ֻg|dm~DUH^~?NY,cPyF7}ϋ=\il*R+֌&aUm<R{kw?K&^C孺sƝϻIG; U؜ۛ-?XL}4g 5]Pռpb9 ^fZi91NHJ8%zދ|QW_gNn(; V3D{6ZDeuo?3g0w\D?6T|Yj6B:u-tK LH0W=vMFgY.򱯩9`2 hE a}ޭ5DD"kx[P^(b gJ3p8D[EIpVwOn"x׏44DigƲbQCG܎Gq#k~/q* #0m4T@&˿FX%%ӕ?]j%asj}fɢ%* ˅Bxzy".>e.zp<ٰٔqR S ((sppHa<+ ާǏ6bBD vvv* =+~ya 77BJj D$i^{Ξ'$V:h2 3T b"߅ȊD\7> r]ZT`Qkr7 t|V$$&@* "CA5-2\TkG0`^$ 'Px1[TʷhEѱSJھ}!Q{mmmAwG_:4ǩץBG?4Gv/<4\_22:cbmg'k2 )g+'gN:廱L rׅ`JT ^x#S;!͚>aNC2eUIMlq *.,  ]+}oڄk095ph^ Xх\NXɈZ U)BLq,U7eO#mGFqנ +{sQ,bk/!u]0Д#s9jJJpADfTPl Z[Vt߅vh Ġ@ ^}-6oMسY]єo4r{ 'TW.8@XXԗZ Jw6ub/ԧqx)h#CqQ־/-FFƿ=/n?;ښABX?# $__ ~\zG @f3謲ʦ le8C+Bj +}E@@{V'}: $9(4%%l.7{J˿66_\.8L6Ovݏb㴵kICr犕^ycSG1?;cB<~6UN5]yx]6y\ Ttԡ?YьlY0!V<=|:*){pcxR}$<*9" QhBܶoy^`|vvpH/o9偪pTtvRM^#mˈ$3*۾'G.s,nWo9<<:!"}k!V|@  Bh { Mrs}Gw 3Mv'hAQl 6\i/cfl[<+N_趫뚺(- ١!} 9e^*u9phV})Hr :t"jH|t1~ev,9&]3Zӹׯ 2RQ(ES6u߂vPj D:mJp7#|p|8wG}v2@(Q ,!2]j߶2  |"=-'/ -(qgdǠ2E1fUr.቟i;ƨڡɌ^C)wbA䳩uҞLS͹ y7)B1DCh2, :*؅ٌBkCGͼ>ΜJjzq2C.Vq 2# ˧(äEx'm<]E1)z)cdglxbU2?d!rDe9E\˥I-3ץ ZLɎVec+%zcqAQERss(PzR<6'v/&_M@R8,RmD๎ -JE2,Ԥa%{JȐחlgY<8|M8]qI8%P (OZ*`N'h$itz47$Ui!D `,&GՇߺu(<#\D]lIe,#ҾHj@ZXI3Lo{UǕh,f:Q_dcid*rC%|GX<M3}DV1i )@ Z K^[d!VFMt _0Us9I憢V) <\=is>E{|˶;=*c-NƘAF\k!:0>"BL %HZo*#=<|dДXTe`lxOC]= ŶN2cud?%'?UqYF`Ze?<2^{?~,9 =҃ҋkhmm=ξw3MnME 䲩+?"wbXl4M13A:Z.}JM?>|vNR\]3:m{7h?E*ʌ )i,,ɰF@`{4=Sptlls~|况zoA?+Z2슒uٖ}ws>sjsS>oK&u9q왞BG/Wر-c n߂mv}L|hی3 OsUOsʄDkgW@feyu@'ۇgoߢ{jKe(i\Sm I&\vK>J\>F1jT !W~Lha,OnL/;"Gh"jQWBu  WCmvla5Pie|Ovo j P7OΎkf 'o@sٸ3SP-D*wS<{[Yhi(9rOF9?\non+i6$loFL@=vREf~a6IU{7@v\'E}Vp& pTg('U0S%D0lSmn@ɧ=zx.Mv֯LnU8IOЎ`/V1{[Zga2TUՏ5C6G79uʝ)4J O!.D[mŻXś=:{ʳQ[0!`M'C\!~EegZ59P*T, rf\Jy/< ra삮Π2JV84h@MmFfUڶ+)%w֣\c|I XTLV58fYR&FU{KGZELeKF+:`3,C %jbClHv<μ<N(-jUjYGoe vMb7qeY#6Ė|I 5JscCV4dC|I ˤK)^ 힙VCyuuUF~&1#kbK|/alj.V;`;Ł)N*Ѡ{@dΰFl-yld?7ztjӎ{ dL1v;IENDB`dianara-v1.4.1/images/attached-image.png0000644000175000017500000004172512261341246016226 0ustar janjanPNG  IHDR>asRGBbKGD pHYs7]7]F]tIME  B IDATxwe]{΍CwUn[R˖-rg &"yf" < 7a0~06(d[խVs9g8޺,5Vo6\_u}]_u}]_u}]_u}]_u}]_u}]_u}]_u}]_/cy 8ו5j5踮[ P^W7m8@3UgUaf1_~^;h?W %v~eƀI}s}x,k,PZ@/- <׺RnZoxuxl1QE<` jMXĶmi/\cA)ҚN)$21kߍ5U2Q2=g1YXz׆i_.b"O GE@6,&̴X-Y|5C>m 3+MraHd)}إ=̍dab?\%m0& m4m ܏ĕjczHO{~z6<۰ҖW*H!@$1%/d{.Mj JuCy\㬜=ͻ~? ^߱X-e!!VpmY(02,jMb`}NJy,l `#Pad۸,ahWZcMfhrr `JLd^=u(7c5lN/ Gm!FR!L]Ѡa'?ȯ2?ÑSXڨ0xo*۹ً=%ZET1:C `y1k*]ƄM 1F̯drH!, ),a@e| 1p k-r; 98CYB(nSA~*Lm]#0B`Hhc!X}ʠj1ÿԝ<eOkĭ1Ꝙŕ AŨeEz~ 5֦khUDmP=tV \8Ap3˾B`I)$0Q"ʭ *ױ]p +˺䲚&+ 3V ȻEլ-wq[kK)w==S(QaW,c BnUM._b`̟.OoB&ђU:A.rh%j?Ib".aD (P0B7yE4DB Ē2$(ňV^QVǓXA;WS&W0 - ȂG9lP[^Y[!F\1nۨ( {&ĵۺCH[_ }/# s߈!拧4³|;{qR(@L A- 'ѺѶpk@@+FIDJ-bwi%egUix6ng<2NLƗX N%GJ֖P @fJ/^9vD0Ӵy-qw?l-)Ci:M_sxé/gn?]D2>K'S]iӁ(d02|!7:# e`tϋeۥX0E55N4$,c2L@D~"*f|`|1,YZG-Zt `tmɺ@ 0ڷeXUQ1!F 8L!$+X%kUU4'J̴GE?P j9^Dx:qB1Ĕ<q4'~AloŖ˴ozS[M`l;PV~/nW>e ɉvn wcK'PDcA[@6 U`ĸ8vl{?Db%<ŲUnꄺZSs^s91N"(A8V ]ZNCVp! C>]5 CQlcؽ8"MqׯtlN Hoᱛ_ٌHÁw'ø;L6)UCnҗ0G$[mFYeѮA:ƴ! pi9 K1 Ej`HIˎi- 0bNKk!Ŏ#h'{dsnnhj$s:dGltBΦp~3y*@kV]@prw5&b­bN}}^m^4mM[D eۧ)b<%Z`:@SsY5?H,eYHi!@e02Fhqẕr˭"9㕰2ydqyn-D8E%VHK@pE=onZi\K7d6~+vY E=Ύr+,y0;Tʊh ys92GbLBK2c\ji5q3+NZ(cܴ;Zحȉ۶.b($cX)WXXH[U0Y7kceAlPRFZryg&ֿ[07[l YT!~G R%R@.V#r;b5iN[K35¥LEYBF߇ F"kV|n^]8dq3CeS%(}fWۜ[X\q[Ȭ=d`AB[Na;~v6!T7vm0V]hٕ5qD#"?w{ozwqO~S|pHZhFٶC{tr٦l_8Sy!_G 2D#9y]̐(9f9Lɹ+fB&6-ɠd hYNt1$WI6t-]l%̶v)f3cw|gr4Oԉ<|y^1 P0JdUsLxȢr fAkı|Z)X2R Z`(]<1]p9e/*1&[pfI'( AIJ(KGXO@׈ސIH|1m]hj z܈_|8ᩝIHG~QVY|:fqCödL'cE0Fdi [-v_}.eׂSje},3KHc;6&VTO `t,X[jaF(/j @L~faŀ2k"_Q8bh%в_HŶiYȴ Ҳ XrDųPZvhe 2 <2`r}.|beĆ KC붢l)$5&qc-jqb ,$9E~l gQvB"ZfcKɏTs2\ςڊ0(,M_I,% J[1+v9Yt.P!u0§ڴT k6n__w(k @iр06&_P"w.w翅W x {!61>/Fᗦ0  Hᔡ԰@E/Hc %Ӽ<IReaBJҘfD5؞YE <, M.kEb?}xla}jkxVض9 ŧmc|!%|&z0W 7MLPFx5xq@Qڅi^T-tF#ۊL't:!.ʲ869fbLHDZDO%[I;jG!pmYȬ3$) YI4rJۣkm7cW@77#6f-P Ai7Rشьu,fu(na8l<'TV5RH|܅BfNP2aOF6MקX4e + b|/&Cz=^$nvNj!a7VƁQvZ@FX͘++>*-Ĵvd.jAn\[]R]4v1 䏝pɈ'S3ؕpž?$ѻ/ itzP)-e ,$VUK`)V6f|!_܎p+".j҅l]}mA;f[. $[L*~Ƥk#X&˨fRJ 1 d80"yay\LREBIʥɗ p śFQj%DK"*6^ dm0mG -le/(ZY81-Wq bAM8#1#)-% 6Ͱn* ['%mؙ|b!ޓ9۱ו \Lϳ=?p0g.{ML^CDjfhw2ijieF#eKvĠ[H0Lu,|C.I7#$DQ3p!ϼ噫Q̋B2R"p݃ӮS^X[4deZXz MiXUp(MX%li#l Q0ƈ̑:D$JE8gXb!0HH"6R81Hwbqf;W¥dOa ZǼ)n>8Bm V;s6^Ssu,{W tMDM%l\7sϩ0Pw&Y1ܷ/;\0*R&/vkX{&\nkgpniuZ5$!H"&FCP6ۄ  %@88'уV{;K,p. NboVBQ3^ 2t,,Q|îb u=g{[|#ځg =+яMH˦ ٜI=Lehxه #[)<\f4/ЫcR`<;9Y]6q܂,:m"_kE^0Hb"@Dd2eQpnq'mR-Ǭ1McvҦ-)DwOx}S\4OuNÇ,.'.܃~_b~xd$(^`W)8wÍ;^-l#:hzPm?AdC  wXhQiMPȰ-;uʼnO%ar.{qq-n Z)p yrv#Gĵ" s3ǸXi39&Y&nK*15 R,(泈<yPŊ,#T2%" |"'syDTVM#<1 7?EGLs]riٯ䆃SH)x7!!+:eoS&b=b)&ã Wq Zn&q3'``]˴utn2`4$x:d 9ʖl3KTei4 g5&A||l?xr坣8N[SڴCV-\6|d'uKfNeē̍ 1&ч>̤:j<Ε]oy NU gfuKxp$d`=߆\m/Ix4zl[ J=c-r{>JR,`=2ӻ(źFEͼJB#$?AhqJNbnJ X>2?vt#Tgd&9TL(* {I^|`wZ?Wdxr7ՕZ FGvBENf342J AkVtBN0t~ c46CZQX<`~)&-n%uYVXzWc֮WǘmqsQŠCcg2S!^27z; ~+M(|#ĺ` b6 }N:-#7Z1)x=1Px>710K ]r FZ6A6Oqp\+4e,ǣ84.&fxb1,/JL>itZ=}ct N*szҲ*Lo6#չݿH&0_٧ /N3/ _`,'i,i>_go݋L'J(6FXv&N9CP -R"\F'"~S_fZs=3O2 %ٱWnG %KheC4wE0ҪV  PVh5n8{1Aq)R)z&׽b:t[irp:&D퐑!Sr,5T IDATɹ1)?3_}7\!O\?F?Zi[ѿ$v0N#ZZ'!t\rc&s/m(Vh;soУ}{aOAYW7K+eNSamcle cgn Ab) ft:zXncPЀx$=¨qDb-ǸDe48[|>GС!?}Kd.gvfo^@0wNr#5N'8150}y_XxJ"UFnQ\9nO}Uc ?s/R9n>zjyflP Y9q#KǮ_l~۵ګ;DB^?@&&A$4뢔%y*}{8q):q Ui!YCyujz;c.]Fr{;NgV>Į1G. ŠCLl0"",]/=}7r8s ˉjˑTѸ >KG ϻk<Kj% :\S # )tF@d L$Z:MڋgQsQ˸EɗY:ll4`͖>۱w2ˁ3 {D 50o~y{N*q쁯Fp'WXL+W9qe%h0hu^ GQyoQoG|ro76Ml]W#QVE_fYyHpS{g%;Jxjk͵xՠLUs/xp'#;XxVJɛC&%Db/JD%EVz=uƐjlϬ'݈bpHmj]!z/ a,'ػsIlǝ46wxiZь!/оaHF $l59y89x\G0cFӇqc5HxĝL6ý;]xOЎ:HZ<3iվr+ڀFʗarPX:s˟#귢tBV4Vۤ: 锯`/[&H&x!;8"&N- . ,i6q89Z`Sm1g DJ+X_&?2I;en 3q[|I~'{sTuZ7g8R0%v#n$Z4Z#7d`Uŧ>DXLk8<7e"_Bj|%fO>ɃWmɿyV=2qLF> _Ŗa!nU籢*hjF48Ҽizѽ>uwLw機]L=H,z /7N0+[iS.܊*awy۝E*s AcyO}rxBXQ bGhPB-cpja湿5_|eȷ$6z/^? Ƨvd# 9l~ hq jn]" 2&2P5)[TR0X%NtP*Aճ/HHfhEԪS֨6i6DJa f+4Z!{&KSa?-M"m2Q=`6"_cs_7pg;M1s2e=џuVZ3f|K<"߂e'q/ܷ̒8BC7kÂ(|E$-T{ y"j'#pj4n[!W?cJčsea8v:4yL1J"l0iWJU|My[#Lni剃12Od,HDDJԔXPN! Jn)E70eIXOOه_ap: {2{m*,vv,6ܺo)R '堍Ñ/2,> [|J9Ks+x!*w(͓u?N܈==TܥdPZP%g?e)elA(Je_jb4\dp1Fv)B  MM߄ )LZ= zgm D Ȕٮ(bq$w _ı$4˧~=|㓟agsm' v+OOqG(lv)\D"KRWӏ3Yeu 1>=EK 8`4F=Nȑsl`4ST Jw!#drH74Rfݞvh߆CgD!vs( lBƵ,/h@tqGP"O\lr@`M%ً퐝"lrƄM^?+3\eR {&}N^Xd! 26<>+<-puof\,ybR<{r9/(N RVY(˳KhF-fshtK);UafzFd4nIxߠ 0X޻X -en*cRjgma|36OJf>1BiN |drЊ -TdAG 0̦QgH䐎ӗ+28lX$Z_2\bm *3НS[ /qtP>a_cx0kgN#ѩAJ:|'\zlU[r"1n]]lkz! h@"A8 8I!1:3>̽;q4G,3|9gVJ`=\VY $adVk9 &aHh̸<%@mzA6Bdx$Zس[⪔ߵ, {'gUZBXV0 _Ep! [ U;&v$@,urC?x@q [8Hc2нH]T>sҦ $3i m@i]H `5kX;ř[eb.b~K -!=4եyA-ʷ\eP6Yj ?TVe?қ[hvrNFfwދ&tSyXho!W(!e SwBZv `}՗8^ɷƭaG^nTn߆Nu/Y.MÆddy`u |Ŧs]bC2w>B-bX}O|a 6-c6(ifR#HF9&&ghZhZ˛oKx؀;W1/ ={Ԁ(ң\OQ/ ]vx0DO`@othBł,~݌fC;Օ0XX$` `}v c6vaue?MU z}_*~*7a=':KJ !"DO QkA,|Ð .Cy=y*j9Uɰ2JqV6ؔ=IDATM?۷oߑGgTʿ&\;Jo 4=fĜ~X_PhdK("q_asoYž &8e8v~)X&Ul4j Vv~wOϜ9sB[7+ϿX ʝuHY?>P,g/0C_S3uw$H!|uZpb={o'cbf BlD 8ǿIҪz/..RdR @oҪ[ҏ~h"O=|̋.F=OC5lZ@SR i%Iih.H(R˭)@$G C8HwhTyy=sHz]&=,&+KK0L B6e4R1bLcä"RCx (<4@Ws$j!IGBpl !8.wб:h:('/{^g8o! `Gpz0`F m7)B"F%yo TKdSBJ8ذmVw9,n8V<=O8}˕&ys,50DtKoBН)暼BR1uP%R~5W~@ݾK`K%o4BQlRqNz}iqaq~quvs˼1M,*XY\ mx 8AZ}^>ӹ su-ip'Xb%Xb%Xb%Xb%Xb%Xb%Xb%XAeҺүIENDB`dianara-v1.4.1/images/button-users.png0000644000175000017500000000342213152476504016041 0ustar janjanPNG  IHDR szzbKGD pHYs^tIMErIIDATxkl\3s_7 R`ӂ"UrPS!Tj*FHBъp*jP"Pǯݵw}ޛUd!˲j=3+ݣ9{g{%o``|@#I/OܻGˊ8- >Ri޹ˠ`^yd.5BH4GbIUӎ)?ű6pvoي[D88Q8 U 8'#únƔݯ?F;} [1{FJ&qɆ5u L9 JiqXr=>|{F)uп&Pþ)=V] X&w7}2< LB~JyK}w O4񏖞Z"xdt:@{MPMe6 $+]Xc_%O)N/GX%xFű5} "e L!pLBͭPwCX*CXj0 :1.xU -@ Y[WnX(KhfaJ/)eqKDrW-&‘p Rk}_6黄ϐH%3IH <&?Grf ɣ7a܆t ^anAvk`ل kXiGL|pym - ESell0  ׃ +Aoړ?ΛCw /8m$ӼZkCS^D&˺o[} ッ/CD %1H R Yn{/R4GVT)0 hqP@Jt69lC1#zJET^\ I @!=tbDqvϾĭtO$qɍ7S=Qu"^P= 13=K2T-SWCru t~#F;P@7#i!o ˗/љ»IoY &'ΰhvp)$$Q`o~fMq8* $G0??IcεL)*z\@d` .M6+=j#{ Eia\BC[7c[( B4)4 LY};:al F ćzMO-\ [[Pf@.ɳ  S%Qcҿ&-IUrg΍V?x<}&JUU ?X~E^.s{&Q,dKtjޜNx7%_ƞjPTĢAίOk9e{B<STgnUBՐ{Bߑ•÷ "ڸkVM>(MBZU~a_ @73\]}Xw@SYmGV;7g8gfTi^W%{\1uMH0!=j׆]ͧKe'WNeKc%}ԹpRʈR% S2ZT+Z²TP<8/<ӖcTIENDB`dianara-v1.4.1/images/feed-inbox.png0000644000175000017500000000154111472551664015413 0ustar janjanPNG  IHDR DsBITO pHYsvv}ՂtEXtSoftwarewww.inkscape.org<YPLTE!!!''''''dddnnn~~~ɛ<<<@@@DDD]]]{{{ۄƕ˘ȟѣ֥ܫدⷷܺټ(tRNS237>?JMrsO"FIDATxWaqI(L}'K%dOhB 3Y˞ y,W|]7,P @@hʰA {pbX P>2tPcA6lasRGBbKGD pHYs7]7]F]tIME )\ IDATx}{eGy߯s_ffiY=A1 +CU.'8V2LN ) B+@ll"YQpR60"!VIvvwfvuݝ?N};;wf{n̽9 \9W+Ǖqr\9bWsc_ P]¾ >KPKDz}Lxkzt.ؿ~t1\p!nжl,*1}0RBR0F.';.jΙV)Cc7!y_T-5؛A`@ ^ @J A`՞c5}Ό.k2<{_ݶ ZXaJc`@JbD 5+}@oo\JYRARB)(*< UaeЂ=NLK @>ٍ?3?O04~>9]:JGDFʜ%`9fa?>ƞް/RL>F9`-3gfB.A(#tFr󇦇R QӠ >Һw=8o3 ,{e)9-%7t\A0=5$Bpd?ֹ T. dv4YX?A(,(?0D9H a7|`& ecƮ3iQQ#ĥijRx8IrژxVYV@ OeZ])UGȔx&Ja9zAQ=Hk֥6n;e>GȔ "S 2 J0:3SbEhA=Px!3JYR0Zk>XV P,D1.:g)JNb \J E$i GsM5eG\SGS&Dn%8`^AgiP qC, 5,9V(e$J;x0YµkȲ Kx&FrMΩ %$sDW'"+Ѝ)kN4Mi") -&(ñʲ6J*M{^43$s h7L8` ;%,_P+. B8*-NRʅiBJS:<@$=G6Pq#"w,8FVsv`toeK![9 i| ·,eYopMVRP־+ϑxXO^PRƚ)`yc}EĮsF4[1b~N'nb!l`;<[%JRbل`u 4q Kӂ%CNK9H )2!iTe , "n"RHy:N_~ O1p!.!IBX_.r\8FMܤVFa3g\Xr fY4[TB䡬`dh+4M!t;&\6%.[1<(3+XN}@eRȲ |l`<3k5ٳh=kʣ JGqN4jz~97{0mnner>P;e 7uRʾm(.ѷR R3Jjp^nR t]q\1_zc $q 삇# ,}m']MnBJI"/(Dgz Kii!V'ҋ~s3amOx(eH9nn,CصB 'OXcl6 ihcX\LU'MXXAc{miac ) /w_콱4lc&IR6v'YGJ j&>GYFz1 4S.\>n` *+T $ y y\ab#%0!\OΝ?_l@M';/%V 9 =[n >3Kd(I}#6)njNW*goX+=gn;677tW*  8,r#F3}P)U[d=Ggܱ3 M8W 4<$w&Qc 666WلV YPX;JH4;J|~à 5sI){a{ NFUulnG {it\}_ut;AUaH ^֑B1&Pz4 u7lP"۬ g!e.g0z%eRa+ϱn,k. `kqs MO{vYG"RJ\pPE|صK)Qr ,hZԩSlllZbvvsu666JLjgv7V e? > ?9beILsCR) )k/xiZ{ɲ Oƙ3g4Mj mΥ/>,˜fsMKoFPz9Ó6sssz?_DÇfnn@+%̔`rr ӐRѣ;& @Rq[` Bb_~*x9GA^wY>ѣG!B(077[O B~iĄN^ǫ_-U a^ffƁR 2DQ  .vZp`1h4yk8@kZ(\3nkbbMЕ9[T*κdi% 9OL/--AJYs:55U(|"}>}oaa~cPՐY-}N A ah ]zJhxEZëq бEVX\\tEFB2J9=ɦ0;|rm& ^C@?0==W]$r:wl{p >(v؃|n[+099Y}DZi9 OLLN-NPyH>P=Ɩk<qܙ rA>B ;oOZF VGL !055ZV4`x3(2&YXXTt_|0pffk16 * w!!M'rR5HLðO(*6Ns5A e{BiZVf (Vx(?8rHMz7IkG6L;]Vv); zr!V.Wf'ePV|zߌ +`R>_A; CDN tQ;&IYofP 0gggK3þ_B܌-(n-3@_y~Na [x=3M7`2 Q>P퀜ZV{/ ?)1!iB6-)K~)vaaW* [< e2B:23e|aqD0<92 $q&" ;55UZQsҶ܃/OZckPo|ۋc A= w˴X76BC>yFQp9O OL||J 48???0o@{Y@k(X#G8ō2 8k4aW]@r~ՏUS]nZ-d>ˮ!ߞ^;a4\R2 Z,ñc.~0GGEf-q &Z@kF<g_؍x΀4|?ϗ&hs%z/BoI5+qTii=g R]A ,@qlPL^?o*[Jɲy٪$زvO'c |嗿8n;t N7K|;E_BkKS? !|AQgk}̹ͫ8|䈛} zؚm"AIv{c;5$ Q\͌ڎcm=Ru4FWQ`Ԙ9j g[U$_ _^v|y;lK6Dcoޏml] 9y~L0˸ ӿp[Nu? xq/@Kʗ-k0}&;j˶V7nHܲ-bm4*2i`@@Hgn pr[/WbnnPJe'm IJ@0}L|Iٌ02@R(mv8 Y@h})^8^ZmІz@`xbAbSW"n #aq53i^hʠ)I:1d:+_4eM7;Q5w_itў%fartJma`B) 37 QIڇ~-N˦ 27_.O<(@_y.d<냾@Co@ NS((JZV3 𖥷Ok}}}@cN<$B>P C_IA7 b܋7}|8/>E@z,6zhԠ&'''?/زTNDqୖJՂ>7ǦvJ^X*)$1z(["~AQ _*t=]:Ii_jBZod6'n `@RO ; ;aIbVLɳ}|\:w;F(BY:#'w]4 zÍ7uKʨA0h_z@Q؍6]IAݤw^Nv_;y\;nfi6o_p9h0ؿ,@*||Y0ja< >zk6DK/ϛӷ߷߁k1c#W7vSiO$0F@ <ш Zl6iwn "Y[=W9*611y΢X^^6'_x!~w/NRSKvP35U/⑍q ko]{; Cw=FD~oU<^ad'YNZ-v:&n;{־ n)K n3}ЛMC%??GxvGPW5/; 5[Xg"Oc]U@U!  la _;ݮUo~K_z Eg`Lz<'k-+DV2!!'gמAPF-"تa5xdQ C@ aawV~ ,˧Q 6ď .{ݧJ %8, DiS] ;~ i%*A4bVVql?&dhoM1=z ƥB 痐[p!.4W!ȡS΀8M`J54&QG + Յ춑mB,~sr ^s}_WGLH (A Yg`vhhgOU0)0N`6LPC=UOw[.)$]M7ۑ$=6:?aH>1YƣOs8yM )D5@4p}cx10edB,YĄ{wk}&y%9aXF0 ::Ν9?zO/g.]Nĵx5/ǡËq+gYN,[nfAJ)SՒ7-?3pD \WkU0,VdJ'mW^w^%>CPo<6cfffo|3?<-{9T0;W} AYb=3==->]w;GֻEE5۬brqb v$7/D^E)MVv?~G¦v7:.7+u*DArǟy#ߟCMpyKԻ4Jr(IETK g,X͎mX`y6QL +ybB]uLۃ_L41+Ǖqr\9WQIENDB`dianara-v1.4.1/images/attached-video.png0000644000175000017500000003066512163340053016247 0ustar janjanPNG  IHDR>asRGBbKGD pHYs7]7]F]tIME &}b' IDATx}y]ey֞ϐ 9+U D۪$ 6%@d3@fF5 5t@D "RPp=Ѐ?-}>{yfptLq~wo PyJlG=1Cğ։=#I\N g^rYDGS12u^bfY:w+v? 0">=pgDh9'$#3E]0~䎏? rMy. B Azط'a*E`w9G d/)8^Thb#=L!-g"ri9;kXvmU|foi 1{XfsL$Ir͞X;?kaH?_*Dp|%ĘznbcvIRBRJ1Z4G;cF[UV$(%%$"0ksba}\-}RS _(Ep7N7:#ֿ5q'"8)%(9[CлN;]lIa$5.laT* #tL2vj/ӎ.%P+=Z@)Pb-\F!f1u #gV'I FRBGn&YH%{2bTJ$F O[cҞqw lj`γDlHDQEsО@@%%1[Y}pm\FI|v4a:Q$I+f!F:BgtKNzH|G$8ؿG4Yj-s$<+" -w{Ψ]FUXwFt[#&|'f 4wivdjF.iAyI"R-K3ZC#AE?Ap)~<݆x4Pk0NVy8 dMq`AhSAFCze_*R1cafS|X.d[L b!Q9ɳ@Fe 1DM^kPFHBY9F($δG b#b3!(Z*zf5Zvب#ݿ 4X ^Z vjY5+=8Xe/hoQte' q%gEaorpRI48Hpy B9o_1@=Ytc]] e9L%N3->~kf GX/ ^ pp޻0L $V"PW0%~:ʜOƝ5dGGd b0b&PͫƳ;C4iXngxN;1 "ؔ>£pDe(;KGp cI1(fMQz2H@|~W+\#'ȭbȫFWhc՗/6cٖZ>|f-}6.|,NpaKb'{  " B"2qpRʴ5G#->jAK)gDZEBIP1ЮLEt.I؅!aS@WBf(g*@^(ԗHqt=_:v6{CS:"V =gz[oT.p{sg%A y @⸶l0rw3"e.t`{^Ho j 3 ;頂=䟋(2.4]@6f-ThEו~/E'EҠ+rɄ]xY )X>b1}gJ@| P<\4|"bx1DD2HG M4| @B>N}{('tEQv<8:A &пϻ,M[l+0Ѯiܮ.@`˷bԽ$ 9PjTϒ(X-*D1-l7 y. "@I`E@+)(gq"Ҥ4{(.p:vHZ(spהy{y.c$$zV` CK \>! a~!@a}+|T{a"TqdesHYʂFT@!Ĉ@p&D ҁg6 C",' t:eW'|!Drj.*Aؗ琰 \6W0XU\OHuZ¬lpwwߋ4MQ)WXo[nůKDK `DL@)˲jQN8q¨$R6ȅs9g;$qXx1P$S c+e 9Ld'dI 54 $I?Rn,7 nGxK_ /1ٽ>,C"MS|(ċEŏ] 8T |D4jJ9.TrȠ +g[(JV+011[\\9k0>SO=ffPB6/}nJK^"H{(EN"\m)>uA8L$A_?㜰(;ǩJr{7x#~ \./9/>7HT3\|G/~Adn}33066v0!:%"UbY!$p%[w* vb;B Ցs$ ֯7t#n':(@\EƵ~񍯁^*ש>{ŗL^oPdJ< 3m^CRmcrG?u!V+Ɗ1<奘XqpӵB[hW &"L۝,rz>kR(J\T(ʸR)㕯|eܤVMaoU -T*Uԫ5KeSN^.T*CQ\GǟO=z)ǯ$_eH{<^joHmkcAhJ9ON]xZ݉RHJ\<%m*mkf:?p1ܪx}r+vjCVO5yud'jc"q.~GPVQ*P*Y+;Z%/9?0Z; ~8|r4 T(ԭj lfngN{Ν~;նT* B,j{D j%;$$*9ӅT0RS$ 33;wy'.VBZCeh:hصk}>v,ӑ>Z\NpeWF\ERE^BPAƲ111I\9 X###T*žɩ}Qolmq!EP`蹞8?h0& ^* QgDRFRAZE`bbozӛq1:MرYƶ+X ˗c['l6}'mFyhRIP.W122+V`llXlQPQVQP* "0 7Ίj3>9!˘x}.[Źa,ȺK=%ۙ$Q e5kuB\^4?w߄!<1V> \*'MyOc[144ILLL`||˗/(*'{$) DvS* `64|DQDۊ%A` 0}D$W |YM6c)̰+7o|1Je֘akeLi1>> LM&''022RP}<'*Wybkh, Tճ 49:I3\8 2{S_JJd|驩)5k駟y%aµ~~ׅh;h4n1Tp0>+WNb$Xnz44j`ep>LA q"26 駽N}!.l|@3Bo߾o}o㦛nP j ˖-$055qZ\.Ec^[&P_?[9F9Ng2hCD5>ug@lr 0R V>cPJȣVt~|sCᩧ~7nC=Jqp񘜘I2 5P|Өlb \.ZbBjOy:xǰ}sn=s1EQsr\GxR!^ozlNzKVz]33Xrl_N86mBfV\x޸h RIۧ/H,ب/᠗N1:ܑҤBST|9qdIi5t9do@h4_u\wMh(qe]TbDZltzrI`xxr8xfn'h ?j"d%}JYtn3y]&.l#cLVr}rL !ai 8\*_{wlU|lذJ\r>,t:l\< i>>h֮=7?Z- Gu4Nvy4u]7ot03NAt Z dC UʕsY@3_z]QqԑGjA%6mŸ|򗮈 jLOU 38#5|vu~v?9w7voؾ}; hȯ"Mmkʾ\0ݵattofw|ovԓOni&z!*r$_}x^Bz4pp!(bT??@f[r.-: Jn^/N #VjuBkȣ^g+O-&Qg"| M|P~KK;r0vE i_cffF of'\gIt$8*U OYKMkz[A?#,W`*i ո;|(>úu Ve8f=#ϷjJ{YhPRt [>>ہ*egBl8qz zY#5 l6o@Uri!߈-ZF. JE06GsV,_NxtZ@\.V<|C)BhFTFVdAಗfh["!%t|)TUaÄmsN03ڭVe>cchhB|,G>ƿBTl/ŗ54瘊"g0&5΀;(:yUaA}vhRڔ `ɘhKfa־-  Ox"A:"V6o:$ڲ,cF1ё~ ;oۿ QiPZ^J43ױ/0աD-E /Ҍp*ټ|/sb V\Y zP+mU8}kTa8 e[Y3P`k43ɋn(vH@E_rNQHK_Y W} eĝsdg>Ρy{DT8abM4z ɻ {ssIC=rPI,Ml1??9IVg4Rg^zyFrD A5|VTMtI00h%އհE@=] ߈AiKdsgggm\$4:{t#1ZjK ^1pl(} &:]y5 y PvZ}KS(|at [1; A"Y)'{6=sHycP}ƠlfS;6+_ƙ{j4|7ʵeƜHWgͨ"hІz|p7rbԗs~ p3]t) ]h#)^cAZ3߫qt4EuC*6GD FJ<}7LU ;ь5I::}/or&J՚]I!iZ3tWG?=&il @,+Ouy[p葄Pu07;-hN׾pƷ} ;:u!C ölblk0S>#t#Ѡdq=9YJE6G3235>'|شA:ky5qE>7E/>C4!K3LO`a^ZG4zlc?W011O|8㌷ٹ]H㰨l5 ULձ--ع(q`7`s(ufF»u&=ݸ[abpq|;j9Xf5fffpg+09cSaHᮻ[k_jTʥ3& ; !Ɗu}n_Gbn".zxމc_j/~/^/k H>: qQÆ AkƑG"*$<2.rIt>)Ґ63uxG<0s#Dm[l@ @0bqu b!$C4 (z/KdZ?^f5+h4/|[݊SO;;M?ERQ,5 r77myk_K~? QY+2ÛL1ǬYgV_Wv=` ;0<Oo@/. i7W}şyy+g@`0Χ-ctү6lJsUG ޠu3 nI֭[~S 6,㠽^ϗz=z=t]03m뮻v8p6@4KmG.=,(fs 3OTCCu @+/V+Fo).4 biǐq)!:;X5O_.^v *EhZFG|5/_O[?@(!#=&h,_?"@4}+* ̮y] j\?W4H{c;,;$|0Yc~~Yaav>L2dn6*hFݶQ@ӽD{ɰ0Iz=m4 q `Ɗ۽W9^'폻r)M1qz=?t1gpN: $>:PH#2c1 qh4>6B~O0fJF_/̡Z5\'5#؄,jnslw#ǰ`c̱Nx0@c/Ob GƣKd sfQ< 9[y鄯SP DaS9y|ocڃO3i?F M#]q뭷`bbϯ]<Xѿp,@SD$?(Iԓij8!?"4}ҥ(HB^GZ3Ȥ}#n#&-W .pdidWMN d9-2ܠnPVv:t^R#|>;XwܱxիNcV-mQaY0/8'ZeΥs\e}(DNVIL^z_{wl[o1@nM |EGxfRvTJD"Ob-]NR`0d,|wgDv*}_x W+؋dD>xxR"f"#2n@bY@1\K,ףbz4*;6תUKo|_<"P}F2uz ,2 nVI ?{)@gcę(Mw[v<}w:\]'o^/WbI0$e.:PQg^mWT!a2Id*x= 2*>[hwįt^o;V5` gs> f@N oiG3?ƾr(le . ѹ^{䮴4Y\p4ʻxN͹{dA})X(veoK l]g뮻{ :[\liN;i@@oעх7"#IVk_J]&$~]t7h]Anھ(nd`NwӶ??% >.O@HFB(m.oaQtNi쵯{y]0dA24wv[Ljnjڌ1ez= q{9dY^bddKI,˸^wO9 6=ؓe}ٺZ^5Wj毜x۶m&{`PD/ kPiײ̹'x?nV ?9h6Fw:ZsR=c߳pϾ| Pd礂cgFM3Cj 1E2OINLkX-M3eCc)Jub(i1&ZrUpwt_A?wKPrI͸=M(]7-x|?Myٲe[׼xv탁b~Z`ū>5O<*ǣtUW}3 ~`! m[eӳf7 K/78$=$lDR\ըl>g[n݅q1[@RARC4M177mڴk]vٷsh?+MVT$U =xua&t l`xme v{_t1` Zb9Xq8t:Cǡ>iQIENDB`dianara-v1.4.1/images/button-filter.png0000644000175000017500000000156511472551664016177 0ustar janjanPNG  IHDR ssBITUF pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATxڥkQso&ZkR}J,tQnt ӅkWRtBPXG}[m5&c3i&s AkeKdUzgS'Ƒ] $ƾTk2Z :2 ?ͨD)-qj=snڑ]zf'cRwӱ0ɋ旫/n^ht;ƕ-j V{d+/4w}EJ89yGȠz?Y+JD%JZ/G40\=='V^=E_0 0l81ffL2F5R@߇Ӄ,Y#ȗCL",ISY/}v+ 3y! ue 8,ɼiHOSĉsG\"9S+Kuݽ`7tǂ-]W÷1c]k]H^Ã0 5#*UMh%`@5DjZpGH`j`<<[ Np AL8}FDxx"4PH KLqmm$ Qǰ``1(SU}3Oֲ|^:a&lIe@?nr,9<˩={::R^/pڵTLKq{< Dl$2ۀHdY;cR0 ,"/}e^zPc# P([1:z?or0Ɯo݀2+E7H eߛoǙ6}:nɚssW z!%nre  CW$7R,Z)qY"m qȚ)bljGn_!9~_^[WGټ:lK>/zLv20Ɂm?zQ=I*5%m .Y"#ڰLRjM߽;ޏߜ;.gX`|6W^{_x ə潤ʇTд"/ݔ sI>wWYy-x1h;"b gf)nF^ c,dycXfJt\6a㪞Iɝd7nT== V24~‚_HYY~``Gw0{ HF99َב5TF R&bd@#3#c80u]V~5Ayòe-.Fn;? p//ӷbN_E)b-?%I*5*J|ӎQ_#f;nH$Ekݶ t~XA@3 g@.dv܃$0 + XVJcm[74e21qZ8=䴈Oe#6dxӽ!bmK޼ꞏKN 2t7OPUU8܌.{ \j@7ӊI4 ?_;[ke4^ 8N)!ēgώQjI8\bm!PU]n7׃=p6ŽOyIfnGSxx 80/Dߒ lzj ;q2r-duW!ba0s,RFUs:GrT>}cT6%D^C_B{%H bFu"X#%d˼t< kn!ԗ[Ϸ~z x˲$?uag/cÂK&70^@!+|%t&FlG_ >FetϽ] mTT}o=p/_)V\n Ǥ4ɷƛ U.n!{q)MoK3.ZCd 3z@Q92'sq%=>O/t"c!Wt7=V)9YtN{SOau`y/,+ 䣮d|k@8֯Xh,MY . H{ "|N^ԾpJSWH{#;Y+}߳W'ZҰe,^p13DRA|vV "Ff \͵}uIF ѓ5։>}4-K(N\N'5W暵koeg1MH$R83`A!r99ru˖-;`@?wtc2b^aV)m5D'Iy\~=,H?.ap NNUU y$NUPO8[>zPMSWW@ Plcwyga hO@τ EJ PXcch8T^&NBsADYw(~b6ʧn*:! ~GGG6i")J6ڑD,ʃ6m_j n޼y;~*@F7CxN+[gkϞ=}1 ܙ)w&x覀3@70S@]c9ջP4h, iQ/_P[%+IENDB`dianara-v1.4.1/images/menu-refresh.png0000644000175000017500000000420611472551664015774 0ustar janjanPNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATxŗ{Wǿy}߽w(`Dj"?4!&H4 b J4!XRbe7sg̜s;eY|ɽ}{~wNH)񿈀;m5|*o\[X\R\nKBػ7"?_5n dIg;/^^ѻNX0%Jc8<^>r=d^d;ݷ9k箽{5uM}eU>UJpqGJ'ؼn qĖnq} U j 9+6c_[`0$7:Ѕs-g&nun^яTq((1048(d s,vb^aJ2u-8Hծs%\K(Gw(\fѕB,11 l23WT!0I(vd7/OE-&mKN xK'XNʬ8PoQcC: 6:[0M] 9q <|@J22 ?Ly[ S =3b@d?n;/&t A^cwFj^N+qeFz'369005EpT9(`sdΫo9f$ h$FPlP zbӑr9}ЬjƍGP QU@҃FXr/G}8jء"hG6j$-H,nCa):D*qƢC;C"₰'@4fm( i,d>)ja2:NA M ^$5 6}Dm]?-x賔V G(6` F.0O6q,EvLyUNiW9Lnpld;n9*3+~%s{"wEng*iR~[NbD@Hj Hb< V(` hӸV)G8ԻVP #efXFB!\!!ՀZj+Ca`x$Tl_M<6)0JSCP)wilrs_7^l6PIl_mVZPՅf\$()BD,YAUbp 'RZ^ VvTiQr 1MҤ!n })@6a\\T\aBD\2I }0!ߩY>)'9 RF)Pc 둥!Q].ndnIEh.ڟ 9m .-| yL\Y ^/tl~#ƋSS#0B]3DKk`R M󯗤#ɧ790r23-)oq-: B2tAyVO,R|NQ`3 @v)I0JbE#M; &4a٘- KCŋ͈MtyO :=o{{uCͳj$k L'+nhO_=]sxݬ-,w2J:`tkadtI>g۷clz; RIENDB`dianara-v1.4.1/images/tray-bg-high.png0000664000175000017500000000123012405111645015636 0ustar janjanPNG  IHDR szz_IDATXÍW  zYђ2¢~R<9wos'|ksy2w@ c>IYе2m9`N1k 0!b0` rS#ѼZXÓ@yhyBE+TZB)Z:lBgN,]9NW'|`,@ ՝ =2o& Cmygdmjr9ϋCFA{]4&jƂɒla?l.Y34f1鼾09PƤEkMb3m6 Xi]8"3e1@7޼2ۯV *5a[pUsѵބx-;p&L+&V9$'bM7t+Pj&d`7-K0L kE4Ā3_АҔ&CLmŋI@ [VI+~3V1CMX(Y)d-.?͘#@IgW:ZYx.(f]#y[@I\(3n߸p\hkI㹙h\o&61LL ^e?[U@5,Ԁ\f %y״rhIENDB`dianara-v1.4.1/images/button-like.png0000644000175000017500000001314211472551664015630 0ustar janjanPNG  IHDR #ꦷbKGD X pHYs7\7\Ǥ vpAg IDATxڕytTU6TUF21!B Qb3" -*h (sQDZ% F@0LÌI>]sbjsj @0e  _iǁ7 hiaywlRڔ0sqb`=IM Ƞ^\\ 7l܄}L0^-ϴ<=[ F viT~E//.g}8Gnux=ugG>; '@P n/W?9~2kƲ;9wr࿗A~l^l^z\jNݎ;״O>[1MD7MߢoC41sw֍{=ڦz ܭCN쭹e{< *)?iFcH 2{jgjNXiXiӑyJl7.zLj«wq7Zqb;You˛7eސ1+}eo) *e5/U#fw'߰[շF1cҏ- mu}DGOF**GX3 W-AX2t%ݢEC5kzif^ۼS\45kjOn_})mm5d&d{u}:zh0bevȬȬf\H:ً8C7ٛ .`aXaXQ,*tΣՈS#N:t0ӻW}z6=4i2 WECc閑fg%_iw)ɛf"zjl5#!%{.좍Bi&*P-z vU*zŬYYFG1 K_XހG#ڼHh:=l5ވ!y|/hEl510b1PĊXJ >J|r4?2>KfzJ<|iHk7_:q&eьps2>ܣG tZt71b66;nS>ཱི΢'G?::#:3,jDG 8]k ®+ !E!E':qujI&}E6|#+iv 3a9ywZ8+kUdQq_gEs]EȶOԖjyUR% cb"P ߥKt &Y{~Y ii~_Tqᮇ~瑥YYuszi$l`Q"Jx."l>/ɚû=bNWQWzqXL3 º-v],7?:nWPbAܶm9@B5ب7/z,2#U*hLc ^J4:: g &4mi^ <];;b5D_G;h`.?O8 λW.z( P@ZP,td-{/eBX]fզi( vie6Y,|3cĮ" Pde[.`|nr.‹sa0@O߲<'ɚ֬5n1 ?I3pXi,?ʺ"`uYi-wQ-?*Bj'.A!H| M&3k[5o9 G,h29+Y "t93 6[0x;R@acFP(tèŽ / ^b˹^tܲD-J{/V'6%ޤ[tK8Ȃ[# :R([K @n^cEUG~z#YPkNόV18!z.`uf߱O'?†I0. UEIv\jm c7o({G\c)gTs-Zpk3o=ۏM6[#qT@r ,j|5>|MaM!`=EOMx/DYwX/ +@{4HBYLSڻ3 YCx Bgg5;U[Z[ \JJ;~6Lvޞzj:f-⪵g,z uo)}SvF]倿ٽ{yΕ\d,%)1@Ĉ1@! ! @}3$ k1-} I9 U~v36xTAO1[̦(헾0/Bl cM?C1GukXX[кt`W͝-k=G}I] ꛠ,GG{=n mz[ meGqby*BߠHYQ&ZhW,>H[zYY@" lE,!|Hl楟1}D_Ds}& hD\yCi"/R5ò²k vOtu(OO}ލwl^+_~! oOe,-A~1c]*ku#F(dFShVމZ{u٤%/e33Z\lv#X*bW1`BF?11Ґv46~3.CIe\eIEI o0jQ XeZNSyeEuv2e,y<ȟO K%((ZYTEc p 8kIxG jLD u':^{iVUߒ~Fdhz)#{ʱeMک[uMo6*Q[^,2v{Bއen֢XV=8+ J4ɲh%Zи8iOQ!aSæ)Cͧh3ݧW^g@O /W?[3offCdU41 *_sMtrp Fh~Pt@Bfe]$O^/ҋ@,%V.ͰD岰? [A,XxHRH iBV%!!-tZ5r>n( V hZ_-lIْݏ.{lԲWoc&n 1A GGSN chlhL~`lX},`$a|`(X0TgeaRkR ~Իɻ ! , jRY*6RB֏TBpDaOa1JmyXu}8 2Z^sHkeSLx|/WPfs`t`S_PWdž)JwC*w=Le4*R6)- w|fwt\Z[|,Y%> &K#< |Dxվna;c0?5ROM?ԩÓ[PR&S!BM5ҕqI:wAlʔ[z2WlPig#ڛJ 2-9>Y 9mRMW/zEt 倲ݽg^W^@cTaMNvݑ2wP%vke"DDsf VڕSJ2k.?8׎:=oY#q|r󡡷O wJI܅Im CW>9ɕ LdwA;iW h2l)MU#[J)aاvR#rZ+ ˔?WZ^SRbb}_u#Cѥ?c9,7h&6wL(p$,eeQ?o6Vqґ^Ȼ,=hoZ .ҕQJ8hԷ]eȵל9nDZu;+Bdpګ =ewf\)#w:vWz"> 8/ B3]gϺ:JsVOe]1j=J1Pm()%h]WrpJW- z3]=W8_\Awfr"920]8*R{`JyBw]󬠝AKjW&Ӆy"zTXtSoftwarex+//.NN,H/J6XS\IENDB`dianara-v1.4.1/images/button-next.png0000644000175000017500000000270111472551664015661 0ustar janjanPNG  IHDR szz pHYs^tIME (SpbKGDNIDATxW[lTU]>fL[F&Tc!%H D( j@HI%BˣB}@[yi-ahX5LrkLwq;8~q]Coe$ "Rm5=g'yOAXaK$ǎog7 ^ $QlfO;> F 4\E1sCXd)DOxn9.JuÆa3#W)+cV%<u}!0Q6{򴂸qb[ܮ7gIOM_GM-lb,;eQݓ[Qr|Hձs%j.&8!Ck՟}+DP|/J]Uvi 귗TVwW#t QEGXB\J-"2gs?vx+me1䑗e vX ˲` ӄa4X>SSEj/dp!=[\}aM.*)' @Ȉ S&{({3~37\cGx|JjN"89q $2Y\P (&Qd`Lл% r]=E@gh΋*'C]رG fhDa , Zڐdaw͆V38} L% 1ȌTpL KxGcP<DiZ&,SAr,𸉜ǐ_b1ALڐ,n߶'YмaM L5pAAPI& ]z#UomؖO5{A"K$AfTtRNS %&(.//012<=@@ACCEKKMNOOOQRRTVWZ[]^^cfoprwgwIDATxmOA<.M+V!$ A1u01zxCē"mŒJKi}̎]PeO旙A/D/zm. "c[jHwj_MQgf@\RÀ}ΚҊ)!}6& 1FR!~_éV 2Xa>\jj\Y wf3{=2lo.@RI'V Xa%`C>+ߦbZO6@gzY@yꝘnw>OYضKz8݁:1 RD%l=z8%d&2_D͉x_"p-Tm `W?wj,.}O*Ow;P1' 2 "_?v;y9Y o O*c; G HA#@>z%d "߽*9xQ$T#X^|ƈĚ!C05 z+ A'.rJq\U# BYrv+Ýv6-2hX ) a?pDEnZfc _ *IENDB`dianara-v1.4.1/images/button-delete.png0000644000175000017500000000246511472551664016154 0ustar janjanPNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATxkLu_LHqr}etMBjY f a4B,2WLpE礗ߞga砲 ^|~>O peY35=Mkok + Q!` # +!^@L)0VdMνbtv;A7[I!~U,UdVU8i M9D.\;{G4aD K<[ocD,=oIODy)P{e࿚"":x(_w32px1S(#\eaMIJI&5c%qQ {NüwKf8- ql![oR"j;S!=$'cW(;xNߋ)9!7Pč|'lg}4.Gkld:Z _fV/|ېEN_WWbpP\ކKY["{J=, 7J3x=׃s!1knږ/W4i}w%!g`PF:zAeV} DgTSRqmftm܈_W@SZA4wax7-|-|T*ΛZ"8ly#|ذ+DڙA֏zWDYB\91C6;"D Wesx~ RS\BW!̳݆D{Xae'Ңi Bk =EơШ,!^9绱`PlpHQ3&+^6\jS"EPY&yKqs]i1\} a*&S w=6RzFPw0n}Ha > *[D:Sah-x}55Q_9C>fb8a-RE4'CZH2|GP/\o=CPҏ$c DcۈܦR,IJ!oAbWOtk nh%5\ 3n{v^LyGz˽||c\]I#\IENDB`dianara-v1.4.1/images/button-share.png0000644000175000017500000000146112261341235015773 0ustar janjanPNG  IHDRasBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATxڍS[Ha=%XR Ө1j %]!#B{*/P=e$BVHaj뺺;;{KߌFbpf.aea#. 60d !љ?ND;|+G$_<n"V01#)Bz3}4SD= 9~>|H?q" :_zpӮ ZL6 MK[f6!hBN\n1WKcf71t鴎v2NחtY˴\+QP P"b<gTu>bt>꼊 N,PIENDB`dianara-v1.4.1/images/image-loading.png0000644000175000017500000003301512213211373016051 0ustar janjanPNG  IHDR00% pHYs7\7\Ǥ vpAg00WbKGD X5jIDATxڴgtG0yF,`1`dGM4ds0&g0Q PΓjKx/{sFyKU]v3!HY`UpݴKǖ':Ol k&DD ?2]sYf||3fIZLX3e֡tЏL^#xf=eY|ղX''ע^cZ9؟9b+_VGjEKoI{έ4u6#(p|^,#_GCSYd?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)M{>ա~V)'I^a\kA퇘ęط-LaTiwIF3+/W153 E(<]2#5!$!uVG16@ !BGXcYd`,ji֞j bD:^MyN\69wAdCJHw81!2Ơv9b ul{<_uF56ڞNE_Ti=mA[$lʹ8wMXؘ%@D^ck(pW564֞hGoFwmׁ`.Au\ܓ5 r,?!$p{GQP҃'{g !܉nύw#zڻ0T-4 QJJ7(Y 'LJXLx"2XQ:4yLvrƦ02uEQNVpH̲Dm0xúz̕1} gGF0md|T[k8 $K+A8dTV0 10o J(2Vv@RYf ^ ?Ll>I2,]G D"hÓ!h"DS@W.N.-Aw .@9 A$H('܇5H-rkea;Q73L9)[cUe1edu~qBkx1P +)^ {zVT3 !%B.j!%=@YyK B&cY, 0@M`;SU\kC}%@{DxNڹDвNg|M2 G*Jr$ ;#RMH&bb|Lh@)p3 TJXF+E( 8P2JR1 (DF+F! , k<)D q*VPĮ,fdz^FAEQKڴU)@=1B.Rgނk3nRDVނoaG" 8K"u!"xöd[B#"p/uXWAʪ%ѱ5 pm̼:ZXЉANo/h>k՞[Xk-^@-GIE$;: e?p"h݊)E˭7 YgWD8;m%H$ Su6em4ZE"RҌ6lB 4#JhzHkD4k(9O0p`tr 63%r57$eݰU(IKIЋ?֊h0 ka&g 4ZS+E ؖ6Vw#oŮ&d gBВnQ x,x<ZE>hm<[$ڕ5* nݺd 2O/㷧'pa)#,L'~?1< \|wQ,TA+c*য়#G$!Ij9q4]I{qzz01~2Ԛ%ϣW(o$Hg}SNNhAg%&''QVQV1;7|ROZuRt{hiFIr|tj7ocX(hիWĩ= ݻfG*51<<.<T PSg"^/O^L㘽p\gL4K9H @Tb.L<<Զ1w_7ϟJ[ǡ:-@@n1ve y b+,ҋzac$ɒqu~~xt(Fd0DOO bÿO| oDd|s`){?˓SIENDB`dianara-v1.4.1/images/button-online.png0000644000175000017500000000325411472551664016173 0ustar janjanPNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<)IDATxWϏE{fzXeA d1F=h$x h4Qތz(Q51ѐhlvu=]jafYV=tNW""<̔*" vCc 06 .BLчTE8 PRث=<<@k1$a4LSQƈ oꢎqe=KsKظj#bA3nΎQ5w?aff ;f5&|^j=/oۃR ׈,2^8?č;c7 C7S|^.^yr7֎dBs!"ܒ_~ϝ@N7Wg=0:gȴCi?];Ld5o⇰z;OCj' #m+ 97[WG[6lց!,<En2 Ye"!lY ]9bxiPmFsh:ꈾ\ d\ۤHoWd2 ~+z?X7F\t[ۈɴU##4Q: oj @>$Īji#M9ގe0NH sEBdkeþC)r!02w;=ol2M(Deo~/I&b|y'=A"k#HֹuvcWYt$P!ʖۍyÁ_Cr{y'1ZՖ÷+ԄIX~ԗ#᱕;%r)! kuKVɝqJ,;[ZAܭMblO-<@1m~( ap{hr&{(PPFjr(az'Zh@Q',>W:jTEJ( G, ulҦ yt G5FE Epkb3 GWzv4h ;}N[9FuE pkIlǢtPiǨ*h wÈ+@u}ϪTp 'ج6T;+,%kd`c#G^CEiRP&dopy i &Φ +sy% !JVUbYE pnnIE_L 2J*F[$vEUW{1W3bryWC@4Ҍfr,r\ 2kIENDB`dianara-v1.4.1/images/button-edit.png0000644000175000017500000000324411120733360015614 0ustar janjanPNG  IHDR szzsRGB pHYs B(xtIME Y bKGD$IDATxڽ l?w>ďc>t]ctt-.h^#N/aŗPS\ C';GG].tC#L>B }u'~~et:͊%iig'W%(;T2tXrY HuΞˀ V5 b,0:1;uo ,BV yH'6g9.y*z_Yz`z Xp@!7~}?.OjxmT9Oeb;U%tr Y @W'P#{x'o'WL^k9U, e.T(CW_ۃ[ͥ??KP@4lf(Ot:)u L^;d`PaӞd2k?౯8!Km L܄o$cx*# ؼ}wOm2>h&|N'UUU444`(mw$u$'q3D*ggދD7q]i5F!-/½^/pAY`&;=wE >>2@_eIjst=7;qݴ(K%8c][ʎ{CT"nD,!:⋨D<ƋG lJu-Pߺ`}V+͜C<ұkwn̙3S)@cSB ,vSݶ Nn2%a@iSm|%d3Ύ/|E4۩,yn, "H&rϟW$ yv`R, IENDB`dianara-v1.4.1/images/list-remove.png0000644000175000017500000000205112313672234015626 0ustar janjanPNG  IHDR szzbKGD pHYs7\7\ǤIDATxT=oG}w$AlJEB vۅP$_`r >F8K6@)-Ȗ"C"ivB.sѱH3xݙy3e,h0D~~st8H/CNֹg޺+ĵ` ăB͛'Ol. X_ǻׯCNMRB7braA47qPښ|v4ɽwt<ܹcW^|Ul4>?\yp =GN"!MT2S{Ur?GBժU[|)pAvv֓ahB2`p~ cX;;?Ϥ1eeBZWZZ>^YQ숧#'L S~cxR"`ǾV6/ ZK(‡׮ybYT9A*5r5~Z#%Oӧ<2Ge174 ;\fM?G V~ vozpnŽo/j͌<74X% $%Ѥb0Jk5 =زKȐZYI%mI"4`E~=BFEB&) `,cX'ߝ"zTXtSoftwarex+//.NN,H/J6XS\IENDB`dianara-v1.4.1/images/button-comment.png0000644000175000017500000000150611472551664016347 0ustar janjanPNG  IHDRasRGBbKGD pHYs B(xtIME .-7IDATx}KhTW7d2Ǩ36-(Q|V$h)I+mRЍ .\X]nwXZɢRZMJYDQ0L23w枹!")9&;9Tg2$[GT}PĔ.Xzgm=Q":ɴ*6gM0OAs7W^:Kel(;J)lץ¯Fj h:ʫs uE!w1;~q<>o)#+q{*=aU1)R`J?l`H 0XY8A Y V[ʹ%FiY`{S#>^JRyEZz]˩Of2c0Ǡh[S= H%-Ea$N/{ hs΁{ôʸsL ӛ=yB`-k=]/Waz1Lbe;1@*+g:6|*ѹcE. vg",Z !ei0?lJnenܱk&}d͂|NC`t/E}|V}8l NtPgwX(Ό5TrNfv퇞^%^&YWEq IENDB`dianara-v1.4.1/images/button-post.png0000644000175000017500000000237211472551664015674 0ustar janjanPNG  IHDR szzsRGB pHYs B(xtIME6+bKGDzIDATx[hU@/"bAswaw~622rt||ΜyѣGzm/+K% ]*YZfTNGpWVTXTl=P$#H$%b ۭPCn nK̽ \Y`('M촑n׊;TsucY2CMc9CAW4$4#l;nwCیIX6D`8s~cc kP e4#gV_ x>YRohٙN^Ig FeD.^DwR\BhN}nۋ+?ON`MK7]5M3_){K(8j ]@Ѥ rf|029%RLnd$M- :! Z*I@)PvVܬkLIk#d(n%@p=8{|>?" <2߷C&ҏ?`iىP8K˸ybo-: (K(תD]PXo4PTㅿh^|s(BN$zhKpub8Nд4EBX\\DtPVwٴmX&,[hk5jZWb1KUU" Àh4*bdUtזe |&ylvyd7Y%'o/^<`kIENDB`dianara-v1.4.1/images/button-busy.png0000644000175000017500000000272111472551664015667 0ustar janjanPNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<NIDATxڭKGOUȐa׸!aY$JU4d8~~Qpb d2""Dfu*Pݝ&0ނWWw9U{u11u|l#md!=!?OUc=u ۏAǰaD$C˜)_K~ 'ǝmMj~[;uL:b6"I8$ɭ[{-Yz5eVNDB`4M `~mpV|NX8Kى]063 'ov$DuX2? iHKҎ _a~{ZE`4Ygca>b|g><@2OKb9Ƙ7lٱ,qF%VL@ (n^腯c:8+cm JF"Me$H9f00qyQj|Njܿ-7juZOAQL>Dl'MGRjx g>a.-B҉I/1CPWnyǏ1pUGx&.bCh`i5Ұ6_4lqP~||LnےU`f9\ɒ'>P,&COQ^';H;̄$D[hS.>ڱ' ЮiDZ͢VadmJ ,:>Q=Q_\x_J? l& ӈK=Ā] K^HX:bt1V2tՑf)%bdßf41_2b l 3vf{Z4ǩ1k~ |->*7IENDB`dianara-v1.4.1/images/tray-bg-low.png0000664000175000017500000000116512405111523015522 0ustar janjanPNG  IHDR szz1^/dnf1=!Q) c@ƅ pM\yn@Wed 5Dܒr 𝔈U)nf?PIp( JsIB, $spVkI VEMYqsd}{Bp p* 'A'!r'e?Nz\ unc';6j`$H ^nk_~?IL9D5ʹ~&Qף*4?~(`] 'f3>X2v0̾/NeHaS p8l<(C^x?B,ۖpvh@' 6qEC4\Kno5Aؔb1gz$s`$3UQv"HXrIENDB`dianara-v1.4.1/images/button-save.png0000644000175000017500000000235711472551664015650 0ustar janjanPNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<lIDATxVoEmvbv " 7gԗ T*oE4""Lq[fFzwBM3w775 @_D]λΛ, KxgHlQ {?iֹo~4ý:8߹ۯ:ȍYN,~/S6Hf$IVc웠yOgy~`n3@s -n,zž&D9a4Kh74 a pm V`5ɨC1S o%\(.qRCQә1<:WJP>4$Ȃ PъOtSq1u+ ^]1.Bi5' "ڎZ]Y95}Qw<2-HZMU.BpL5WDd<`u!9 hQt%a 'C׃K/0S]['=jO~z ŠV+8[I}k@X^?):50Mjқο0\`ⱊ/+0.saST, ,_ CZŗtܸyV(>2o3 ON( KS((N)x QKD-(dg0!#jj:   l@G$ +L"({4tࢢ8>|q$ +¢K $9_BdEdM FX_/z`V ǁ1Z2a]4M/I |9ߞv>46˗.}P־RU-E&ωP( Y+2Mv,^lULƣ?766~gx0 9=BjDП>(h*]2ѐC4wh4<"+츮빎(]a'/8"06}۱gxX`:Лap 壻f;4GTOD#K@4յ^xψȝL&5pgߚғCIENDB`dianara-v1.4.1/images/menu-find.png0000644000175000017500000000325111472551664015255 0ustar janjanPNG  IHDR szzsBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<&IDATxVYLTgFVPYgf}o0ðZW(-j["uTmjmҤOKCj]7ԴE[ܹs; bu %xW* ~¨&N޹+ؽi3I^&瑩`xx"@[`aB.B@a_k5ϧ%%AQlEl,aaP(g lV+$P*P?w"!ua*H%׷EBB-z`"2,G2;`6t8iw@&!|۴4 $Ȉ/s:7lD{[st8~G`ۂ ۉR4ÝlFa^n"1 l6<**""zFNj\tzd9hjlĖ>y ْ)2~UeՃK1o7p1 l0vը5-f4X 7I0-Гطg/.;+_F~~ϔKQ?8>U,S2DORB ȭQkaQW'pYTσs U܂::1xBCC/=C,vn pwWgVlb?4*5 ij"29pr{I˖VnCT?\" #yp9!Daa)|J`ܸqo ?5b|\<"##Ke8%*b& QD-H2 rW6lEn݊ヌWzں_~&d0̏҂\s!#p0S`C´;fTUIMIhf?w|^zE.5imasRGBbKGD pHYs7]7]F]tIME !Xr IDATx}[,yW3=םNHڲ/,@""` >+%T 0BJ|(o7z 1SGF@ -P#L˞ٝ[߻+]53g{m`0;U}Ώ8?Ώ8?ΏQuys>Cϙ)lhA!ɹLhj4x p G#{p{pʕ+Ȳ {{{w^{{{x"<>.^h ;;[[[}looS`4bs]YyB2'5;\!= Ne!^QJFq ԇM9y]HUW=JGtoumu,ZRm40qm@*!O&b!F(t: q`6! sLSF#l qpp !$ 8F)>19?0NV=3I$q0 K !f.rq~SVAy,CH)]H$Ii(d#Y:MSJዦx`'1.e)S)cxͦ4M! e4 cl6pyzh2B\m4B@pY Y^`R$].ˊF zG rlR{r-lnoCPy0cAh9 jX)AAJH YHfS_FPtP2lQurJ )z8RSCsҔs£>`Cc䢶/H5\%<'+ \(]CQ9RkyjC4FDsDpt'2ʑȒ yPch^F?{gA{UO rB$N oVbwwW[ NzZȁcoo{a녀s~x! hrG,(Ip0`<֖EFraadGq||QdW8Q ( GQ!) ߗs PZ0}w ! P$iV@0Ͳ ) MfhL73>I$ wlKIȱN Q̓q(0 CSTaX F677!㺘cs0[$$|^~^Q.M' 0ߗ@e HQ=A0tqCTN9[ϫGAkxꚍF4kgcL$r dS@ `%lYOB) 7Lc܎ #&PƓh+dU}tK xƕqi bvL𨦡Ԁ i i"^jL1醙'MD76%wS骬c"2By<@1g>(i~߿{l>߾n[ s;DP羏}4ah~QWwE&1zQ/Qa_?8GBɤTXaF*A!;$# ISA߁ C2@ fe\^:W\)mll vMvpt:ŋ5FTn# v R&!f<hw:Ԍ BXoZ(BZd@v%qN [)ZaGy F H 8!Rǻm+}Q=+渜#ea(NF 0`䊒MGF*Ֆ8ww`*c,"j:\@ԯ$5l6x ԓAyPq clZeI4~vռH-iu޼0Ck.!d91 DDY U_SjE@k/0z6GL^\wh"ffHN#J%|+KhZ 1xB4 e5ߵԪyZT@߯.vN`FScssF1H AbӨz$It($P O~?gY)GP?(Sq0,u"nž!S݂aק9W,tF$ *n`+YAʽ{6Oh9_Re亮*_9n"\:s%"gεZ#X.gZ諌^~[TXle32kSMPq$=)B^|e1ok0@rY~zIlZKC~_ hZ{|>+|\~Tgs*WBD^p 7Q8DdRd4Mu9>9jǧ%/Gϔ[kR<R"Q!MzPr#"Hf~t=Z0<)RuJF^>HfSIu4Z J`p)cLQN ^yul ]G1lj,U7?,{f@-+`omh;|=w"1Nu'*GvZQ @'~ T:˗/QimssS#`gP!|Dž p8^/]ossTзn($&r xy^Zf4 c6aggt:|>6ZQ9Rn[FP)G<[@} ^Շ'Yz8aA]R3_[R+ U(*q=(riU;iZ+YVw׏ gs(:MSY gXqVs",㜫BҖy ]x{''"^v(9 CloooqdzK4_V+ ?G6J!HM}r&c}*7U)r9#!<0&:h3OcM8jpqn/ A,yNg!|fS/Z뺮(Ҩ_5qfiE6Ȳ Y@ޛy^ X㸤< 4>Ӹ!|&OPRnڑ@GG*tCqf[{^^ 2r+5--]vUܽ* V3K_{2x |_DHf~_WyAOS|l6C D'^Xa{c(3WVJc3aGyַV6|'\ AG} _w G}TG8K-rxQfKK~_%N*;jG^BP>bQ(ۊ7~ !ǝs6%{<2ؤ-+9X+ 2 Cq >64djj kQaGb HoYlFQ^hI TslqZKhc 82HKƸ@)b(y}I9k06mxfQW90\QLJc 61 2cMv]8b&Wa! JuLJ8L\5.B=υm[2jWF%d29#+@n\3Ӗj$.AdMj23z^,@M#rRU(ON)K9Zj>fB*"ǭH$%vOicCq9( T J7P_w]GXF?Zsj0oX/𠺔9c}άˇ$I !Aj*p15>]Z62ols)XGL FᑽVagN!5"nY9 TuG/z:JuKLKũ.݃~|(w}2$c q/ E E E]wDT8#WiOcqg~jc){ZR. K58F.Mr(.)˶a70RfVKGFu>cggG/5sv2AbO.-)W(S`!mнGݏnhC4 .ǻ}yWյ~621h67s8F< t4+o-#,/ l)@RPH ;ɐ0mGy@"B`K ad HdիW t1MraRϑJJ$IRQh*AnN@Jn{T8|ܑϙQ03DZ=dz>ztӔ.smTrV2W0"t64|jJER"li *8-2<Ũ;b\޽{%iQs0C%ヒ_|QnGE:S,}CjS5Wt1,2+-V"7_i%0eZTwnƵkDw5~]lmoc6駟׾]${{: jS26Nq;Ώ]t$n-'aN>+i-hXJvu\v N>5$g|3^CJߚ){9|SZfQ\WIUí삠|"-8Ufo5GiEW=<1Jkg;Ass>#->%ZM-t'o#eӃy 9/ rnQam۹KD&._<'$U <)$^p\@KG>]o6-)26p'uL.'ehgxD0uד/]>[jgWBF6tXu2xy0LcIU^>`>?x'~OkU[><>ߒ6q5\I0rT7 y͓2)LN=)å o1|5kG B[BHysd|rx|d1N K#!<= C_… Bw;oSO}&N)SC2.C]oٹekjc4fWSZ|9h:yGO{Sb| ώFW7?fH}5ic/`0zFyHSA $ӿ{Ͻ +|]?q,w{Wt2Ә9rC*?^я^Q?0ws^V #>۲;m| ^4L&G~Ό>.8_~~/'uQ{̍ #k,7SuI=LKou1IDATxM0ƴqE n)?_s; #rf 3[z_u Mu0x)drdᇡlzo| `|@ϙ*'U?V$bVKL3:`cW ^s04ij0VK| _O[b(ߩ ^8ls Ki {HE9o4E1lV ?d}s/?o3ǧ5 FQbrpo)[9jМVP6h4?dw:go~gFjmFh"ssi]vǩ{uu][0?XH8~_lVA]a@RX\q,'[/J ߆35qmt2* z>$[P;uUzL||OTv7z_ꫯx]<7PxY9* lh3V) IOC*7˲gY&nя~?,_+aO7z7r*Ȳ5Vy6u%8Zy.[_ܹsJ3!3~ `˗GJQl`嫄knPm]a, qM>|z>vZQ5ySd+p'u[LȴJF&8IHR8> 6jpNSU⪑t-#!XXvU@j*}S8EIr<V¯U^c]% #(:"gxja\O{~~SǿV¯U|k-c[d.&% "}do5IX"s1\x{{?_gw{L3`Vll,g/m=}LL ?c ;]_{uWU\)\IHD6!]2 *S  |pO k+54M&^y啗>o `Ŋ~] jx@U^FZXX'׿_reٻh j>D \$j8 _y1N㽦R S,4t:^_KʿUn3h>k:"3|B+vbVD|8Mzjn Q-/ 4@,{t 58!IBܤɨ/anl4h~o9~|d}5!5"{\ iNThqVy)J.hDx@XIOD"YVA17 ` Dij4 u# )E=G yȕajm>7Ѭ3JO =VG?ANmySCTv,ڄƋhB.4{sLe Dԗhb+l7 qZ X\.aiN(dW 8jI5h(CaӮg 6hi n|cgA<[,d~#jCrKB~[Hj^1l~ʏ"jąU_}r;Py9=6o1zqK%#ĥjij6`φj(^x9$,"i<6|$^9oC(:|)2`̝n'nDmAܶq8Kp]e-H@KH kȌQbqrsA=B6Py/5U~Ǫ2Nz9SF#n'ojsgb#8WP1~nhb|܄vϷ '֛jiYIv%=Ĵ/NbB~ .q̞SCfՅ}ݐST9JJtrYS1 H:Qٍ<5 R:3EJ}r󠉪7.v}jjˈisɐq]ӰcϠ,dX-b6`=7"uVYlu23n BX%%jel>pm0qG TV1i%T"*H$GI %5jćDa)?+)ô>J AMe:(kl޶q u]& ! 1#9RCj %D2RʶҢ~ a +3. Zf]vw(PmvOe%4h:|u\d5ph g0`UhB>N@b2f̑RK8ՌiJl\e}ҁ|"!ɑSOH/IENDB`dianara-v1.4.1/images/button-close.png0000644000175000017500000000404511472551664016013 0ustar janjanPNG  IHDR szz pHYs B(xtIME  bKGDIDATxڥ}lUg?~r[ʻ-+" ` l:4F'e7]f M⒑)SP,3eI c%3`7>BKo~s}yyynQ]+qku+|U/R P]:f2y}P`s@>B``AΆ?3#@ԇawq(d5øe˨?_4*x y,y@B [P{ZJKڕ+n2L&du/(=gΣ2f@qzv:>A95G+/A P%18pQ~=.P^/~U8(ؖŹݻ唼8x$gk`ŀ)+VP1`i,3kfqű ,! dPiȊ,+ qJ| VYIŜ;`ݤ.SvחHuMw F0#d2N|&.]J)[&<'k(G,ZV;d׾#X5|>T7_51izzЦNgS?_ZJŜ;xqxK1 0\$ 4(|p4Uk)֘fڻ"l8NT䗨A H&}}DǎexlApdBK)PRW_ AEdB5j+yÏ?B2mT sp Ny PAl lV6H`!Rrq;+"&M%Z3D75t {ZX3E%f[;F" X8FY8C3h9o"VR{\M2XՆpQC"tAyjAV"̙#~c9NbC E.ІMIx)@))WPTpaSYsg=;8k;ז5TLr9\BUd- Pl0Y8^fЖe,\[`'-!,%,#⦡W$0DȤC|6* ~9r ` i~8656{Aq6A_ :L7AKr${+A \ކʕ. zzݜ Ft }X?hIj%wCH|{_I115n 5IENDB`dianara-v1.4.1/images/button-cancel.png0000644000175000017500000000423711472551664016136 0ustar janjanPNG  IHDR szzbKGD pHYs^tIME (x,IDATx՗kl[9;v8vnBӥkM*QFWtih4nӐ&}&1&m(c+m)Z'4t6$M\\v|n;= QS5\ĤiG-yϣhu O4Ųfӝ?{{gg >\| ?vٳzz~/:i*p?aDobD7oX Ũ#94hD(誊IsH!zeϞ+ka¦50QgGqr9[G߻`IWyMU Q }|Z/oX5bZu!z 0v씩3N^"w|$A~`e^߷Zx†K50KN Sywّz0kH |a-PDcZp dGUx&,c՗ZwW.;`iހKDoC؟Hy|wY 6.VIE>ùe&)XzaҊp3oVH݊wӰE(|:3B8\ɩ3Uy&-YB<| ;%n/. 4+'ct o6~=t(A>W0&.J (n,Y%Lt~]8Be)$!n7\>7tꀯTeD*Le2J5Cfu"5ʌƤI+6MgV27/c_T^+H@3T,$F-%dZxR G>藛7=Ҿi9jt6Tzrd) @5=?vWjcr1Q@[Mib( o, xYeE xMmrɰHv=@G h[ʽ w9R8Ͼxj:%ޠ\^nZ4yeD*[*,8!.LKDZ-|k J O.+Fdx+F 'O@"m𯁡\֏1F }Y }6}K'쮪Ѩ @vKL 1_蓚x.+@\[eZhI X{*+n4,ɜXr68|$R EIކs+_j~fEяnLQ쪝NDy I -HVGP- r RypE-3!6Ѱ`M/ޮ2x- -p1p`,lds&e{F<˦Y MJ!noW=zqy1x3i  GiZߚ5AEz$ , eѣ̌/K6\pClBHq r3@rj#oa:ϣIENDB`dianara-v1.4.1/images/button-open.png0000644000175000017500000000340611557627111015642 0ustar janjanPNG  IHDR szzsRGBbKGD pHYs B(xtIME4HBIDATXå]]Wk}?I2CD[m XMi$4 _P_EJ A$-YĈ"#TIM Ȅ&$d&3w{{N*)nrgkk=Bcݻ377h9zIqei++v{yv'ok~m af(˒pHcqq>||mӿ=sLWoXDPUD5A V @.^hNC9|WƑդ7i9UZ 2=3c?۷o?{#|񩿞ܓٱ*,Ldh F✳?~bC`}߭^uȂd@{b~yjJ};v {P@ )G~s챿{a^qz0 U,FDkSUbLk`&kk}~<ر烴:w[@F`F1bfʥZ U5Q[-53r|9Ѻ 36TBu57q8pjزe{Oa){];pCW,ݴ̌рݵ+fa9GezzN@ۅ[ٱkeq?4__h0P bH,Gzlurn`1kRpnb]<EQK޶_||{'!XaD(FIJT@Q^EYb̈!P@Y!Ua%07EFň'>㿿D`0Xg5,"IX[K}.v ҟ%ZXʎ #]yst;hFyP zk d>e.w%"{͛qY[^g~7 NOX|/-;i8s -u|O>((#& .\Zq! 'PVL 6*2%D]w^_&V *ʘc d:I7@di <E4h[ %h)$S!Pūu,Zm3Rq^먄D5* $b3 fK)$ր-IٰCNm攏3-Sf!V "XTm'sJ$-JF l(,C 9B'n"dy|`JVǶ"w%\37Sĥm`> uq" 'ѹrfa#kljڝy;6};t tZsl,Y|fuFc KV]Ç` Eڀ&P8[cgA]T)BĿ|h>,d@GWZUBȵ^5X_c򁋉"$UOy ki^jdM#2r?F݇cöPScr*:Ny,-(:/y ͊QՊ}2РHj]_\# b.E%VȞ坱/:9[< ""*f,X4A``bf"1U<"b*M0AIMD$"v_CIENDB`dianara-v1.4.1/images/feed-clock.png0000644000175000017500000000365511472551664015377 0ustar janjanPNG  IHDR szzsRGB pHYs B(xtIMEGnbKGD-IDATxڵYlUUH8F} I1*IHL Zh`@d(tk \:{;OzaV*"䞽kڧΓG!!www W<{SSSgggYsGpoc kq__ʡ΁`nLJwW4qoc k'"',l ]7; Zm-I-[d۶mr p$''KBBDo[n5iҮk^03/]zb[+%9%Y"##B\J__tttHRR`~)*.͛7KY)`&Jy /[3<ٴi vOOrMjQR|aQ<B#FN>Mtvt6޽[~?XD$ 7'{pbRb n'##cЈT=(rF!&zT. 3w!XO(Jj170 (6p;cHN5oQQ l~`‹%i>^LD!D: ]P5p̱g*{eŕs+`LQkaXU ?#ΌHV.8𣯶F`H䊹M 47 VupedH!W^cǏKllb p9 آ2 pmvjR*F9%s|QDޙ3gL]5_ \B+K_onni]V.'+5a;-Ck^sSN Lpe+:ӺmϩCњfBImm-O-nMig|񴴹3 L|ʕtuuՐA=iq >NIIbʒ)MHMMDB} ` c۳K? ~;ݰo~"c|>j {[UUyB{.fummSv#L]Osc k^0`/mܰbnuq5e?R׮]~oWsyAG.IENDB`dianara-v1.4.1/images/button-offline.png0000644000175000017500000000213711472551664016330 0ustar janjanPNG  IHDR ssBITUF pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATx}k\U?8o2&Q4LTLIuTXJEq7Bq#(d7 M0h-7bRk"P4i27y];Ƒ/37|?ܹc9=i32%xu |vL22"8QL\aM]XmA:nNt=e}R*cunI#Jgl2U9_8KɕV;૰xǐ(:n&g7?勧8v!|G;U`ѹ3Op-SR"w1bEŅa2[ +!<,wqM$rlIc 72|G༮|,O1gdq]a7@-rɗz EA dC'53ϑ߸a?)(+M;~>BHwۢegx ed6{ȷ8j6NZ^ ȊvHY/uF(oDP}ۍ2 bwXf~mHx58ʃT=f CUcԮ#?7ujlc|@ڒbm?:J;Dh"Rő I~7h,eh7Kb(r2[dxy戨H9ɨb4enkiclYBW})X%j2Lza]L*5KHyŧPOR;79 I5=欗$, *&!I* Ӑ$ 쑬[BLՅAb+Nb; T5/y9Qn͆e5; iགྷ4K } En>DXrlTLrfv/V%kVWx jolwao^n9ЖPW DJhhiB?-Dў@ s@#c#Kt  ,Q,˵eQBxzUV&t:%3= ڮrQ`ѕE>m ȘtkH?Q`p HiQ0F > ɽ٤fr)#?f8IЋՃ0^Sz:;\2͍&Y&0t!!, %[d/+}x'׍  <.İ0N"D| ǃ> 07y9M1 'I9L=ڙAWN{s(~W4Bn:yaLtc뉴N):FcJ8XZbFXLb! Εم;b >'ow, D +AdkJ]UfVZaV񔥽(zsbnZ-9>Vg:z0J0@JHCko=;Os7a+ݠڑVd5hAKV5Brq->?BN} ;GtuaF/HQnIc$ ] ifmݔZ&)W~^˴[U(ӑ"ZoV)OIZrn4~<;]P9 |.za<97h]+Ǻ'G~U3K3>k[ˎ[Z%_(Mpc\͏$;=oS-SoYAIY{ffnQM _:XDBеaW_q:JJJz)**B$X5S)ةd%XcU3v6Llv^\&HI3hV3/xJLܒNܒNZmč9u;/:FG3 $]設E_Q_xi }w8_/M'MHoohh@?P MMMևfh݄jZ[[1Ќ~(30 /^`(ZD/|C к 9srȆ{=+!bx} V~IENDB`dianara-v1.4.1/images/button-download.png0000644000175000017500000000461511311242647016506 0ustar janjanPNG  IHDR szzsRGBbKGD pHYs B(xtIME )! IDATxڽW pT}޽L6!!O0!`Hӂ/u:δ۱3vvhh-3 j"UV@$’B&n{UgBs瞳.--D snyl^m]uu/ڒV#ôFƦJgO~phh둃|/y\C MmʫZT͋pl`M03oڈf, iA=+Xxw_&>\ |wǢſZu=kִv.nB = %F^pAG-R dEbnKVo ?oXNEEOwm˟<˰Qp&QA&hy}cw/?=о!p=K~<̱9̂ cZ=a^k9I4q pe0& FI ׆H\'Wr ۆKWZ[vuղKΘXue;*7-.;0 k{RYciA9HY t|:'#+V|cYpȃa!Ä .l>DY1A<A@IЃXX^al~[KM!9\/Y%ES.CKk%&yXA0EFI#p!щ8L=F8G:uQH$[n2q 6w^2yF Hp؆, (-aZ>JidB iڐKS|0E V^ 5 7pV(^k @iL 1NH0M{"CyK Y@#S$dLnZBVT6LM@;UMOX,a)Ul2%npJz82F `5hTTFIOFͷz`MV^w#i|*,I)a\d*E8 =84TGt(`Kᖅ_h56c,}}}ٍћg],`X]e>IOd$#) |/S3!R Pqi! KnZۆ@Ne 7rYX'$]7\0ȩ$NeO0E=E*1C&OdSui#OQp3{!U9;–06l)}Q$Nc}, (*43QbSb&`=NKLYP+,,gsl(c- %d?"q\^n:[T;5 %J,ò gHEgH"$9<4z341g ['iˉLKm C  |-yOfX:ǏCILNz7,F!o@Wvesj A&U$ȌA/.rK4|dT6@ulXSh #PLiRH̼Mx57ܲʎEGy[\4:=%9ġ.`y!*`7hdƂ(yk+ҹA77?n&G";.3h˖8/ RY| ί_ZE g𥁩,[;eY 7K!:~ C|J1@5Ά!JOQIENDB`dianara-v1.4.1/images/button-parent.png0000644000175000017500000000064211472551664016176 0ustar janjanPNG  IHDR(-SsBITO pHYs:tEXtSoftwarewww.inkscape.org<~PLTEU]Rϫ\QId@ˢ8{0tǘZcԌRЁI8zAIIRR[]iuׁ tRNS  "()+;^gt@jIDAT}( *%8Ǵ$M|K뮫{70N Notifications, enabled by default) - Optional activity icons in the minor feeds. (Configuration > Timelines, disabled by default) - Several improvements in the account dialog. - Building with Qt 4 is no longer supported. - Removed libmagic dependency, since Qt 5 has methods for mimetypes. - Removed QJSON dependency, since Qt 5 has methods to handle JSON. - Fixed #41, accepting username@localhost as valid Webfinger ID, for testers. - Handle new HTML-based error messages sent by server since Pump.io 4.0. - Several cosmetic fixes. - Other minor fixes. v1.3.7 (March 26, 2017) - Image viewer now supports dragging the image around with the mouse, zooming with the wheel, and rotating animated images correctly. - Fixed case-insensitive sorting of contacts in auto-completion lists. - Server version will be shown in the log. v1.3.6 (December 17, 2016) - HTTP redirections will be followed when loading images (Qt 5 only). - Duration of popup notifications is now configurable, and notifications can be set to be persistent. - Some input fields will show a button to clear them (Qt 5 only). - Added Galician translation by EVAnaRkISTO. - Other small fixes. v1.3.5 (October 9, 2016) - Items highlighted due to filtering rules will show the reasons for highlighting. - After downloading an attachment, a button will appear, to open the file using the default program from the user's desktop environment. - Added buttons to rotate images in the image viewer (Ctrl+Left/Right keys). - Building with Qt 5 is officially supported now. *** Note that your system will need a Qt 5 build of the QOAuth library. *** - Some windows that used to block input to the rest of the program, such as posts opened from the Meanwhile feed, will be independent now. - The color setting for highlighted items is now enabled by default. - Several improvements in page selector. - Fix corner case where timeline might update while a comment is being composed, destroying it (#35). - Other minor fixes. v1.3.4 (June 11, 2016) - Automatic timeline updates will avoid interrupting the user. - Different snippet limits for regular and highlighted activities. - Snippets set to 'Always' by default. - More detailed new post notifications. - Clicking "Comment" in posts will show an option to check for comments, if the post doesn't have (or show) any comments yet. - Filtering (searching) contact lists will show the number of matches. - Items in the list of configured filtering rules can be sorted manually, via drag-and-drop. - Fixed a possible crash when pasting a URL which points to an image. - Updated a few links to Pump.io sites. - Minor visual fixes and other improvements. v1.3.3 (March 21, 2016) - When trying to follow a contact, the user ID is verified. - The image viewer gained basic zoom capabilities. - Timelines show thumbnails of embedded images, full size in the viewer. - When pasting text in a post or comment, proper links will be made from URLs, even when the pasted text is rich format. - Links created from the Format menu are verified to have a proper scheme. - Post publisher area can grow bigger. - Configurable avatar sizes in comments and minor feeds. - Better error handling when timelines fail to load. - Added AppData (AppStream) file. - Fixed some size issues in comments. - Other small fixes and improvements. v1.3.2 (October 31, 2015) - Added a D-Bus interface, to control the program from other programs, such as scripts, using tools like qdbus or dbus-send. - Option to list the newest users from your own Pump server, located under Neighbors, inside the Contacts tab. - Welcome wizard, to guide new users. - New privacy option: private likes. Liking posts or comments will only inform the author. - Support for non-https servers, using the --nohttps command line parameter. - Hebrew translation, by GreenLunar. - Improved localization support, and fixed some issues with RTL languages. - JSON-based error messages from the server, which might have Unicode symbols, are now shown correctly. - Several minor fixes. v1.3.1 (August 2, 2015) - Option to browse posts from a user (only for users on the same server, for now). - Option to set or change e-mail address for the account. - Nick autocompleter now displays user ID, too. - Proper links are made from URL's found anywhere, when pasting plain text. - Added a Privacy category in the settings, with a couple of new options related to following people and managing lists. - Button to configure account on status bar when account is not configured. - Option to scroll main timeline to the new stuff line on update. - Enhanced timeline page selector. - Progress bar during program startup. - Option to start application hidden in the system tray. - Command line option to change the color of links: --linkcolor=color (useful in GTK environments). - Other minor fixes. v1.3.0 (May 1, 2015) - Enhanced timeline updates and pagination. Timeline updates are much faster now, require less network traffic, and don't mark previous posts as read every time a feed is updated. - Nickname autocompletion in post/comment composers. Type '@' to get a list of names, then type the first characters. When creating a note, this will add that contact to the "To" list. In comments it will just be a simple link. - Posts opened from the "+" button in the Meanwhile feed will be able to load comments correctly much more often. - Added a button to cancel an attachment in the publisher. - The Favorites timeline will be scheduled for update when liking or unliking things, so liking several posts in a few minutes will only reload it once. - Option to hide duplicated posts, that is, posts which were already visible in the timeline and have been received again due to sharing. - Added setting to automatically set an initial post title from the file name of an attachment. - Option to choose the size of avatars for posts. - Comment area in posts will use the space more efficiently. - Option to make shared posts more obvious, with sharer's avatar. - Option to highlight comments made by the author of a post, and your own comments, with a subtle hint. - Optional character counter in the publisher. - Other new options in the settings dialog. Some were rearranged. - The Account dialog will be locked when Dianara is already authorized to use your account. - Some highlighting colors are now enabled by default. - Time of last timeline update will be shown on the menu bar. - Attached images which fail to load (usually due to permissions) will show a clear message. - A demo notification will be shown when setting the notification style. - Command line option to ignore SSL errors. Use with care! - Command line options now have short form alternatives like -c or -d. - Fixed handling system shutdown; properly close at environment's request. - Fixed loading of remote images when the URL has parameters, and when the URL doesn't have a schema. - Fixed issues with initial width of comments. - Fixed handling cases when the tray icon is not available. - Fixed Ctrl+Shift+V, to paste without format, in comments. - Fixed flickering effect on some timestamps. - Other small fixes. v1.2.5 (December 16, 2014) - Mentions and Actions feeds. Access them via keyboard with Control+1/2/3. (Keyboard shortcut to reload Meanwhile feed has been changed to F2) - Made some of the labels expandable; they will show extra information when clicked. - Offer to insert links to image files as embedded images, when pasting. - Very long post titles will be cut when posting. - Shared posts now show sharer's information in a wide line at the top. - Show warning when posting only to Followers, but having none. - Added new configuration categories, and rearranged some options. - Added option to show post client information directly. - Option to insert basic tables in messages. - Avatar menus now sync their Follow/Unfollow option based on global contact list changes. - Added more fallback icons. - Added Control+Enter shortcut for the "Done" button when selecting specific recipients for a message. - Enhancements in the experimental group support. - Added a few changes to support GNU Mediagoblin's upcoming Pump.io API. You'll be able to use Dianara to post to Mediagoblin sites once its 0.8.0 version is out. - Added links to the Pump.io User Guide. (https://github.com/e14n/pump.io/wiki/User-Guide) - Fixed #4: some memory leaks, thanks to a patch by Gregor Herrmann. - Fixed wrong order in list of likes in posts. - Fixed extra spaces inserted when creating links in the middle of some existing text. - Other small fixes. v1.2.4 (October 20, 2014) - Optional snippets in the Meanwhile feed, with configurable character limit. - Clicking the button that shows the page number at the bottom of a timeline (or pressing Control+G) will open a window to jump to any page. - Menu option to enable/disable timeline auto-updating. - New status bar icon indicates initialization stage and state of auto-updates. Clicking it will also toggle the state of auto-updates. - Tray icon shows how many of the new messages are also highlighted. - Names of recipients are shown as links in the Publisher, so hovering over them will show their addresses in the status bar. - More logging, specifically during initial client registration and the authorization token process. - Settings dialog switched from tabs to stacked view, to have more categories in the future. - Added a TRANSLATING file, with instructions for new translators. - Fixed quoting texts containing "<" and ">". - Fixed long shutdown time. - Lots of other small visual changes and fixes. v1.2.3 (September 6, 2014) - Configurable fonts. - Ability to open the parent post for posts in the timelines, if they were replies to something, such as a shared comment appearing in the timeline. - Option to send a message to a contact directly from the avatar menu and from the contact list. - Show your user ID in popup notifications. Useful if you run more than one instance of Dianara for different accounts. - Show total number of items in timelines, in the tooltips of their tabs, and the total number of pages with the current page number, at the bottom. - Show how many Meanwhile items are highlighted, among those that are new. - Some settings in Configure dialog have been rearranged. - Filter comparison for "Activity description" removes links from it, allowing for simpler rules. - More details about post location, in the tooltip of the location name. - Fixed Meanwhile feed growing very wide sometimes. - Fixed Edit button not working on posts opened in separate window. - Other bug fixes and minor changes. v1.2.2 (July 31, 2014) - Proxy configuration support. Password is not stored securely, so you can leave it empty and be prompted for it on startup, if you wish. - Animated images are now animated when viewed in the separate image viewer. - Filters are now case insensitive, ie. "openfarmgame" matches "OpenFarmGame". - Added a delay before reloading all comments after posting a reply. - Enhanced initialization, so Dianara will keep on trying to get all initial data (your profile, etc) if it fails initially. - Basic Help window. - Configurable "unread post" color. - Show To/CC info in the timestamp tooltip of minor feed activities. - The Normalize Text Colors option in posts works in most cases now. - Link color is no longer specified, so other contacts will see links in the color configured by them. - New way to show attachments, including attached images. - Better filename suggestions when saving attachments. - Option to create bullet lists in the Format menu. - More symbols in the Format > Symbols menu. - Ask for confirmation when quitting Dianara if a post is being composed. - Pressing Enter in title field jumps to message body. Likewise, pressing the Up Arrow at the start of the message jumps to the title field. - Better notification of authentication-related errors. - Added some more fallback icons, for environments without (good) iconsets. - Updated Italian translation, by Metal Biker. - Partial updates to Polish translation by Derping Muffins and CyberKiller, and German translation by Emvigo. - Other small fixes. v1.2.1 (May 22, 2014) - Upload of other media types (audio, video, misc files). Keep in mind that most people won't be able to see these at the moment! (Related Pump.io issue: https://github.com/e14n/pump.io/issues/1014) - Nicer download of media attachments. - New --config parameter, to use a different configuration on startup. Using this, you can run 2 or 3 instances of Dianara for different accounts. - System tray icon can be configured to show your avatar, or a custom image. - Added an optional toolbar. - Added some widgets to the status bar. - Avatar button gets highlighted when hovering, to make it more obvious. - New messages are highlighted with a gradient on the right side. - Added partial German translation, by Emvigo. - Some minor fixes. v1.2.0 (April 16, 2014) - Comments can be edited. - Ability to search the contact list by partial name or address. - Filters have been extended to be used in the timelines too, and can also be set to *highlight* posts, in addition to hiding them. - Fixed very wide comments. - Nicer avatars-buttons with options are used everywhere now. - Meanwhile items such as "someone followed someone else", also have an avatar-button for that person, with the usual options. - 'Meanwhile' item highlighting uses different, customizable colors. These are also used in the Timeline posts, where appropriate. - Configurable notifications. - More information in Meanwhile tooltips. - Fuzzy timestamps, like "3 minutes ago" are updated every minute. - Log window. - Post of type 'audio', 'video' and 'file' are shown in a basic manner. Video/Audio upload mostly works but has been disabled for this release. - More keyboard control for timelines (Ctrl+Up/Down/PgUp/PgDown/Home/End). - Quoting partial comments (selected text) now works, though it has issues. - Info about a hovered URL in posts and comments is now shown in the status bar. - New contacts are added to the lists without the need to fully reload them after following or unfollowing someone. - Several enhancements in the Image Viewer. - Added some fallback icons, to be used when the system iconset doesn't have an appropriate icon. - Disable some menus and widgets until Dianara is authorized to use an account. - New libmagic dependency. - Lots of other fixes and improvements. v1.1 (January 11, 2014) - 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 differently now, so a few things will reset on first use. - Lots of other small fixes and enhancements. v1.0 (October 29, 2013) - 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 (August 5, 2013) - 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 (July 8, 2013) - 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 (June 16, 2013) - 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 over 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 (June 6, 2013) - 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 (May 30, 2013) - 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 - First Pump.io release (May 23, 2013) - 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.4.1/README0000644000175000017500000001520413066004427012270 0ustar janjan Dianara - A Pump.io client Copyright 2012-2017 JanKusanagi JRR =============================================================================== 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' and setting a visual theme to your liking. Press F1 (or click on the Help menu) from Dianara's main window to check out the basic help. What it does =============================================================================== - Posting text (with some HTML formatting), to Followers, Public, custom person lists created by you, or specific people, in any combination. - Uploading pictures, audio, video and general files. - Showing different timelines, with configurable number of posts per page, moving forward and backward in pages, or jumping to any page directly. - Liking, commenting, sharing, editing and deleting posts. - Liking, editing and deleting comments. - Contact listing, following and unfollowing, either by entering their webfinger address or through buttons in the lists and in the avatar menus. - Editing your profile, changing your avatar, setting your e-mail address. - Watch the 'minor feeds' of activities, "Meanwhile", "Mentions" and "Actions", and opening related posts and following people from there. - Filtering or highlighting posts, according to a set of configurable rules. - Popup notifications when there are new posts, or new "Meanwhile" activities. - And more! Some usage tips =============================================================================== - Press F1 in the program to read the general help. - You can set "public posting" as the default in the Configuration window. - You can click on the avatar of a post's author to get some options. This also works in the comments, and in the Meanwhile feed. - There are tooltips everywhere, so if you hover over 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 use Control+Up/Down/PgUp/PgDown/Home/End to scroll the timeline. Use Control+Left/Right to go forward and backwards in the timeline pages. Press Control+G to jump to any page. Control+1/2/3 alternate between the minor feeds. - If a post is of "image" type, you can click on the image to see it full size. - You can hide/show the side panel by pressing F9. Many items in the "Meanwhile" column have a "+" button to open the referenced post. - Adding titles to your posts will make the "Meanwhile" feed and the e-mail notifications a lot more useful! You can have automatic titles set from attachments if you enable that option in the program configuration, in the Composer category. - Resizing the window to half the screen width usually looks better, even though you can use it maximized, or in full screen mode, of course. - You can change the main window's icon and the icon used in system notifications by placing a 64x64 PNG image called dianara.png into /usr/share/icons/hicolor/64x64/apps/ - You can temporarily stop (and restart) the auto-updating of timelines by clicking on the "state" icon in the status bar, or by using the Session > Auto-update Timelines option. - If you connect to the Internet through a proxy, you can set it up in the program settings. - Dianara offers a D-Bus interface that allows some control from other applications. The interface is at org.nongnu.dianara, and you can access it with tools such as qdbus or dbus-send. It offers methods like 'toggle' and 'post'. See the BUGS and TODO files for a list of known issues. If you find a bug not listed there, you can report it at the issue tracker: https://gitlab.com/dianara/dianara-dev/issues Command line parameters =============================================================================== - You can use the --config (or -c) command line parameter to specify that a different configuration file be used. For example, if you run "dianara --config myotheraccount", the config file in use will be "Dianara_myotheraccount.conf" instead of the usual "Dianara.conf". This way, you can switch between different accounts easily. You can even run two instances of Dianara at the same time. You can use any name you wish to identify the configuration, but it must be one word, no spaces. - Use the --debug (or -d) command line parameter to have detailed debugging information on what the program is doing. - If your server does not support HTTPS, you can use the --nohttps parameter. - If you need to connect to a server with a invalid SSL configuration, such as a self-signed certificate or an expired certificate, you can use the --ignoresslerrors parameter. This is not recommended. Languages =============================================================================== Dianara is available in English, Catalan, Spanish, Italian, German, Hebrew and Galician. There is a partial polish translation. If you're interested in translating it to your language, check out the TRANSLATING file, and let me know. It's quite easy to do and it would be much appreciated! =============================================================================== Visit https://jancoding.wordpress.com/dianara for more information. Get the latest development source from https://gitlab.com/dianara/dianara-dev dianara-v1.4.1/icon/0000755000175000017500000000000012166364170012342 5ustar janjandianara-v1.4.1/icon/64x64/0000755000175000017500000000000012140335260013123 5ustar janjandianara-v1.4.1/icon/64x64/dianara.png0000664000175000017500000001263612166366451015260 0ustar janjanPNG  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|X7I03 7`5ΜijHG}Zl gZ.Յ&ݝ,j6w©`n ?տ&+Jk `m._LaĺrĿCz}$ ڪy^kZCv Kv|LHFDzO IQ}-K}iS 3^P4 =============================================================================== 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/ =============================================================================== These are some basic instructions on how to build Dianara from source, in case you don't have precompiled packages available for your operating system. See https://jancoding.wordpress.com/dianara for info on package availability. General runtime dependencies (check carefully!) =============================================================================== - Qt 5.6 or later - QOAuth 2.x, built with Qt 5 - OpenSSL plugin for QCA (qca2-plugin-openssl, libqca2-plugin-ossl, or similar) In some cases, the QCA package includes the plugins directly. *** Dianara will _crash_ if you don't have this! Dependencies for building =============================================================================== You'll need qmake and the qt-devel (>= 5.6, including QtNetwork and QtDBUS modules, if they are separate), 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 for the build dependencies in some GNU/Linux distributions: - Mageia (probably in Mandriva and ROSA, too): libqt5base5-devel and libqoauth-qt5-devel. ** Note: In x86_64 arch, "lib" packages start with "lib64", such as lib64qt5base5-devel. > Build tools: gcc-c++, make - Debian (probably in any of its derivatives, too): qt5-default and libqoauth-dev (>=2.0.0, or libqoauth-qt5-dev). > Build tools: build-essential, g++ - Fedora: qt5-qtbase-devel, qoauth-qt5-devel. > Build tools: gcc-c++ - openSUSE: libqt5-qtbase-devel and qoauth-qt5-devel. - Archlinux and Parabola (runtime deps include build-time deps): qt5-base and qoauth. > Build tools: gcc, make - FreeBSD (runtime deps include build-time deps): qt5-widgets and qoauth-qt5. > Build tools: qmake-qt5, qt5-buildtools You might also need to install qt5-qmake, if your distribution does not include it with the Qt development package. Build process =============================================================================== From Dianara's main directory, where Dianara.pro is located, execute: 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 [*]you might need to use the command 'qmake-qt5' instead That should do it! There is an installation target if you wish to use 'make install', but you can just run the resulting "dianara" binary without installation. 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 https://jancoding.wordpress.com/dianara for more information. Get the latest development source from https://gitlab.com/dianara/dianara-dev dianara-v1.4.1/Dianara.pro0000644000175000017500000001456613207626026013505 0ustar janjan## Dianara - A Pump.io client ## Copyright 2012-2017 JanKusanagi JRR ## ## 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 ## ------------------------------------------------- message("Generating Makefile for Dianara... $$escape_expand(\\n)\ Using $$_FILE_$$escape_expand(\\n)") QT *= core gui widgets network message("Building with Qt v$$QT_VERSION") lessThan(QT_MAJOR_VERSION, 5) { warning(" >>> You're trying to build with Qt 4") warning(" >>> This version of Dianara requires Qt 5") warning(" >>> You might need to use qmake-qt5 instead $$escape_expand(\\n)") error("Aborting!") } TARGET = dianara TEMPLATE = app SOURCES += src/main.cpp\ src/mainwindow.cpp \ src/configdialog.cpp \ src/notifications.cpp \ src/post.cpp \ src/timeline.cpp \ src/publisher.cpp \ src/composer.cpp \ src/timestamp.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 \ src/logviewer.cpp \ src/asperson.cpp \ src/avatarbutton.cpp \ src/colorpicker.cpp \ src/filterchecker.cpp \ src/contactmanager.cpp \ src/contactlist.cpp \ src/downloadwidget.cpp \ src/proxydialog.cpp \ src/helpwidget.cpp \ src/fontpicker.cpp \ src/groupsmanager.cpp \ src/globalobject.cpp \ src/pageselector.cpp \ src/hclabel.cpp \ src/userposts.cpp \ src/emailchanger.cpp \ src/siteuserslist.cpp \ src/firstrunwizard.cpp \ src/bannernotification.cpp \ src/filtermatcheswidget.cpp \ src/ivgraphicsview.cpp \ src/draftsmanager.cpp \ src/datafile.cpp HEADERS += src/mainwindow.h \ src/configdialog.h \ src/notifications.h \ src/post.h \ src/timeline.h \ src/publisher.h \ src/composer.h \ src/timestamp.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 \ src/logviewer.h \ src/asperson.h \ src/avatarbutton.h \ src/colorpicker.h \ src/filterchecker.h \ src/contactmanager.h \ src/contactlist.h \ src/downloadwidget.h \ src/proxydialog.h \ src/helpwidget.h \ src/fontpicker.h \ src/groupsmanager.h \ src/globalobject.h \ src/pageselector.h \ src/hclabel.h \ src/userposts.h \ src/emailchanger.h \ src/siteuserslist.h \ src/firstrunwizard.h \ src/bannernotification.h \ src/filtermatcheswidget.h \ src/ivgraphicsview.h \ src/draftsmanager.h \ src/datafile.h # If D-Bus available, include D-Bus interface and enable Dbus-based notifications qtHaveModule(dbus) { message("Building with D-Bus support (QtDBus module OK!) $$escape_expand(\\n)") QT += dbus SOURCES += src/dbusinterface.cpp HEADERS += src/dbusinterface.h } else { warning(">>> QtDBus module not available! $$escape_expand(\\n)") } # SOURCE_DATE_EPOCH is read from environment, to enable reproducible builds in Debian source_date_epoch = $$(SOURCE_DATE_EPOCH) !isEmpty(source_date_epoch) { message("Creating a reproducible build (avoiding hardcoded timestamps) \ because SOURCE_DATE_EPOCH is defined: $$(SOURCE_DATE_EPOCH)") DEFINES += REPRODUCIBLEBUILD } OTHER_FILES += \ CHANGELOG \ README \ dianara.desktop \ INSTALL \ TODO \ BUGS \ TRANSLATING \ manual/dianara.1 \ translations/translation-status 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 \ translations/dianara_he.ts \ translations/dianara_EMPTY.ts RESOURCES += dianara.qrc load(oauth) { message("QtOAuth module found OK") CONFIG += oauth } !load(oauth) { warning("QtOAuth module NOT found!") warning("Compilation will fail.") warning("If you have QtOAuth installed, your installation might be missing \ a .prf feature file.") warning("See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=825976") } win32 { # Console support under mswin message("Enabling MSwin console support") CONFIG += console } ## This is here so the makefile has a 'make install' target target.path = /usr/bin/ desktop_file.files = dianara.desktop desktop_file.path = /usr/share/applications/ man_file.files = manual/dianara.1 man_file.path = /usr/share/man/man1/ appdata_file.files = appdata/dianara.appdata.xml appdata_file.path = /usr/share/metainfo/ icon32_png.files = icon/32x32/dianara.png icon32_png.path = /usr/share/icons/hicolor/32x32/apps/ icon64_png.files = icon/64x64/dianara.png icon64_png.path = /usr/share/icons/hicolor/64x64/apps/ INSTALLS += target \ desktop_file \ man_file \ appdata_file \ icon32_png \ icon64_png message("$$escape_expand(\\n\\n\\n)\ Makefile done!$$escape_expand(\\n\\n)\ If you're building the binary, you can run 'make' now. $$escape_expand(\\n)") dianara-v1.4.1/LICENSE0000644000175000017500000004325411701354763012430 0ustar janjan 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.4.1/TODO0000644000175000017500000000445413216116026012101 0ustar janjan General to-do list and ideas for Dianara, in no particular order: ================================================================= /// v1.4.2? - Use QDialogButtonBox in dialogs. - Translatable Appdata file. - Option to open attachments without saving them. - Granular options for notifications: only DM, only highlighted by filters, etc. - Fix crash when moving contacts around in the Audience lists of a post. (Qt bug https://bugreports.qt.io/browse/QTBUG-63546) - Use Qt's own plural handling. - Fix quoting partial comment. - Fix links losing bold/italic attributes due to style tag removal. - Ability to 'unshare' a post. This does not currently work as one would expect. (Pump.io issue) - Group support. ***EXPERIMENTAL WIP****************************************** *****Run qmake with DEFINES+=GROUPSUPPORT to test********** *****but make sure you run 'make clean' first************** *****This is intended for tests only*********************** - Keep checking compatibility with GNU MediaGoblin. /////////// - Migrate Composer to KTextWidgets (KF5, Tier 3!). - Add several contacts to a list at the same time. This should always use silent mode. - Ability to rename/update a person list. - Share to specific audience. - In-page string search. - Support for animated avatars, as an option. - Option to add a link, when inserting a remote image, in the image itself. - Show if a contact is following you, in the avatar menu or the avatar itself. - Option to open links to youtube.com/youtu.be in external program (i.e. Minitube) (maybe even any custom URL via regexp) - URL's favorited via hip2.it go to the Favorites timeline, producing "weird" objects. Add checks for that. - Add custom templates below Symbols menu. - "Clone this post" capability. - Better keyboard support.**** WIP - pumplive.com stats "support". - Handle server error 500 when following someone you already followed. - Accept more parameters from CLI and more actions via D-Bus interface. - Backup of "Activity" timeline to a directory with date-objectId-named files and possible attachments. - Ofirehose support? - Spellcheck. (Qt5? For free with KTextWidgets...) - Contacts import from file? - Split PumpController into a library. (now an ActivityPub library) - Use Websockets. dianara-v1.4.1/src/0000755000175000017500000000000013221567115012176 5ustar janjandianara-v1.4.1/src/fontpicker.h0000664000175000017500000000302613202667133014516 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 FONTPICKER_H #define FONTPICKER_H #include #include #include #include #include #include #include class FontPicker : public QWidget { Q_OBJECT public: explicit FontPicker(QString description, QString initialFontString, QWidget *parent = 0); ~FontPicker(); void updateFontSample(); QString getFontInfo(); signals: public slots: void selectFont(); private: QHBoxLayout *m_layout; QLabel *m_descriptionLabel; QLineEdit *m_sampleLineEdit; QPushButton *m_button; QFont m_currentFont; }; #endif // FONTPICKER_H dianara-v1.4.1/src/mischelpers.h0000664000175000017500000000562013131756166014700 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include #include #include #include #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 getSuggestedFilename(QString authorId, QString postType, QString postTitle, QString fileUrl, QString mimeType=""); static QString getFileMimeType(QString fileUri); static int getImageWidth(QString fileURI); static bool isImageAnimated(QString fileUri); static QStringList iconsForActivity(QString verb); static const QStringList imageExtensions(); static const QStringList audioExtensions(); static const QStringList videoExtensions(); static const QString fileFilterString(QStringList extensions); static QString fixLongName(QString name); static QString fileSizeString(QString fileURI); static QString resolutionString(int width, int height); static QStringList htmlWithReplacedImages(QString originalHtml, int postWidth); static QString cleanupHtml(QString originalHtml); static QString htmlWithoutLinks(QString originalHtml); static QString htmlToPlainText(QString html, int charLimit=0); static QString quotedText(QString author, QString content); static QString elidedText(QString text, int charLimit); static QString mediaHtmlBase(QString postType, QString attachmentFilename, QString tooltipMessage, QString belowMessage, int imageWidth = -1); static bool openUrl(QUrl url, QWidget *parentWidget); }; #endif // MISCHELPERS_H dianara-v1.4.1/src/userposts.h0000644000175000017500000000445013207022163014412 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 USERPOSTS_H #define USERPOSTS_H #include #include #include #include #include #include #include #include #include "timeline.h" #include "filterchecker.h" #include "pumpcontroller.h" #include "globalobject.h" class UserPosts : public QWidget { Q_OBJECT public: UserPosts(QString userId, QString userName, QIcon userAvatar, QString userOutbox, PumpController *pumpController, GlobalObject *globalObject, FilterChecker *filterChecker, QWidget *parent); ~UserPosts(); signals: public slots: void fillTimeLine(QVariantList postList, QString previousLink, QString nextLink, int totalItems, QString url); void notifyTimelineUpdate(); void onTimelineFailed(); void scrollTimelineTo(QAbstractSlider::SliderAction sliderAction); protected: virtual void resizeEvent(QResizeEvent *event); virtual void closeEvent(QCloseEvent *event); virtual void keyPressEvent(QKeyEvent *event); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_bottomLayout; QScrollArea *m_scrollArea; TimeLine *m_timeline; QLabel *m_infoLabel; QPushButton *m_closeButton; QString m_timelineTitle; QString m_timelineUrl; QString m_userInfoString; PumpController *m_pumpController; GlobalObject *m_globalObject; }; #endif // USERPOSTS_H dianara-v1.4.1/src/colorpicker.cpp0000644000175000017500000000676013202670261015223 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "colorpicker.h" ColorPicker::ColorPicker(QString description, QString initialColorString, QWidget *parent) : QWidget(parent) { m_descriptionLabel = new QLabel(description, this); m_descriptionLabel->setWordWrap(true); m_descriptionLabel->setAlignment(Qt::AlignTop); m_checkBox = new QCheckBox(this); m_buttonPixmap = QPixmap(32, 32); m_button = new QPushButton(tr("Change..."), this); m_button->setIconSize(QSize(32, 32)); m_button->setDisabled(true); // Disabled initially connect(m_button, &QAbstractButton::clicked, this, &ColorPicker::changeColor); connect(m_checkBox, &QAbstractButton::toggled, m_button, &QWidget::setEnabled); m_layout = new QHBoxLayout(); m_layout->addWidget(m_descriptionLabel, 10); m_layout->addSpacing(4); m_layout->addStretch(1); m_layout->addWidget(m_checkBox, 0); m_layout->addSpacing(8); m_layout->addWidget(m_button, 0); this->setLayout(m_layout); QColor initialColor(initialColorString); if (initialColor.isValid()) { m_currentColor = initialColor; m_checkBox->setChecked(true); } else { if (initialColorString.startsWith(QStringLiteral("DISABLED"))) { m_currentColor = initialColorString.remove(QStringLiteral("DISABLED")); } else { m_currentColor = QColor(Qt::blue); } } this->setButtonColor(m_currentColor); qDebug() << "ColorPicker created"; } ColorPicker::~ColorPicker() { qDebug() << "ColorPicker destroyed"; } void ColorPicker::setButtonColor(QColor color) { m_buttonPixmap.fill(color); m_button->setIcon(QIcon(m_buttonPixmap)); } QString ColorPicker::getCurrentColor() { if (m_checkBox->isChecked()) { return m_currentColor.name(); // return in #RRGGBB format } else { // Return invalid color, but keeping the numeric code return QString("DISABLED%1").arg(m_currentColor.name()); } } //////////////////////////////////////////////////////////////////////////// ////////////////////////////////// SLOTS /////////////////////////////////// //////////////////////////////////////////////////////////////////////////// void ColorPicker::changeColor() { QColor newColor = QColorDialog::getColor(m_currentColor, this, tr("Choose a color")); if (newColor.isValid()) { m_currentColor = newColor; setButtonColor(m_currentColor); } qDebug() << "New color:" << m_currentColor; } dianara-v1.4.1/src/downloadwidget.h0000664000175000017500000000410313203371622015356 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 DOWNLOADWIDGET_H #define DOWNLOADWIDGET_H #include #include #include #include #include #include #include #include #include "pumpcontroller.h" class DownloadWidget : public QFrame { Q_OBJECT public: explicit DownloadWidget(QString m_fileUrl, QString suggestedFilename, PumpController *pumpController, QWidget *parent = 0); ~DownloadWidget(); void resetWidget(); signals: public slots: void downloadAttachment(); void openAttachment(); void cancelDownload(); void completeDownload(QString url); void onDownloadFailed(QString url); void storeFileData(); void updateProgressBar(qint64 received, qint64 total); private: QHBoxLayout *m_layout; QLabel *m_infoLabel; QPushButton *m_openButton; QProgressBar *m_progressBar; QPushButton *m_downloadButton; QPushButton *m_cancelButton; QString m_fileUrl; QString m_suggestedFilename; PumpController *m_pumpController; QNetworkReply *m_networkReply; QFile m_downloadedFile; bool m_downloading; }; #endif // DOWNLOADWIDGET_H dianara-v1.4.1/src/profileeditor.h0000664000175000017500000000534613207045626015232 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include "pumpcontroller.h" #include "mischelpers.h" #include "emailchanger.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); void setAvatar(QString filename); void toggleWidgetsEnabled(bool state); signals: public slots: void redrawAvatar(QString avatarUrl, QString avatarFilename); void findAvatarFile(); void saveProfile(); void sendProfileData(QString newImageUrl = QString()); void enableSaveButton(); protected: virtual void closeEvent(QCloseEvent *event); private: QVBoxLayout *m_mainLayout; QFormLayout *m_topLayout; QHBoxLayout *m_emailLayout; QHBoxLayout *m_avatarLayout; QHBoxLayout *m_bottomLayout; QLabel *m_webfingerLabel; QLabel *m_emailLabel; QPushButton *m_changeEmailButton; EmailChanger *m_emailChanger; QLabel *m_avatarLabel; QPushButton *m_changeAvatarButton; bool m_avatarHasChanged; QLineEdit *m_fullNameLineEdit; QLineEdit *m_hometownLineEdit; QTextEdit *m_bioTextEdit; QLabel *m_newAvatarInfoLabel; QPushButton *m_saveButton; QPushButton *m_cancelButton; QAction *m_cancelAction; QString m_currentAvatarUrl; QString m_oldAvatarFilename; QString m_newAvatarFilename; QString m_newAvatarContentType; PumpController *m_pumpController; }; #endif // PROFILEEDITOR_H dianara-v1.4.1/src/post.h0000644000175000017500000001566713211035600013337 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include "pumpcontroller.h" #include "globalobject.h" #include "timestamp.h" #include "mischelpers.h" #include "commenterblock.h" #include "imageviewer.h" #include "asactivity.h" #include "avatarbutton.h" #include "downloadwidget.h" #include "hclabel.h" #include "filtermatcheswidget.h" class Post : public QFrame { Q_OBJECT public: Post(ASActivity *activity, bool highlightedByFilter, bool isStandalone, PumpController *pumpController, GlobalObject *globalObject, QWidget *parent); ~Post(); void updateDataFromActivity(ASActivity *activity); void updateDataFromObject(ASObject *object); void updateCommentFromObject(ASObject *object); void setCommentDeletedFromObject(ASObject *object); void setPostContents(); void onResizeOrShow(); void setPostHeight(); void getPendingImages(); QString likesUrl(); void setLikes(QVariantList likesList, int likesCount=-1); void appendLike(QString actorId, QString actorName, QString actorUrl); void removeLike(QString actorId); void refreshLikesInfo(int likesListSize, int likesCount); void setLikesLabel(int likesCount); QString commentsUrl(); void setComments(QVariantList commentsList); void appendComment(ASObject *comment); void setCommentsLabel(int commentsCount); void updateBestCommentsUrl(); QString sharesUrl(); void setShares(QVariantList sharesList, int sharesCount=-1); void setSharesLabel(int resharesCount); void setPostUnreadStatus(); void setPostAsNew(); void setPostAsRead(bool informTimeline=true); void setPostDeleted(QString postDeletedTime); int getHighlightType(); QString getActivityId(); QString getObjectId(); void setFuzzyTimestamps(); void syncAvatarFollowState(); bool isBeingCommented(); bool isNew(); enum PostHightlightType { NoHighlight = -1, MessageForUserHighlight, OwnMessageHighlight, FilterRulesHighlight }; signals: void postRead(bool wasHighlighted); void commentingOnPost(QWidget *commenterWidget); public slots: void likePost(bool like); void fixLikeButton(bool isLiked); void getAllLikes(); void commentOnPost(); void sendComment(QString commentText); void updateComment(QString commentId, QString commentText); void requestCommenterComments(); void getAllComments(); void setAllComments(QVariantList commentsList, QString originatingPostUrl); bool canGetAllComments(); void sharePost(); void unsharePost(); void editPost(); void deletePost(); void joinGroup(); void openClickedUrl(QUrl url); void showHighlightedUrl(QString url); void openPostInBrowser(); void copyPostUrlToClipboard(); void openParentPost(); void normalizeTextFormat(); void triggerResize(); void delayedResize(); void redrawImages(QString imageUrl); void onImageFailed(QString imageUrl); protected: virtual void resizeEvent(QResizeEvent *event); virtual void mousePressEvent(QMouseEvent *event); virtual void keyPressEvent(QKeyEvent *event); virtual void leaveEvent(QEvent *event); virtual void closeEvent(QCloseEvent *event); virtual void showEvent(QShowEvent *event); private: QHBoxLayout *mainLayout; QVBoxLayout *leftColumnLayout; QVBoxLayout *rightColumnLayout; QVBoxLayout *outerLayout; QFrame *leftColumnFrame; QFrame *rightColumnFrame; QString activityId; QString postId; QString postType; QString postUrl; QString postAuthorId; QString postAuthorName; QString postSharedById; QString postShareInfoString; QString postSharedToCCString; QString postShareTime; bool postIsOwn; bool postIsUnread; bool postIsDeleted; int highlightType; QString unreadPostColor; AvatarButton *postAuthorAvatarButton; QAction *openPostInBrowserAction; QAction *copyPostUrlAction; QAction *normalizeTextAction; QAction *closeAction; QLabel *postAuthorNameLabel; HClabel *postCreatedAtLabel; QLabel *postGeneratorLabel; HClabel *postLocationLabel; QLabel *postToLabel; QLabel *postCCLabel; QLabel *postIsSharedLabel; QLabel *shareHintLabel; HClabel *postLikesCountLabel; QLabel *postCommentsCountLabel; HClabel *postSharesCountLabel; QPushButton *openParentPostButton; //QPushButton *closeButton; QLabel *postTitleLabel; QLabel *postSummaryLabel; QTextBrowser *postText; QHBoxLayout *buttonsLayout; QPushButton *likeButton; QPushButton *commentButton; QPushButton *shareButton; QPushButton *editButton; QPushButton *deleteButton; QPushButton *joinLeaveButton; QLabel *groupInfoLabel; DownloadWidget *downloadWidget; FilterMatchesWidget *filterMatchesWidget; QString postTitle; QString postSmallImageUrl; QString postFullImageUrl; QString postImageUrl; QSize postImageSize; bool postImageIsAnimated; bool postImageFailed; QString postAudioUrl; QString postVideoUrl; QString postFileUrl; QString postFileMimeType; QString postAttachmentPureUrl; QString postOriginalText; QVariantMap postParentMap; QString postCreatedAtString; QString postUpdatedAtString; QString postGeneratorString; int postWidth; QString postLikesUrl; int postLikesCount; QVariantMap postLikesMap; QString postCommentsUrl; QString postSharesUrl; QStringList pendingImagesList; bool standalone; QString seeFullImageString; QString downloadAttachmentString; QTimer *resizeTimer; PumpController *pController; GlobalObject *globalObj; CommenterBlock *commenter; }; #endif // POST_H dianara-v1.4.1/src/globalobject.cpp0000644000175000017500000004422413200132257015327 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "globalobject.h" GlobalObject::GlobalObject(QObject *parent) : QObject(parent) { QSettings settings; settings.beginGroup("Configuration"); /////////////////////////////////////////////////////////////////// GENERAL ///////////////////////////////////////////////////////////////////// FONTS QFont defaultTitleFont; // 1 point larger defaultTitleFont.setPointSize(defaultTitleFont.pointSize() + 1); defaultTitleFont.setBold(true); QFont defaultContentFont; // Just the default text size QFont defaultCommentsFont; // 1 point smaller defaultCommentsFont.setPointSize(defaultCommentsFont.pointSize() - 1); QFont defaultMinorFeedFont; // 2 points smaller defaultMinorFeedFont.setPointSize(defaultMinorFeedFont.pointSize() - 2); this->syncFontSettings(settings.value("font1", defaultTitleFont).toString(), settings.value("font2", defaultContentFont).toString(), settings.value("font3", defaultCommentsFont).toString(), settings.value("font4", defaultMinorFeedFont).toString()); //////////////////////////////////////////////////////////////////// COLORS this->colorsList.clear(); // Defaults: colorsList << settings.value("color1", "#CC2030").toString() // Red << settings.value("color2", "#5599CC").toString() // Blue << settings.value("color3", "#DDCC10").toString() // Yellow << settings.value("color4", "#10BB10").toString() // Green << settings.value("color5", "#AA77EE").toString() // Purple << settings.value("color6").toString(); // No need to call this->syncColorSettings() here... ///////////////////////////////////////////////////////////////// TIMELINES int pppMain = qBound(5, settings.value("postsPerPageMain", 20).toInt(), 50); int pppOther = qBound(1, settings.value("postsPerPageOther", 5).toInt(), 30); this->syncTimelinesSettings(pppMain, pppOther, settings.value("showDeletedPosts", false).toBool(), settings.value("hideDuplicatedPosts", false).toBool(), settings.value("jumpToNewOnUpdate", false).toBool(), settings.value("mfSnippetsType", 2).toInt(), // Default: Always settings.value("snippetCharLimit", 30).toInt(), settings.value("snippetCharLimitHl", 200).toInt(), settings.value("minorFeedAvatarIndex", 1).toInt(), // Default: 2nd (32x32) settings.value("minorFeedIconType", 0).toInt()); // Default: No ///////////////////////////////////////////////////////////////////// POSTS this->syncPostSettings(settings.value("postAvatarIndex", 3).toInt(), // 4th (64x64) by default settings.value("commentAvatarIndex", 1).toInt(), // 2nd (32x32) by default settings.value("postExtendedShares", true).toBool(), settings.value("postShowExtraInfo", false).toBool(), settings.value("postHLAuthorComments", true).toBool(), settings.value("postHLOwnComments", true).toBool(), settings.value("postIgnoreSslInImages", false).toBool(), settings.value("postFullImages", false).toBool()); ////////////////////////////////////////////////////////////////// COMPOSER this->syncComposerSettings(settings.value("publicPosts", false).toBool(), settings.value("useFilenameAsTitle", false).toBool(), settings.value("showCharacterCounter", false).toBool()); /////////////////////////////////////////////////////////////////// PRIVACY this->syncPrivacySettings(settings.value("silentFollows", false).toBool(), settings.value("silentLists", true).toBool(), settings.value("silentLikes", false).toBool()); ///////////////////////////////////////////////////////////// NOTIFICATIONS this->syncNotificationSettings(settings.value("notificationTaskbar", true).toBool()); // TODO: load the other nofification options ////////////////////////////////////////////////////////////////////// TRAY this->syncTrayOptions(settings.value("systrayHideInTray", false).toBool()); // TODO: load the other tray options settings.endGroup(); // Model for nick completion used by Composer this->nickCompletionModel = new QStandardItemModel(this); nickCompletionModel->setColumnCount(2); this->filterCompletionModel = new QSortFilterProxyModel(this); filterCompletionModel->setSortCaseSensitivity(Qt::CaseInsensitive); filterCompletionModel->setSourceModel(nickCompletionModel); // Timeline height used to calculate maximum optimal height for post contents this->timelineHeight = 400; // Some initial value this->programClosing = false; qDebug() << "GlobalObject created"; } GlobalObject::~GlobalObject() { qDebug() << "GlobalObject destroyed"; } // General options void GlobalObject::syncGeneralSettings() { // TODO } // Font options void GlobalObject::syncFontSettings(QString postTitleFont, QString postContentsFont, QString commentsFont, QString minorFeedFont) { this->postTitleFontInfo = postTitleFont; this->postContentsFontInfo = postContentsFont; this->commentsFontInfo = commentsFont; this->minorFeedFontInfo = minorFeedFont; qDebug() << "GlobalObject::syncFontSettings() - font info sync'd"; } QString GlobalObject::getPostTitleFont() { return this->postTitleFontInfo; } QString GlobalObject::getPostContentsFont() { return this->postContentsFontInfo; } QString GlobalObject::getCommentsFont() { return this->commentsFontInfo; } QString GlobalObject::getMinorFeedFont() { return this->minorFeedFontInfo; } // Color options void GlobalObject::syncColorSettings(QStringList newColorList) { this->colorsList = newColorList; qDebug() << "GlobalObject::syncColorSettings() - color list sync'd"; } QStringList GlobalObject::getColorsList() { return this->colorsList; } QString GlobalObject::getColor(int colorIndex) { QString color; if (colorIndex >= 0 && colorIndex < this->colorsList.length()) { color = this->colorsList.at(colorIndex); if (!QColor::isValidColor(color)) // If invalid, clear it { color.clear(); } } return color; } // Timeline options void GlobalObject::syncTimelinesSettings(int pppMain, int pppOther, bool showDeleted, bool hideDuplicates, bool jumpToNew, int minorFeedSnippets, int snippetsChars, int snippetsCharsHl, int mfAvatarIdx, int mfVerbIconType) { this->postsPerPageMain = pppMain; this->postsPerPageOther = pppOther; this->showDeletedPosts = showDeleted; this->hideDuplicatedPosts = hideDuplicates; this->jumpToNewOnUpdate = jumpToNew; this->minorFeedSnippetsType = minorFeedSnippets; this->snippetsCharLimit = snippetsChars; this->snippetsCharLimitHl = snippetsCharsHl; this->mfAvatarIndex = mfAvatarIdx; this->mfAvatarSize = this->getAvatarSizeForIndex(mfAvatarIdx); this->mfIconType = mfVerbIconType; // TODO: more... } int GlobalObject::getPostsPerPageMain() { return this->postsPerPageMain; } int GlobalObject::getPostsPerPageOther() { return this->postsPerPageOther; } bool GlobalObject::getShowDeleted() { return this->showDeletedPosts; } bool GlobalObject::getHideDuplicates() { return this->hideDuplicatedPosts; } bool GlobalObject::getJumpToNewOnUpdate() { return this->jumpToNewOnUpdate; } int GlobalObject::getMinorFeedSnippetsType() { return this->minorFeedSnippetsType; } int GlobalObject::getSnippetsCharLimit() { return this->snippetsCharLimit; } int GlobalObject::getSnippetsCharLimitHl() { return this->snippetsCharLimitHl; } int GlobalObject::getMfAvatarIndex() { return this->mfAvatarIndex; } QSize GlobalObject::getMfAvatarSize() { return this->mfAvatarSize; } int GlobalObject::getMfIconType() { return this->mfIconType; } // Post options void GlobalObject::syncPostSettings(int postAvatarIdx, int commentAvatarIdx, bool extendedShares, bool showExtraInfo, bool hlAuthorComments, bool hlOwnComments, bool ignoreSslInImages, bool fullImages) { this->postAvatarIndex = postAvatarIdx; this->postAvatarSize = this->getAvatarSizeForIndex(postAvatarIdx); this->commentAvatarIndex = commentAvatarIdx; this->commentAvatarSize = this->getAvatarSizeForIndex(commentAvatarIdx); this->postExtendedShares = extendedShares; this->postShowExtraInfo = showExtraInfo; this->postHLAuthorComments = hlAuthorComments; this->postHLOwnComments = hlOwnComments; this->postIgnoreSslInImages = ignoreSslInImages; this->postFullImages = fullImages; } int GlobalObject::getPostAvatarIndex() { return this->postAvatarIndex; } QSize GlobalObject::getPostAvatarSize() { return this->postAvatarSize; } int GlobalObject::getCommentAvatarIndex() { return this->commentAvatarIndex; } QSize GlobalObject::getCommentAvatarSize() { return this->commentAvatarSize; } bool GlobalObject::getPostExtendedShares() { return this->postExtendedShares; } bool GlobalObject::getPostShowExtraInfo() { return this->postShowExtraInfo; } bool GlobalObject::getPostHLAuthorComments() { return this->postHLAuthorComments; } bool GlobalObject::getPostHLOwnComments() { return this->postHLOwnComments; } bool GlobalObject::getPostIgnoreSslInImages() { return this->postIgnoreSslInImages; } bool GlobalObject::getPostFullImages() { return this->postFullImages; } // Composer options void GlobalObject::syncComposerSettings(bool publicPosts, bool filenameAsTitle, bool showCharCounter) { this->publicPostsByDefault = publicPosts; this->useFilenameAsTitle = filenameAsTitle; this->showCharacterCounter = showCharCounter; } bool GlobalObject::getPublicPostsByDefault() { return this->publicPostsByDefault; } bool GlobalObject::getUseFilenameAsTitle() { return this->useFilenameAsTitle; } bool GlobalObject::getShowCharacterCounter() { return this->showCharacterCounter; } // Privacy options void GlobalObject::syncPrivacySettings(bool silentFollows, bool silentLists, bool silentLikes) { this->silentFollowing = silentFollows; this->silentListsHandling = silentLists; this->silentLiking = silentLikes; } bool GlobalObject::getSilentFollows() { return this->silentFollowing; } bool GlobalObject::getSilentLists() { return this->silentListsHandling; } bool GlobalObject::getSilentLikes() { return this->silentLiking; } // Notification options void GlobalObject::syncNotificationSettings(bool notifyTaskbar) { this->notifyInTaskbar = notifyTaskbar; // TODO, most still handled elsewhere } bool GlobalObject::getNotifyInTaskbar() { return this->notifyInTaskbar; } // Tray options void GlobalObject::syncTrayOptions(bool hideInTrayStartup) { this->hideInTray = hideInTrayStartup; // TODO, some still handled elsewhere } bool GlobalObject::getHideInTray() { return this->hideInTray; } /////////////////////////////////////////////////////////////////////////////// void GlobalObject::setDataDirectory(QString dataDir) { this->dataDirectory = dataDir; } QString GlobalObject::getDataDirectory() { return this->dataDirectory; } void GlobalObject::createMessageForContact(QString id, QString name, QString url) { // Send signal to be caught by Publisher() emit messagingModeRequested(id, name, url); qDebug() << "GlobalObject; asking for Messaging mode for " << name << id << url; } void GlobalObject::browseUserMessages(QString userId, QString userName, QIcon userAvatar, QString userOutbox) { // Signal to be caught by MainWindow emit userTimelineRequested(userId, userName, userAvatar, userOutbox); } void GlobalObject::editPost(QString originalPostId, QString type, QString title, QString contents) { // Signal to be caught by Publisher emit postEditRequested(originalPostId, type, title, contents); qDebug() << "GlobalObject; asking to edit post: " << originalPostId << title << type; } QSortFilterProxyModel *GlobalObject::getNickCompletionModel() { return this->filterCompletionModel; } void GlobalObject::addToNickCompletionModel(QString id, QString name, QString url) { QStandardItem *itemName = new QStandardItem(name); itemName->setData(id, Qt::UserRole + 1); itemName->setData(url, Qt::UserRole + 2); QStandardItem *itemId = new QStandardItem(id); QList itemLine; itemLine.append(itemName); itemLine.append(itemId); this->nickCompletionModel->appendRow(itemLine); } void GlobalObject::removeFromNickCompletionModel(QString id) { qDebug() << "GlobalObject::removeFromNickCompletionModel()" << id; QList allNicks; allNicks = nickCompletionModel->findItems(QString(), Qt::MatchContains); foreach (QStandardItem *item, allNicks) { //qDebug() << item->data(Qt::UserRole + 1).toString(); if (item->data(Qt::UserRole + 1).toString() == id) { int rowNum = item->row(); /* * FIXME: Ensure deleting the item is not needed. * Deleting it before removeRow doesn't seem to do any harm. * removeRow seems to delete it anyway; trying to delete it * afterwards results in segfault. * * delete item; */ this->nickCompletionModel->removeRow(rowNum); } } } void GlobalObject::clearNickCompletionModel() { // clear() seems to delete the items properly this->nickCompletionModel->clear(); } void GlobalObject::sortNickCompletionModel() { this->filterCompletionModel->sort(0); } /* * Return a Display Name + URL pair for a given user ID, * to be used when restoring audience from a draft * */ QPair GlobalObject::getDataForNick(QString id) { QList matchingNicks; matchingNicks = nickCompletionModel->findItems(id, Qt::MatchExactly, 1); if (matchingNicks.isEmpty()) { // Return basic data for invalid case return QPair(id, QString()); } QStandardItem *item = nickCompletionModel->item(matchingNicks.first()->row(), 0); // FIXME: add some error control return QPair(item->text(), item->data(Qt::UserRole + 2).toString()); } /* * Change status bar message in main window * */ void GlobalObject::setStatusMessage(QString message) { emit messageForStatusBar(message); } /* * Add a log message to the log viewer * */ void GlobalObject::logMessage(QString message, QString url) { emit messageForLog(message, url); } void GlobalObject::storeTimelineHeight(int height) { // Substract some pixels to account for the row of buttons, etc. this->timelineHeight = qMax(height - 80, 50); // Never less than 50, but window should never be that small } int GlobalObject::getTimelineHeight() { return this->timelineHeight; } /* * Get corresponding QSize for specified index from combo box * */ QSize GlobalObject::getAvatarSizeForIndex(int index) { int pixelSize; switch (index) { case 0: pixelSize = 16; break; case 1: pixelSize = 32; break; case 2: pixelSize = 48; break; // case 3 = default = 64 case 4: pixelSize = 96; break; case 5: pixelSize = 128; break; case 6: pixelSize = 256; break; default: // index = 3 or invalid option pixelSize = 64; } return QSize(pixelSize, pixelSize); } void GlobalObject::notifyProgramShutdown() { this->programClosing = true; emit programShuttingDown(); } bool GlobalObject::isProgramShuttingDown() { return this->programClosing; } dianara-v1.4.1/src/logviewer.cpp0000644000175000017500000001033613202665502014706 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "logviewer.h" LogViewer::LogViewer(QWidget *parent) : QWidget(parent) { this->setWindowTitle(tr("Log") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("text-x-log", QIcon(":/images/log.png"))); this->setWindowFlags(Qt::Window); this->setMinimumSize(320, 400); QSettings settings; this->resize(settings.value("LogViewer/logWindowSize", QSize(760, 560)).toSize()); QList closeShortcuts; closeShortcuts << QKeySequence(Qt::Key_Escape); closeShortcuts << QKeySequence(Qt::Key_F12); m_closeAction = new QAction(this); m_closeAction->setShortcuts(closeShortcuts); connect(m_closeAction, &QAction::triggered, this, &QWidget::hide); this->addAction(m_closeAction); m_logTextBrowser = new QTextBrowser(this); m_logTextBrowser->setReadOnly(true); m_logTextBrowser->setOpenExternalLinks(true); m_clearButton = new QPushButton(QIcon::fromTheme("edit-clear-list", QIcon(":/images/button-delete.png")), tr("Clear &Log"), this); connect(m_clearButton, &QAbstractButton::clicked, m_logTextBrowser, &QTextEdit::clear); m_closeButton = new QPushButton(QIcon::fromTheme("window-close", QIcon(":/images/button-close.png")), tr("&Close"), this); connect(m_closeButton, &QAbstractButton::clicked, this, &QWidget::hide); // Layout m_buttonsLayout = new QHBoxLayout(); m_buttonsLayout->addWidget(m_clearButton); m_buttonsLayout->addStretch(); m_buttonsLayout->addWidget(m_closeButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->addWidget(m_logTextBrowser); m_mainLayout->addLayout(m_buttonsLayout); this->setLayout(m_mainLayout); qDebug() << "LogViewer created"; } LogViewer::~LogViewer() { qDebug() << "LogViewer destroyed"; } void LogViewer::closeEvent(QCloseEvent *event) { this->hide(); event->ignore(); } void LogViewer::hideEvent(QHideEvent *event) { QSettings settings; if (settings.isWritable()) { settings.setValue("LogViewer/logWindowSize", this->size()); } event->accept(); } /* * Scroll log to bottom when showing * */ void LogViewer::showEvent(QShowEvent *event) { this->m_logTextBrowser->moveCursor(QTextCursor::End); event->accept(); } /****************************************************************************/ /********************************* SLOTS ************************************/ /****************************************************************************/ void LogViewer::addToLog(QString message, QString url) { QString logLine = "[" + QDateTime::currentDateTime().toString(Qt::DefaultLocaleShortDate) + "] "; logLine.append(message); if (!url.isEmpty()) { logLine.append(": " + MiscHelpers::elidedText(url, 40) + ""); } m_logTextBrowser->append(logLine); } void LogViewer::toggleVisibility() { if (this->isVisible()) { this->hide(); } else { this->show(); } } dianara-v1.4.1/src/asobject.cpp0000664000175000017500000004314513211036462014500 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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) { m_originalObjectMap = objectMap; /// Meta information m_id = ASPerson::cleanupId(objectMap.value("id").toString()); m_type = objectMap.value("objectType").toString(); m_url = objectMap.value("url").toString(); m_createdAt = objectMap.value("published").toString(); m_updatedAt = objectMap.value("updated").toString(); // FIXME: still missing some fields... QVariantMap locationMap = objectMap.value("location").toMap(); m_locationName = locationMap.value("displayName").toString(); m_locationFormatted = locationMap.value("address").toMap() .value("formatted").toString(); m_locationCountry = locationMap.value("address").toMap() .value("country").toString(); // Author m_author = new ASPerson(objectMap.value("author").toMap(), this); // This will hold the date when the object was deleted, or empty if it wasn't m_deleted = objectMap.value("deleted").toString(); m_liked = objectMap.value("liked").toBool(); // The object to which this one replies, if any (note to which a comment replies, etc) m_inReplyToMap = objectMap.value("inReplyTo").toMap(); /// /// End of "meta"; Start of content /// if (m_type == QStringLiteral("image")) { /* Get the "small" version. * See if there's a proxy URL for the image first * (for private images on remote servers) */ qDebug() << "Trying Proxyed thumbnail image"; m_smallImageUrl = objectMap.value("image").toMap() .value("pump_io").toMap() .value("proxyURL").toString(); // And if that's empty, use regular image->url if (m_smallImageUrl.isEmpty()) { qDebug() << "Trying direct thumbnail image"; m_smallImageUrl = objectMap.value("image").toMap() .value("url").toString(); } // Same for the full size image m_imageUrl = objectMap.value("fullImage").toMap() .value("pump_io").toMap() .value("proxyURL").toString(); qDebug() << "Trying Proxyed fullImage"; // If that's empty, use regular fullImage->url field if (m_imageUrl.isEmpty()) { qDebug() << "Trying direct fullImage"; m_imageUrl = objectMap.value("fullImage").toMap() .value("url").toString(); } m_imageWidth = objectMap.value("fullImage").toMap() .value("width").toInt(); m_imageHeight = objectMap.value("fullImage").toMap() .value("height").toInt(); // If it's STILL empty, use the thumbnail also as full image if (m_imageUrl.isEmpty()) { m_imageUrl = m_smallImageUrl; m_imageWidth = objectMap.value("image").toMap() .value("width").toInt(); m_imageHeight = objectMap.value("image").toMap() .value("height").toInt(); } // And if there's no small image for some reason, use the full one as thumbnail if (m_smallImageUrl.isEmpty()) { m_smallImageUrl = m_imageUrl; } qDebug() << "postImage:" << m_imageUrl << "> Small:" << m_smallImageUrl; // To have a filename with extension, even when downloading from proxyURL's m_attachmentPureUrl = objectMap.value("fullImage").toMap() .value("url").toString(); if (m_attachmentPureUrl.isEmpty()) { m_attachmentPureUrl = objectMap.value("image").toMap() .value("url").toString(); } } // Get audio file URL if (m_type == QStringLiteral("audio")) { // To have a filename with extension, even when downloading from proxyURL's m_attachmentPureUrl = objectMap.value("stream").toMap() .value("url").toString(); m_audioUrl = objectMap.value("stream").toMap() .value("pump_io").toMap() .value("proxyURL").toString(); if (m_audioUrl.isEmpty()) { qDebug() << "No proxyed link to audio, fetching regular link"; m_audioUrl = m_attachmentPureUrl; } } // Get video file URL if (m_type == QStringLiteral("video")) { // To have a filename with extension, even when downloading from proxyURL's m_attachmentPureUrl = objectMap.value("stream").toMap() .value("url").toString(); m_videoUrl = objectMap.value("stream").toMap() .value("pump_io").toMap() .value("proxyURL").toString(); if (m_videoUrl.isEmpty()) { qDebug() << "No proxyed link to video, fetching regular link"; m_videoUrl = m_attachmentPureUrl; } } // Get general file URL if (m_type == QStringLiteral("file")) { // To have a filename with extension, even when downloading from proxyURL's m_attachmentPureUrl = objectMap.value("fileUrl").toString(); // FIXME: there are no proxyURL for misc files ATM; not my fault though =) m_fileUrl = objectMap.value("fileUrl").toMap() .value("pump_io").toMap() .value("proxyURL").toString(); if (m_fileUrl.isEmpty()) { qDebug() << "No proxyed link to general file, fetching regular link"; m_fileUrl = m_attachmentPureUrl; } m_mimeType = objectMap.value("mimeType").toString(); qDebug() << "File mimeType:" << m_mimeType; } // If it's a group, get member count, etc. if (m_type == QStringLiteral("group")) { m_memberCount = objectMap.value("members").toMap() .value("totalItems").toInt(); m_memberUrl = objectMap.value("members").toMap() .value("url").toString(); } // Title can be in non-image posts, too! m_title = objectMap.value("displayName").toString().trimmed(); m_summary = objectMap.value("summary").toString(); m_content = objectMap.value("content").toString(); m_likesCount = objectMap.value("likes").toMap().value("totalItems").toInt(); m_commentsCount = objectMap.value("replies").toMap().value("totalItems").toInt(); m_sharesCount = objectMap.value("shares").toMap().value("totalItems").toInt(); // Get last likes, comments and shares list here, used from Post() m_lastLikesList = objectMap.value("likes").toMap().value("items").toList(); m_lastCommentsList = objectMap.value("replies").toMap().value("items").toList(); m_lastSharesList = objectMap.value("shares").toMap().value("items").toList(); m_proxiedUrls = false; // Get URL for likes; first, proxyURL if it exists m_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 (m_likesUrl.isEmpty()) { m_likesUrl = objectMap.value("likes").toMap().value("url").toString(); } // Get URL for comments; first, proxyURL if it exists m_commentsUrl = objectMap.value("replies").toMap().value("pump_io").toMap() .value("proxyURL").toString(); if (m_commentsUrl.isEmpty()) // If still empty, get regular URL { m_commentsUrl = objectMap.value("replies").toMap().value("url").toString(); } else { m_proxiedUrls = true; } // FIXME: get sharesUrl... qDebug() << "ASObject created" << m_id; } ASObject::~ASObject() { qDebug() << "ASObject destroyed" << m_id; } void ASObject::updateAuthorFromPerson(ASPerson *person) { m_author->updateDataFromPerson(person); } /////// Getters ASPerson *ASObject::author() { return m_author; } QString ASObject::getId() { return m_id; } QString ASObject::getType() { return m_type; } QString ASObject::getTranslatedType(QString typeString) { QString translatedTypeString; if (typeString == QStringLiteral("note")) { translatedTypeString = tr("Note", "Noun, an object type"); } else if (typeString == QStringLiteral("article")) { translatedTypeString = tr("Article", "Noun, an object type"); } else if (typeString == QStringLiteral("image")) { translatedTypeString = tr("Image", "Noun, an object type"); } else if (typeString == QStringLiteral("audio")) { translatedTypeString = tr("Audio", "Noun, an object type"); } else if (typeString == QStringLiteral("video")) { translatedTypeString = tr("Video", "Noun, an object type"); } else if (typeString == QStringLiteral("file")) { translatedTypeString = tr("File", "Noun, an object type"); } else if (typeString == QStringLiteral("comment")) { translatedTypeString = tr("Comment", "Noun, as in object type: a comment"); } else if (typeString == QStringLiteral("group")) { translatedTypeString = tr("Group", "Noun, an object type"); } else if (typeString == QStringLiteral("collection")) { translatedTypeString = tr("Collection", "Noun, an object type"); } else { translatedTypeString = tr("Other", "As in: other type of post") + " (" + typeString + ")"; } return translatedTypeString; } QString ASObject::getUrl() { return m_url; } QString ASObject::getCreatedAt() { return m_createdAt; } QString ASObject::getUpdatedAt() { return m_updatedAt; } QString ASObject::getLocationName() { return m_locationName; } QString ASObject::getLocationFormatted() { return m_locationFormatted; } QString ASObject::getLocationCountry() { return m_locationCountry; } QString ASObject::getLocationTooltip() { QString locationTooltip = m_locationFormatted; if (!m_locationCountry.isEmpty()) { if (!locationTooltip.isEmpty()) { locationTooltip.append("\n"); } locationTooltip.append("(" + m_locationCountry + ")"); } // If still empty, return informational string if (locationTooltip.isEmpty()) { locationTooltip = tr("No detailed location"); } return locationTooltip; } QString ASObject::getDeletedTime() { return m_deleted; } QString ASObject::getDeletedOnString() { QString deletedOnString; if (!m_deleted.isEmpty()) { deletedOnString = this->makeDeletedOnString(m_deleted); } return deletedOnString; } QString ASObject::makeDeletedOnString(QString deletionTime) { return tr("Deleted on %1").arg(Timestamp::localTimeDate(deletionTime)); } bool ASObject::isLiked() { return m_liked; } QString ASObject::getTitle() { return m_title; } QString ASObject::getSummary() { return m_summary; } QString ASObject::getContent() { return m_content; } QString ASObject::getImageUrl() { return m_imageUrl; } QString ASObject::getSmallImageUrl() { return m_smallImageUrl; } int ASObject::getImageWidth() { return m_imageWidth; } int ASObject::getImageHeight() { return m_imageHeight; } QString ASObject::getAudioUrl() { return m_audioUrl; } QString ASObject::getVideoUrl() { return m_videoUrl; } QString ASObject::getFileUrl() { return m_fileUrl; } QString ASObject::getMimeType() { return m_mimeType; } QString ASObject::getAttachmentPureUrl() { return m_attachmentPureUrl; } int ASObject::getMemberCount() { return m_memberCount; } QString ASObject::getMemberUrl() { return m_memberUrl; } int ASObject::getLikesCount() { return m_likesCount; } int ASObject::getCommentsCount() { return m_commentsCount; } int ASObject::getSharesCount() { return m_sharesCount; } bool ASObject::hasProxiedUrls() { return m_proxiedUrls; } QVariantList ASObject::getLastLikesList() { return m_lastLikesList; } QVariantList ASObject::getLastCommentsList() { return m_lastCommentsList; } QVariantList ASObject::getLastSharesList() { return m_lastSharesList; } QString ASObject::getLikesUrl() { return m_likesUrl; } QString ASObject::getCommentsUrl() { return m_commentsUrl; } QString ASObject::getSharesUrl() { return m_sharesUrl; } QVariantMap ASObject::getOriginalObject() { return m_originalObjectMap; } QVariantMap ASObject::getInReplyTo() { return m_inReplyToMap; } QString ASObject::getInReplyToId() { return m_inReplyToMap.value("id").toString(); } /* * This version should be deprecated soon -- WIP * */ QString ASObject::personStringFromList(QVariantList variantList, int count) { QString personString; foreach (QVariant listItem, variantList) { QVariantMap itemMap = listItem.toMap(); QString name = itemMap.value("displayName").toString(); if (name.isEmpty()) { name = ASPerson::cleanupId(itemMap.value("id").toString()); // fallback } QString profileUrl = itemMap.value("url").toString(); personString.prepend("" + name + ", "); // Reverse order } int remainingCount = count - variantList.size(); // Add "and $COUNT other" if (count != -1 && remainingCount > 0) { if (remainingCount == 1) { personString.append(tr("and one other")); } else { personString.append(tr("and %1 others").arg(remainingCount)); } } else { // Remove last comma and space personString.remove(-2, 2); } return personString; } // deprecating... WIP QVariantMap ASObject::simplePersonMapFromList(QVariantList variantList) { QVariantMap map; // Read the list in reverse order: for (int position = variantList.size() - 1; position >= 0; --position) { QVariantMap itemMap = variantList.at(position).toMap(); QString id = ASPerson::cleanupId(itemMap.value("id").toString()); QString name = itemMap.value("displayName").toString(); if (name.isEmpty()) { name = id; // fallback } QString profileUrl = itemMap.value("url").toString(); ASObject::addOnePersonToSimpleMap(id, name, profileUrl, &map); } return map; } void ASObject::addOnePersonToSimpleMap(QString personId, QString personName, QString personUrl, QVariantMap *personMap) { personMap->insert(personId, "" + personName + ""); } QString ASObject::personStringFromSimpleMap(QVariantMap personMap, int totalCount) { QString personString; foreach (QString key, personMap.keys()) { personString.append(personMap.value(key).toString() + ", "); } int remainingCount = totalCount - personMap.keys().length(); // Add "and $COUNT other" if (remainingCount > 0) { if (remainingCount == 1) { personString.append(tr("and one other")); } else { personString.append(tr("and %1 others").arg(remainingCount)); } } else { // Remove last comma and space personString.remove(-2, 2); } return personString; } /* * Returns true if objectType is one of the types that Post() can display * * */ bool ASObject::canDisplayObject(QString objectType) { bool canDisplay = false; if (objectType == QStringLiteral("note") || objectType == QStringLiteral("article") || objectType == QStringLiteral("image") || objectType == QStringLiteral("audio") || objectType == QStringLiteral("video") || objectType == QStringLiteral("file") || objectType == QStringLiteral("comment") || objectType == QStringLiteral("group")) { canDisplay = true; } else { qDebug() << "ASObject::canDisplayObject() - Unsupported:" << objectType; } return canDisplay; } dianara-v1.4.1/src/logviewer.h0000664000175000017500000000337513202665433014365 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 LOGVIEWER_H #define LOGVIEWER_H #include #include #include #include #include #include #include #include #include #include #include #include #include "mischelpers.h" class LogViewer : public QWidget { Q_OBJECT public: explicit LogViewer(QWidget *parent = 0); ~LogViewer(); signals: public slots: void addToLog(QString message, QString url=""); void toggleVisibility(); protected: virtual void closeEvent(QCloseEvent *event); virtual void hideEvent(QHideEvent *event); virtual void showEvent(QShowEvent *event); private: QVBoxLayout *m_mainLayout; QTextBrowser *m_logTextBrowser; QHBoxLayout *m_buttonsLayout; QPushButton *m_clearButton; QPushButton *m_closeButton; QAction *m_closeAction; }; #endif // LOGVIEWER_H dianara-v1.4.1/src/configdialog.cpp0000644000175000017500000013442213173643571015344 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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(GlobalObject *globalObject, QString dataDirectory, int updateInterval, int tabsPosition, bool tabsMovable, FDNotifications *notifier, QWidget *parent) : QWidget(parent) { this->globalObj = globalObject; this->fdNotifier = notifier; this->setWindowTitle(tr("Program Configuration") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("configure", QIcon(":/images/button-configure.png"))); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::ApplicationModal); this->setMinimumSize(640, 520); QSettings settings; QSize savedWindowsize = settings.value("ConfigDialog/configWindowSize").toSize(); if (savedWindowsize.isValid()) { this->resize(savedWindowsize); } settings.beginGroup("Configuration"); // Standalone Proxy config window QByteArray proxyPasswd = QByteArray::fromBase64(settings.value("proxyPassword") .toByteArray()); proxyDialog = new ProxyDialog(settings.value("proxyType", 0).toInt(), settings.value("proxyHostname").toString(), settings.value("proxyPort").toString(), settings.value("proxyUseAuth", false).toBool(), settings.value("proxyUser").toString(), QString::fromLocal8Bit(proxyPasswd), this); //////////////////////////////////////////////////////////////// Upper part // Page 1, general options this->createGeneralPage(updateInterval, tabsPosition, tabsMovable); // Page 2, fonts this->createFontsPage(); // Page 3, colors this->createColorsPage(); // Page 4, timelines options this->createTimelinesPage(); // Page 5, posts options this->createPostsPage(); // Page 6, composer options this->createComposerPage(); // Page 7, privacy options this->createPrivacyPage(); // Page 8, notifications options this->createNotificationsPage(&settings); // Page 9, systray options this->createSystrayPage(&settings); settings.endGroup(); ///////////////////////////////////// List of categories and stacked widget categoriesListWidget = new QListWidget(this); categoriesListWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); categoriesListWidget->setMinimumWidth(140); // kinda TMP/FIXME #if 0 // enable for large-icon-mode with text below categoriesListWidget->setViewMode(QListView::IconMode); categoriesListWidget->setFlow(QListView::TopToBottom); categoriesListWidget->setIconSize(QSize(48, 48)); categoriesListWidget->setWrapping(false); categoriesListWidget->setMovement(QListView::Static); #endif categoriesListWidget->setIconSize(QSize(32, 32)); categoriesListWidget->setUniformItemSizes(true); categoriesListWidget->setWordWrap(true); categoriesListWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); categoriesListWidget->addItem(tr("General Options")); categoriesListWidget->item(0)->setIcon(QIcon::fromTheme("preferences-other", QIcon(":/images/button-configure.png"))); categoriesListWidget->addItem(tr("Fonts")); categoriesListWidget->item(1)->setIcon(QIcon::fromTheme("preferences-desktop-font", QIcon(":/images/button-configure.png"))); categoriesListWidget->addItem(tr("Colors")); categoriesListWidget->item(2)->setIcon(QIcon::fromTheme("preferences-desktop-color", QIcon(":/images/button-configure.png"))); categoriesListWidget->addItem(tr("Timelines")); categoriesListWidget->item(3)->setIcon(QIcon::fromTheme("view-list-details", QIcon(":/images/feed-inbox.png"))); categoriesListWidget->addItem(tr("Posts")); categoriesListWidget->item(4)->setIcon(QIcon::fromTheme("mail-message", QIcon(":/images/button-post.png"))); categoriesListWidget->addItem(tr("Composer")); categoriesListWidget->item(5)->setIcon(QIcon::fromTheme("document-edit", QIcon(":/images/button-edit.png"))); categoriesListWidget->addItem(tr("Privacy")); categoriesListWidget->item(6)->setIcon(QIcon::fromTheme("object-locked", QIcon(":/images/button-password.png"))); categoriesListWidget->addItem(tr("Notifications")); categoriesListWidget->item(7)->setIcon(QIcon::fromTheme("preferences-desktop-notification", QIcon(":/images/button-online.png"))); categoriesListWidget->addItem(tr("System Tray")); // dashboard-show ? categoriesListWidget->item(8)->setIcon(QIcon::fromTheme("configure-toolbars", QIcon(":/images/button-configure.png"))); categoriesStackedWidget = new QStackedWidget(this); categoriesStackedWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); categoriesStackedWidget->addWidget(generalOptionsWidget); categoriesStackedWidget->addWidget(fontOptionsWidget); categoriesStackedWidget->addWidget(colorOptionsWidget); categoriesStackedWidget->addWidget(timelinesOptionsWidget); categoriesStackedWidget->addWidget(postsOptionsWidget); categoriesStackedWidget->addWidget(composerOptionsWidget); categoriesStackedWidget->addWidget(privacyOptionsWidget); categoriesStackedWidget->addWidget(notificationOptionsWidget); categoriesStackedWidget->addWidget(systrayOptionsWidget); connect(categoriesListWidget, &QListWidget::currentRowChanged, categoriesStackedWidget, &QStackedWidget::setCurrentIndex); topLayout = new QHBoxLayout(); topLayout->addWidget(categoriesListWidget); topLayout->addWidget(categoriesStackedWidget); /////////////////////////////////////////////////////////////// Bottom part // Label to show where the data directory is dataDirectoryLabel = new QLabel(tr("Dianara stores data in this folder:") + QString(" %2") .arg(dataDirectory).arg(dataDirectory), this); dataDirectoryLabel->setWordWrap(true); dataDirectoryLabel->setOpenExternalLinks(true); // Save / Cancel buttons saveConfigButton = new QPushButton(QIcon::fromTheme("document-save", QIcon(":/images/button-save.png")), tr("&Save Configuration"), this); connect(saveConfigButton, &QAbstractButton::clicked, this, &ConfigDialog::saveConfiguration); cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("&Cancel"), this); connect(cancelButton, &QAbstractButton::clicked, this, &QWidget::hide); this->buttonsLayout = new QHBoxLayout(); buttonsLayout->setAlignment(Qt::AlignRight); buttonsLayout->addWidget(saveConfigButton); buttonsLayout->addWidget(cancelButton); // ESC to close closeAction = new QAction(this); closeAction->setShortcut(QKeySequence(Qt::Key_Escape)); connect(closeAction, &QAction::triggered, this, &QWidget::hide); this->addAction(closeAction); //// Set up main layout mainLayout = new QVBoxLayout(); mainLayout->addLayout(topLayout, 20); mainLayout->addSpacing(8); mainLayout->addStretch(1); mainLayout->addWidget(dataDirectoryLabel); mainLayout->addSpacing(8); mainLayout->addStretch(2); mainLayout->addLayout(buttonsLayout); this->setLayout(mainLayout); // Activate the first category (so the row already looks selected) this->categoriesListWidget->setCurrentRow(0); this->categoriesListWidget->setFocus(); qDebug() << "Config dialog created"; } ConfigDialog::~ConfigDialog() { qDebug() << "Config dialog destroyed"; } /* * Page 1, general options * */ void ConfigDialog::createGeneralPage(int updateInterval, int tabsPosition, bool tabsMovable) { updateIntervalSpinbox = new QSpinBox(this); updateIntervalSpinbox->setRange(2, 99); // 2-99 min updateIntervalSpinbox->setSuffix(" "+ tr("minutes")); updateIntervalSpinbox->setValue(updateInterval); tabsPositionCombobox = new QComboBox(this); 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", "tabs on left side/west; RTL not affected")); tabsPositionCombobox->addItem(QIcon::fromTheme("arrow-right"), tr("Right side", "tabs on right side/east; RTL not affected")); tabsPositionCombobox->setCurrentIndex(tabsPosition); tabsMovableCheckbox = new QCheckBox(this); tabsMovableCheckbox->setChecked(tabsMovable); proxyConfigButton = new QPushButton(QIcon::fromTheme("preferences-system-network", QIcon(":/images/button-configure.png")), tr("Pro&xy Settings"), this); connect(proxyConfigButton, &QAbstractButton::clicked, proxyDialog, &QWidget::show); filterEditorButton = new QPushButton(QIcon::fromTheme("view-filter", QIcon(":/images/button-filter.png")), tr("Set Up F&ilters"), this); connect(filterEditorButton, &QAbstractButton::clicked, this, &ConfigDialog::filterEditorRequested); QFrame *lineFrame1 = new QFrame(this); // ---------------------------- lineFrame1->setFrameStyle(QFrame::HLine); QFrame *lineFrame2 = new QFrame(this); // ---------------------------- lineFrame2->setFrameStyle(QFrame::HLine); generalOptionsLayout = new QFormLayout(); generalOptionsLayout->addRow(tr("Timeline &update interval"), updateIntervalSpinbox); generalOptionsLayout->addRow(lineFrame1); generalOptionsLayout->addRow(tr("&Tabs position"), tabsPositionCombobox); generalOptionsLayout->addRow(tr("&Movable tabs"), tabsMovableCheckbox); generalOptionsLayout->addRow(lineFrame2); generalOptionsLayout->addRow(tr("Network configuration"), proxyConfigButton); generalOptionsLayout->addRow(tr("Filtering rules"), filterEditorButton); generalOptionsWidget = new QWidget(this); generalOptionsWidget->setLayout(generalOptionsLayout); } /* * Page 2, fonts * */void ConfigDialog::createFontsPage() { fontPicker1 = new FontPicker(tr("Post Titles"), globalObj->getPostTitleFont(), this); fontPicker2 = new FontPicker(tr("Post Contents"), globalObj->getPostContentsFont(), this); fontPicker3 = new FontPicker(tr("Comments"), globalObj->getCommentsFont(), this); fontPicker4 = new FontPicker(tr("Minor Feeds"), globalObj->getMinorFeedFont(), this); // FIXME: more for "Timestamps" or something else? // FIXME: add a "base font size" option fontOptionsLayout = new QVBoxLayout(); fontOptionsLayout->addWidget(fontPicker1); fontOptionsLayout->addWidget(fontPicker2); fontOptionsLayout->addWidget(fontPicker3); fontOptionsLayout->addWidget(fontPicker4); fontOptionsLayout->addStretch(1); fontOptionsWidget = new QWidget(this); fontOptionsWidget->setLayout(fontOptionsLayout); } /* * Page 3, colors * */ void ConfigDialog::createColorsPage() { QStringList colorList = globalObj->getColorsList(); colorPicker1 = new ColorPicker(tr("You are among the recipients " "of the activity, such as " "a comment addressed to you.") + "\n" + tr("Used also when highlighting posts " "addressed to you in the timelines."), colorList.at(0), this); colorPicker2 = new ColorPicker(tr("The activity is in reply to something " "done by you, such as a comment posted " "in reply to one of your notes."), colorList.at(1), this); colorPicker3 = new ColorPicker(tr("You are the object of the activity, " "such as someone adding you to a list."), colorList.at(2), this); colorPicker4 = new ColorPicker(tr("The activity is related to one of " "your objects, such as someone " "liking one of your posts.") + "\n" + tr("Used also when highlighting your " "own posts in the timelines."), colorList.at(3), this); colorPicker5 = new ColorPicker(tr("Item highlighted due to filtering rules."), colorList.at(4), this); colorPicker6 = new ColorPicker(tr("Item is new."), colorList.at(5), this); colorOptionsLayout = new QVBoxLayout(); colorOptionsLayout->addWidget(colorPicker1); colorOptionsLayout->addWidget(colorPicker2); colorOptionsLayout->addWidget(colorPicker3); colorOptionsLayout->addWidget(colorPicker4); colorOptionsLayout->addWidget(colorPicker5); colorOptionsLayout->addWidget(colorPicker6); colorOptionsWidget = new QWidget(this); colorOptionsWidget->setLayout(colorOptionsLayout); } /* * Page 4, timelines options * */ void ConfigDialog::createTimelinesPage() { postsPerPageMainSpinbox = new QSpinBox(this); postsPerPageMainSpinbox->setRange(5, 50); // 5-50 ppp postsPerPageMainSpinbox->setSuffix(" "+ tr("posts", "Goes after a number, as: " "25 posts")); postsPerPageMainSpinbox->setValue(globalObj->getPostsPerPageMain()); postsPerPageOtherSpinbox = new QSpinBox(this); postsPerPageOtherSpinbox->setRange(1, 30); // 1-30 ppp postsPerPageOtherSpinbox->setSuffix(" "+ tr("posts", "This goes after a number, " "like: 10 posts")); postsPerPageOtherSpinbox->setValue(globalObj->getPostsPerPageOther()); showDeletedCheckbox = new QCheckBox(this); showDeletedCheckbox->setChecked(globalObj->getShowDeleted()); hideDuplicatesCheckbox = new QCheckBox(this); hideDuplicatesCheckbox->setChecked(globalObj->getHideDuplicates()); jumpToNewCheckbox = new QCheckBox(this); jumpToNewCheckbox->setChecked(globalObj->getJumpToNewOnUpdate()); minorFeedSnippetsCombobox = new QComboBox(this); minorFeedSnippetsCombobox->addItem(tr("Highlighted activities, except mine")); minorFeedSnippetsCombobox->addItem(tr("Any highlighted activity")); minorFeedSnippetsCombobox->addItem(tr("Always")); minorFeedSnippetsCombobox->addItem(tr("Never")); minorFeedSnippetsCombobox->setCurrentIndex(globalObj->getMinorFeedSnippetsType()); snippetLimitSpinbox = new QSpinBox(this); snippetLimitSpinbox->setRange(10, 10000); snippetLimitSpinbox->setSuffix(" " + tr("characters", "This is a suffix, after a number")); snippetLimitSpinbox->setValue(globalObj->getSnippetsCharLimit()); snippetLimitHlSpinbox = new QSpinBox(this); snippetLimitHlSpinbox->setRange(5, 10000); snippetLimitHlSpinbox->setSuffix(snippetLimitSpinbox->suffix()); snippetLimitHlSpinbox->setValue(globalObj->getSnippetsCharLimitHl()); mfAvatarSizeCombobox = this->newAvatarComboBox(); mfAvatarSizeCombobox->setCurrentIndex(globalObj->getMfAvatarIndex()); mfIconTypeCombobox = new QComboBox(this); mfIconTypeCombobox->addItem(tr("No")); mfIconTypeCombobox->addItem(tr("Before avatar")); mfIconTypeCombobox->addItem(tr("Before avatar, subtle")); mfIconTypeCombobox->addItem(tr("After avatar")); mfIconTypeCombobox->addItem(tr("After avatar, subtle")); mfIconTypeCombobox->setCurrentIndex(globalObj->getMfIconType()); // TMP/FIXME, use GroupBoxes instead of a separator QFrame *lineFrame = new QFrame(this); // ----------------------- lineFrame->setFrameStyle(QFrame::HLine); timelinesOptionsLayout = new QFormLayout(); timelinesOptionsLayout->addRow(tr("&Posts per page, main timeline"), postsPerPageMainSpinbox); timelinesOptionsLayout->addRow(tr("Posts per page, &other timelines"), postsPerPageOtherSpinbox); timelinesOptionsLayout->addRow(tr("Show information for deleted posts"), showDeletedCheckbox); timelinesOptionsLayout->addRow(tr("Hide duplicated posts"), hideDuplicatesCheckbox); timelinesOptionsLayout->addRow(tr("Jump to new posts line on update"), jumpToNewCheckbox); timelinesOptionsLayout->addRow(lineFrame); timelinesOptionsLayout->addRow(tr("Show snippets in minor feeds"), minorFeedSnippetsCombobox); timelinesOptionsLayout->addRow(tr("Snippet limit"), snippetLimitSpinbox); timelinesOptionsLayout->addRow(tr("Snippet limit when highlighted"), snippetLimitHlSpinbox); timelinesOptionsLayout->addRow(tr("Minor feed avatar sizes"), // tmp string mfAvatarSizeCombobox); timelinesOptionsLayout->addRow(tr("Show activity icons"), mfIconTypeCombobox); timelinesOptionsWidget = new QWidget(this); timelinesOptionsWidget->setLayout(timelinesOptionsLayout); } /* * Page 5, posts options * */ void ConfigDialog::createPostsPage() { postAvatarSizeCombobox = this->newAvatarComboBox(); postAvatarSizeCombobox->setCurrentIndex(globalObj->getPostAvatarIndex()); commentAvatarSizeCombobox = this->newAvatarComboBox(); commentAvatarSizeCombobox->setCurrentIndex(globalObj->getCommentAvatarIndex()); showExtendedSharesCheckbox = new QCheckBox(this); showExtendedSharesCheckbox->setChecked(globalObj->getPostExtendedShares()); showExtraInfoCheckbox = new QCheckBox(this); showExtraInfoCheckbox->setChecked(globalObj->getPostShowExtraInfo()); postHLAuthorCommentsCheckbox = new QCheckBox(this); postHLAuthorCommentsCheckbox->setChecked(globalObj->getPostHLAuthorComments()); postHLOwnCommentsCheckbox = new QCheckBox(this); postHLOwnCommentsCheckbox->setChecked(globalObj->getPostHLOwnComments()); postIgnoreSslInImages = new QCheckBox(tr("Only for images inserted from " "web sites.") + "\n" + tr("Use with care."), this); postIgnoreSslInImages->setChecked(globalObj->getPostIgnoreSslInImages()); postFullImagesCheckbox = new QCheckBox(this); postFullImagesCheckbox->setChecked(globalObj->getPostFullImages()); postsOptionsLayout = new QFormLayout(); postsOptionsLayout->addRow(tr("Avatar size"), postAvatarSizeCombobox); postsOptionsLayout->addRow(tr("Avatar size in comments"), commentAvatarSizeCombobox); postsOptionsLayout->addRow(tr("Show extended share information"), showExtendedSharesCheckbox); postsOptionsLayout->addRow(tr("Show extra information"), showExtraInfoCheckbox); postsOptionsLayout->addRow(tr("Highlight post author's comments"), postHLAuthorCommentsCheckbox); postsOptionsLayout->addRow(tr("Highlight your own comments"), postHLOwnCommentsCheckbox); postsOptionsLayout->addRow(tr("Ignore SSL errors in images"), postIgnoreSslInImages); postsOptionsLayout->addRow(tr("Show full size images"), postFullImagesCheckbox); postsOptionsWidget = new QWidget(this); postsOptionsWidget->setLayout(postsOptionsLayout); } /* * Page 6, composer options * */ void ConfigDialog::createComposerPage() { publicPostsCheckbox = new QCheckBox(this); publicPostsCheckbox->setChecked(globalObj->getPublicPostsByDefault()); useFilenameAsTitleCheckbox = new QCheckBox(this); useFilenameAsTitleCheckbox->setChecked(globalObj->getUseFilenameAsTitle()); showCharacterCounterCheckbox = new QCheckBox(this); showCharacterCounterCheckbox->setChecked(globalObj->getShowCharacterCounter()); composerOptionsLayout = new QFormLayout(); composerOptionsLayout->addRow(tr("Public posts as &default"), publicPostsCheckbox); composerOptionsLayout->addRow(tr("Use attachment filename as initial " "post title"), useFilenameAsTitleCheckbox); composerOptionsLayout->addRow(tr("Show character counter"), showCharacterCounterCheckbox); composerOptionsWidget = new QWidget(this); composerOptionsWidget->setLayout(composerOptionsLayout); } /* * Page 7, privacy options * */ void ConfigDialog::createPrivacyPage() { silentFollowsCheckbox = new QCheckBox(this); silentFollowsCheckbox->setChecked(globalObj->getSilentFollows()); silentListsCheckbox = new QCheckBox(this); silentListsCheckbox->setChecked(globalObj->getSilentLists()); silentLikesCheckbox = new QCheckBox(this); silentLikesCheckbox->setChecked(globalObj->getSilentLikes()); privacyOptionsLayout = new QFormLayout(); privacyOptionsLayout->addRow(tr("Don't inform followers when " "following someone"), silentFollowsCheckbox); privacyOptionsLayout->addRow(tr("Don't inform followers when " "handling lists"), silentListsCheckbox); privacyOptionsLayout->addRow(tr("Inform only the author when " "liking things"), silentLikesCheckbox); privacyOptionsWidget = new QWidget(this); privacyOptionsWidget->setLayout(privacyOptionsLayout); } /* * Page 8, notifications options * */ void ConfigDialog::createNotificationsPage(QSettings *settings) { notificationStyleCombobox = new QComboBox(this); notificationStyleCombobox->addItem(QIcon::fromTheme("preferences-desktop-notification"), tr("As system notifications")); notificationStyleCombobox->addItem(QIcon::fromTheme("view-conversation-balloon"), tr("Using own notifications")); notificationStyleCombobox->addItem(QIcon::fromTheme("user-busy"), // dialog-cancel tr("Don't show notifications")); notificationStyleCombobox->setCurrentIndex(settings->value("showNotifications", 0).toInt()); // Keeping these connects old-style due to overload connect(notificationStyleCombobox, SIGNAL(currentIndexChanged(int)), this, SLOT(toggleNotificationDetails(int))); // Check FD.org notifications availability when selecting an option connect(notificationStyleCombobox, SIGNAL(currentIndexChanged(int)), this, SLOT(showDemoNotification(int))); notificationsStatusLabel = new QLabel(this); notificationDurationSpinbox = new QSpinBox(this); notificationDurationSpinbox->setRange(1, 60); notificationDurationSpinbox->setSuffix(" " + tr("seconds", "Next to a duration, in seconds")); notificationDurationSpinbox->setValue(settings->value("notificationDuration", 4).toInt()); // 4s default notificationPersistentCheckbox = new QCheckBox(this); connect(notificationPersistentCheckbox, &QAbstractButton::toggled, notificationDurationSpinbox, &QWidget::setDisabled); notificationPersistentCheckbox->setChecked(settings->value("notificationPersistent", false).toBool()); notificationTaskbarCheckbox = new QCheckBox(this); notificationTaskbarCheckbox->setChecked(globalObj->getNotifyInTaskbar()); notifyNewTLCheckbox = new QCheckBox(this); notifyNewTLCheckbox->setChecked(settings->value("notifyNewTL", false).toBool()); notifyHLTLCheckbox = new QCheckBox(this); notifyHLTLCheckbox->setChecked(settings->value("notifyHLTL", true).toBool()); notifyNewMWCheckbox = new QCheckBox(this); notifyNewMWCheckbox->setChecked(settings->value("notifyNewMW", false).toBool()); notifyHLMWCheckbox = new QCheckBox(this); notifyHLMWCheckbox->setChecked(settings->value("notifyHLMW", true).toBool()); notifyErrorsCheckbox = new QCheckBox(this); notifyErrorsCheckbox->setChecked(settings->value("notifyErrors", true).toBool()); this->syncNotifierOptions(); // Initial check to see if currently selected style is available this->checkNotifications(notificationStyleCombobox->currentIndex()); notificationOptionsLayout = new QFormLayout(); notificationOptionsLayout->addRow(tr("Notification Style"), notificationStyleCombobox); notificationOptionsLayout->addRow(QString(), // Empty label on left to align with right column notificationsStatusLabel); notificationOptionsLayout->addRow(tr("Duration"), notificationDurationSpinbox); notificationOptionsLayout->addRow(tr("Persistent Notifications"), notificationPersistentCheckbox); notificationOptionsLayout->addRow(tr("Also highlight taskbar entry"), notificationTaskbarCheckbox); notificationOptionsLayout->addRow(new QLabel("
" // TMP/FIXME: put these options "" // inside a GroupBox? + tr("Notify when receiving:") + "" "
", this)); notificationOptionsLayout->addRow(tr("New posts"), notifyNewTLCheckbox); notificationOptionsLayout->addRow(tr("Highlighted posts"), notifyHLTLCheckbox); notificationOptionsLayout->addRow(tr("New activities in minor feed"), notifyNewMWCheckbox); notificationOptionsLayout->addRow(tr("Highlighted activities in minor feed"), notifyHLMWCheckbox); notificationOptionsLayout->addRow(tr("Important errors"), notifyErrorsCheckbox); notificationOptionsWidget = new QWidget(this); notificationOptionsWidget->setLayout(notificationOptionsLayout); } /* * Page 9, systray options * */ void ConfigDialog::createSystrayPage(QSettings *settings) { systrayIconTypeCombobox = new QComboBox(this); systrayIconTypeCombobox->addItem(tr("Default")); systrayIconTypeCombobox->addItem(tr("System iconset, if available")); systrayIconTypeCombobox->addItem(tr("Show your current avatar")); systrayIconTypeCombobox->addItem(tr("Custom icon")); systrayIconTypeCombobox->setCurrentIndex(settings->value("systrayIconType", 0).toInt()); // connect kept old-style connect(systrayIconTypeCombobox, SIGNAL(currentIndexChanged(int)), this, SLOT(toggleCustomIconButton(int))); systrayCustomIconButton = new QPushButton(tr("S&elect..."), this); systrayIconLastUsedDir = QDir::homePath(); systrayCustomIconFN = settings->value("systrayCustomIconFN").toString(); // FIXME: merge this with code used in pickCustomIconFile() // and turn the warning messageBox into a label if (!QPixmap(systrayCustomIconFN).isNull()) { systrayCustomIconButton->setIcon(QIcon(systrayCustomIconFN)); systrayCustomIconButton->setToolTip("" + systrayCustomIconFN); } else { systrayCustomIconButton->setIcon(QIcon(":/icon/32x32/dianara.png")); } connect(systrayCustomIconButton, &QAbstractButton::clicked, this, &ConfigDialog::pickCustomIconFile); // Enable/disable initially based on loaded config this->toggleCustomIconButton(systrayIconTypeCombobox->currentIndex()); systrayHideCheckbox = new QCheckBox(this); systrayHideCheckbox->setChecked(this->globalObj->getHideInTray()); systrayOptionsLayout = new QFormLayout(); systrayOptionsLayout->addRow(tr("System Tray Icon &Type"), systrayIconTypeCombobox); systrayOptionsLayout->addRow(tr("Custom &Icon"), systrayCustomIconButton); systrayOptionsLayout->addRow(tr("Hide window on startup"), systrayHideCheckbox); systrayOptionsWidget = new QWidget(this); systrayOptionsWidget->setLayout(systrayOptionsLayout); } QComboBox *ConfigDialog::newAvatarComboBox() { QComboBox *avatarCombobox = new QComboBox(this); avatarCombobox->addItem("16x16"); avatarCombobox->addItem("32x32"); avatarCombobox->addItem("48x48"); avatarCombobox->addItem("64x64"); avatarCombobox->addItem("96x96"); avatarCombobox->addItem("128x128"); avatarCombobox->addItem("256x256"); return avatarCombobox; } void ConfigDialog::syncNotifierOptions() { this->toggleNotificationDetails(notificationStyleCombobox->currentIndex()); int notificationDuration = notificationDurationSpinbox->value(); if (notificationPersistentCheckbox->isChecked()) { notificationDuration = 0; } this->fdNotifier->setNotificationOptions(notificationStyleCombobox->currentIndex(), notificationDuration, notifyNewTLCheckbox->isChecked(), notifyHLTLCheckbox->isChecked(), notifyNewMWCheckbox->isChecked(), notifyHLMWCheckbox->isChecked(), notifyErrorsCheckbox->isChecked()); } /* * Get a demo message for the current notification style * * Show also a warning is system notifications are not available * */ QString ConfigDialog::checkNotifications(int notificationStyle) { QString demoText; if (notificationStyle == FDNotifications::SystemNotifications) { if (fdNotifier->areNotificationsAvailable()) { demoText = tr("This is a system notification"); } else { demoText = tr("System notifications are not available!") + "
" + tr("Own notifications will be used."); notificationsStatusLabel->setText("" + demoText + ""); /* FIXME: Should also check availability of system tray icon, * needed to show Qt's balloon notifications */ } } else { demoText = tr("This is a basic notification"); } return demoText; } void ConfigDialog::setPublicPosts(bool value) { this->publicPostsCheckbox->setChecked(value); this->saveConfiguration(); // This might be overkill -- FIXME TMP } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// SLOTS ////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void ConfigDialog::saveConfiguration() { QSettings settings; if (!settings.isWritable()) { // TMP FIXME: notify user properly qDebug() << "ERROR SAVING CONFIGURATION (maybe disk full?)"; return; } settings.beginGroup("Configuration"); // General settings.setValue("updateInterval", this->updateIntervalSpinbox->value()); settings.setValue("tabsPosition", this->tabsPositionCombobox->currentIndex()); settings.setValue("tabsMovable", this->tabsMovableCheckbox->isChecked()); // Fonts settings.setValue("font1", this->fontPicker1->getFontInfo()); settings.setValue("font2", this->fontPicker2->getFontInfo()); settings.setValue("font3", this->fontPicker3->getFontInfo()); settings.setValue("font4", this->fontPicker4->getFontInfo()); this->globalObj->syncFontSettings(this->fontPicker1->getFontInfo(), this->fontPicker2->getFontInfo(), this->fontPicker3->getFontInfo(), this->fontPicker4->getFontInfo()); // Colors settings.setValue("color1", this->colorPicker1->getCurrentColor()); settings.setValue("color2", this->colorPicker2->getCurrentColor()); settings.setValue("color3", this->colorPicker3->getCurrentColor()); settings.setValue("color4", this->colorPicker4->getCurrentColor()); settings.setValue("color5", this->colorPicker5->getCurrentColor()); settings.setValue("color6", this->colorPicker6->getCurrentColor()); QStringList highlightColorsList; highlightColorsList << this->colorPicker1->getCurrentColor() << this->colorPicker2->getCurrentColor() << this->colorPicker3->getCurrentColor() << this->colorPicker4->getCurrentColor() << this->colorPicker5->getCurrentColor() << this->colorPicker6->getCurrentColor(); this->globalObj->syncColorSettings(highlightColorsList); // Timelines settings.setValue("postsPerPageMain", this->postsPerPageMainSpinbox->value()); settings.setValue("postsPerPageOther", this->postsPerPageOtherSpinbox->value()); settings.setValue("showDeletedPosts", this->showDeletedCheckbox->isChecked()); settings.setValue("hideDuplicatedPosts", this->hideDuplicatesCheckbox->isChecked()); settings.setValue("jumpToNewOnUpdate", this->jumpToNewCheckbox->isChecked()); settings.setValue("mfSnippetsType", this->minorFeedSnippetsCombobox->currentIndex()); settings.setValue("snippetCharLimit", this->snippetLimitSpinbox->value()); settings.setValue("snippetCharLimitHl", this->snippetLimitHlSpinbox->value()); settings.setValue("minorFeedAvatarIndex", this->mfAvatarSizeCombobox->currentIndex()); settings.setValue("minorFeedIconType", this->mfIconTypeCombobox->currentIndex()); this->globalObj->syncTimelinesSettings(this->postsPerPageMainSpinbox->value(), this->postsPerPageOtherSpinbox->value(), this->showDeletedCheckbox->isChecked(), this->hideDuplicatesCheckbox->isChecked(), this->jumpToNewCheckbox->isChecked(), this->minorFeedSnippetsCombobox->currentIndex(), this->snippetLimitSpinbox->value(), this->snippetLimitHlSpinbox->value(), this->mfAvatarSizeCombobox->currentIndex(), this->mfIconTypeCombobox->currentIndex()); // Posts settings.setValue("postAvatarIndex", this->postAvatarSizeCombobox->currentIndex()); settings.setValue("commentAvatarIndex", this->commentAvatarSizeCombobox->currentIndex()); settings.setValue("postExtendedShares", this->showExtendedSharesCheckbox->isChecked()); settings.setValue("postShowExtraInfo", this->showExtraInfoCheckbox->isChecked()); settings.setValue("postHLAuthorComments", this->postHLAuthorCommentsCheckbox->isChecked()); settings.setValue("postHLOwnComments", this->postHLOwnCommentsCheckbox->isChecked()); settings.setValue("postIgnoreSslInImages", this->postIgnoreSslInImages->isChecked()); settings.setValue("postFullImages", this->postFullImagesCheckbox->isChecked()); this->globalObj->syncPostSettings(this->postAvatarSizeCombobox->currentIndex(), this->commentAvatarSizeCombobox->currentIndex(), this->showExtendedSharesCheckbox->isChecked(), this->showExtraInfoCheckbox->isChecked(), this->postHLAuthorCommentsCheckbox->isChecked(), this->postHLOwnCommentsCheckbox->isChecked(), this->postIgnoreSslInImages->isChecked(), this->postFullImagesCheckbox->isChecked()); // Composer settings.setValue("publicPosts", this->publicPostsCheckbox->isChecked()); settings.setValue("useFilenameAsTitle", this->useFilenameAsTitleCheckbox->isChecked()); settings.setValue("showCharacterCounter", this->showCharacterCounterCheckbox->isChecked()); this->globalObj->syncComposerSettings(this->publicPostsCheckbox->isChecked(), this->useFilenameAsTitleCheckbox->isChecked(), this->showCharacterCounterCheckbox->isChecked()); // Privacy settings.setValue("silentFollows", this->silentFollowsCheckbox->isChecked()); settings.setValue("silentLists", this->silentListsCheckbox->isChecked()); settings.setValue("silentLikes", this->silentLikesCheckbox->isChecked()); this->globalObj->syncPrivacySettings(this->silentFollowsCheckbox->isChecked(), this->silentListsCheckbox->isChecked(), this->silentLikesCheckbox->isChecked()); // Notifications settings.setValue("showNotifications", this->notificationStyleCombobox->currentIndex()); settings.setValue("notificationDuration", this->notificationDurationSpinbox->value()); settings.setValue("notificationPersistent", this->notificationPersistentCheckbox->isChecked()); settings.setValue("notificationTaskbar", this->notificationTaskbarCheckbox->isChecked()); settings.setValue("notifyNewTL", this->notifyNewTLCheckbox->isChecked()); settings.setValue("notifyHLTL", this->notifyHLTLCheckbox->isChecked()); settings.setValue("notifyNewMW", this->notifyNewMWCheckbox->isChecked()); settings.setValue("notifyHLMW", this->notifyHLMWCheckbox->isChecked()); settings.setValue("notifyErrors", this->notifyErrorsCheckbox->isChecked()); this->syncNotifierOptions(); this->globalObj->syncNotificationSettings(this->notificationTaskbarCheckbox->isChecked()); // FIXME, most still missing // Tray settings.setValue("systrayIconType", this->systrayIconTypeCombobox->currentIndex()); settings.setValue("systrayCustomIconFN", this->systrayCustomIconFN); settings.setValue("systrayHideInTray", this->systrayHideCheckbox->isChecked()); this->globalObj->syncTrayOptions(this->systrayHideCheckbox->isChecked()); // FIXME: some // still empty settings.endGroup(); settings.sync(); emit configurationChanged(); // Ask MainWindow to reload some stuff this->hide(); // this->close() would end the program if mainWindow was hidden qDebug() << "ConfigDialog: config saved"; } void ConfigDialog::pickCustomIconFile() { systrayCustomIconFN = QFileDialog::getOpenFileName(this, tr("Select custom icon"), systrayIconLastUsedDir, tr("Image files") + " (*.png *.jpg *.jpe " "*.jpeg *.gif);;" + tr("All files") + " (*)"); if (!systrayCustomIconFN.isEmpty()) { qDebug() << "Selected" << systrayCustomIconFN << "as new custom tray icon"; QFileInfo fileInfo(systrayCustomIconFN); this->systrayIconLastUsedDir = fileInfo.path(); QPixmap iconPixmap = QPixmap(systrayCustomIconFN); if (!iconPixmap.isNull()) { this->systrayCustomIconButton->setIcon(QIcon(systrayCustomIconFN)); this->systrayCustomIconButton->setToolTip("" + systrayCustomIconFN); } else { QMessageBox::warning(this, tr("Invalid image"), tr("The selected image is not valid.")); qDebug() << "Invalid tray icon file selected"; } } } void ConfigDialog::showDemoNotification(int notificationStyle) { notificationsStatusLabel->clear(); if (notificationStyle == FDNotifications::NoNotifications) { return; } this->syncNotifierOptions(); QString demoNotificationText = this->checkNotifications(notificationStyle); this->fdNotifier->showMessage(demoNotificationText); } void ConfigDialog::toggleNotificationDetails(int currentOption) { bool state = true; if (currentOption == 2) // No notifications; disable details so it's clearer { state = false; } this->notificationDurationSpinbox->setEnabled(state // This one also depends on another option && !notificationPersistentCheckbox->isChecked()); this->notificationPersistentCheckbox->setEnabled(state); this->notificationTaskbarCheckbox->setEnabled(state); this->notifyNewTLCheckbox->setEnabled(state); this->notifyHLTLCheckbox->setEnabled(state); this->notifyNewMWCheckbox->setEnabled(state); this->notifyHLMWCheckbox->setEnabled(state); this->notifyErrorsCheckbox->setEnabled(state); } void ConfigDialog::toggleCustomIconButton(int currentOption) { bool state = false; if (currentOption == 3) // Enable only for last option, "Custom icon" { state = true; } this->systrayCustomIconButton->setEnabled(state); } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////// PROTECTED //////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void ConfigDialog::closeEvent(QCloseEvent *event) { this->hide(); event->ignore(); } void ConfigDialog::hideEvent(QHideEvent *event) { QSettings settings; settings.setValue("ConfigDialog/configWindowSize", this->size()); event->accept(); } dianara-v1.4.1/src/emailchanger.cpp0000644000175000017500000001456713207030362015327 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "emailchanger.h" EmailChanger::EmailChanger(QString explanation, PumpController *pumpController, QWidget *parent) : QWidget(parent) { this->setWindowTitle(tr("Change E-mail Address") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("view-pim-mail")); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::WindowModal); this->setMinimumSize(500, 300); m_pumpController = pumpController; m_infoLabel = new QLabel(explanation + ".", this); m_infoLabel->setWordWrap(true); m_mailLineEdit = new QLineEdit(this); m_mailLineEdit->setClearButtonEnabled(true); connect(m_mailLineEdit, &QLineEdit::textEdited, this, &EmailChanger::validateFields); m_mailRepeatLineEdit = new QLineEdit(this); m_mailRepeatLineEdit->setClearButtonEnabled(true); connect(m_mailRepeatLineEdit, &QLineEdit::textEdited, this, &EmailChanger::validateFields); m_passwordLineEdit = new QLineEdit(this); m_passwordLineEdit->setEchoMode(QLineEdit::Password); connect(m_passwordLineEdit, &QLineEdit::textEdited, this, &EmailChanger::validateFields); m_errorsLabel = new QLabel(this); m_errorsLabel->setAlignment(Qt::AlignCenter); m_errorsLabel->setWordWrap(true); QFont errorsFont; errorsFont.setPointSize(errorsFont.pointSize() - 1); errorsFont.setItalic(true); m_errorsLabel->setFont(errorsFont); m_changeButton = new QPushButton(QIcon::fromTheme("document-save", QIcon(":/images/button-save.png")), tr("Change"), this); m_changeButton->setDisabled(true); // Initially disabled connect(m_changeButton, &QAbstractButton::clicked, this, &EmailChanger::changeEmail); m_cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("&Cancel"), this); connect(m_cancelButton, &QAbstractButton::clicked, this, &EmailChanger::cancelDialog); // ESC to cancel, too m_cancelAction = new QAction(this); m_cancelAction->setShortcut(QKeySequence(Qt::Key_Escape)); connect(m_cancelAction, &QAction::triggered, this, &EmailChanger::cancelDialog); this->addAction(m_cancelAction); // Layout m_middleLayout = new QFormLayout(); m_middleLayout->addRow(tr("E-mail Address:"), m_mailLineEdit); m_middleLayout->addRow(tr("Again:"), m_mailRepeatLineEdit); m_middleLayout->addRow(tr("Your Password:"), m_passwordLineEdit); m_bottomLayout = new QHBoxLayout(); m_bottomLayout->setAlignment(Qt::AlignRight); m_bottomLayout->addWidget(m_changeButton); m_bottomLayout->addWidget(m_cancelButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->addWidget(m_infoLabel); m_mainLayout->addSpacing(8); m_mainLayout->addStretch(1); m_mainLayout->addLayout(m_middleLayout); m_mainLayout->addStretch(1); m_mainLayout->addWidget(m_errorsLabel, 2); m_mainLayout->addStretch(1); m_mainLayout->addSpacing(8); m_mainLayout->addLayout(m_bottomLayout); this->setLayout(m_mainLayout); qDebug() << "EmailChanger created"; } EmailChanger::~EmailChanger() { qDebug() << "EmailChanger destroyed"; } void EmailChanger::setCurrentEmail(QString email) { m_currentEmail = email; m_mailLineEdit->setText(m_currentEmail); } /****************************************************************************/ /******************************** SLOTS *************************************/ /****************************************************************************/ void EmailChanger::validateFields() { bool validFields = true; const QString mail1 = m_mailLineEdit->text().trimmed(); const QString mail2 = m_mailRepeatLineEdit->text().trimmed(); QString errorString = "
    "; // Both e-mails must be equal if (mail1 != mail2) { errorString.append("
  • " + tr("E-mail addresses don't match!") + "
  • "); validFields = false; } // Password can't be empty if (m_passwordLineEdit->text().isEmpty()) { errorString.append("
  • " + tr("Password is empty!") + "
  • "); validFields = false; } errorString.append("
"); m_errorsLabel->setText(errorString); m_changeButton->setEnabled(validFields); } void EmailChanger::changeEmail() { m_currentEmail = m_mailLineEdit->text().trimmed(); m_pumpController->updateUserEmail(m_currentEmail, m_passwordLineEdit->text()); this->cancelDialog(); } void EmailChanger::cancelDialog() { m_mailRepeatLineEdit->clear(); m_passwordLineEdit->clear(); m_errorsLabel->clear(); m_changeButton->setDisabled(true); // Ensure good e-mail is set, in case dialog was cancelled with bad value m_mailLineEdit->setText(m_currentEmail); m_mailLineEdit->setFocus(); this->hide(); } /****************************************************************************/ /****************************** PROTECTED ***********************************/ /****************************************************************************/ void EmailChanger::closeEvent(QCloseEvent *event) { event->ignore(); this->cancelDialog(); } dianara-v1.4.1/src/notifications.h0000644000175000017500000000454313210104206015210 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #ifdef QT_DBUS_LIB #include #endif #include class FDNotifications : public QObject { Q_OBJECT public: FDNotifications(QObject *parent); ~FDNotifications(); bool areNotificationsAvailable(); void setNotificationOptions(int style, int duration, bool notifyNewTL, bool notifyHLTL, bool notifyNewMW, bool notifyHLMW, bool notifyErr); void setCurrentUserId(QString newId); bool getNotifyNewTimeline(); bool getNotifyHLTimeline(); bool getNotifyNewMeanwhile(); bool getNotifyHLMeanwhile(); bool getNotifyErrors(); enum notificationTypes { SystemNotifications, FallbackNotifications, NoNotifications }; signals: void showFallbackNotification(QString title, QString message, int duration); public slots: void showMessage(QString message); private: #ifdef QT_DBUS_LIB QDBusConnection *m_busConnection; #endif bool m_notificationsAvailable; int m_notificationType; int m_notificationDuration; bool m_notifyNewTimeline; bool m_notifyHLTimeline; bool m_notifyNewMeanwhile; bool m_notifyHLMeanwhile; bool m_notifyErrors; QString m_currentUserId; }; #endif // NOTIFICATIONS_H dianara-v1.4.1/src/hclabel.h0000664000175000017500000000304713202666553013754 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 HCLABEL_H #define HCLABEL_H #include #include #include #include class HClabel : public QLabel { Q_OBJECT public: explicit HClabel(QString initialText = QString(), QWidget *parent = 0); ~HClabel(); void setHighlighted(bool highlighted); void setExpanded(bool isExpanded); void setBaseText(QString text); void setExtendedText(QString text); signals: void clicked(); public slots: void toggleContent(); protected: virtual void mousePressEvent(QMouseEvent *event); private: QString m_baseText; QString m_extendedText; bool m_expanded; QTimer *m_toggleTimer; }; #endif // HCLABEL_H dianara-v1.4.1/src/draftsmanager.cpp0000664000175000017500000003014113210124705015511 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "draftsmanager.h" DraftsManager::DraftsManager(GlobalObject *globalObject, QWidget *parent) : QWidget(parent) { m_globalObject = globalObject; this->setWindowTitle(tr("Draft Manager") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("document-edit", QIcon(":/images/button-edit.png"))); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::ApplicationModal); this->setMinimumSize(500, 400); QSettings settings; QSize savedWindowsize = settings.value("DraftsManager/" "draftsWindowSize").toSize(); if (savedWindowsize.isValid()) { this->resize(savedWindowsize); } // Prepare submenus for the exported "Drafts" button m_loadMenu = new QMenu(tr("Load"), this); m_loadMenu->setIcon(QIcon::fromTheme("document-open", QIcon(":/images/button-open.png"))); connect(m_loadMenu, &QMenu::triggered, this, &DraftsManager::onDraftSelectedFromMenu); m_saveAction = new QAction(QIcon::fromTheme("document-save", QIcon(":/images/button-save.png")), tr("Save"), this); m_saveAction->setShortcut(QKeySequence("Ctrl+S")); connect(m_saveAction, &QAction::triggered, this, &DraftsManager::saveDraftRequested); m_showManagerAction = new QAction(QIcon::fromTheme("file-manager", QIcon(":/images/button-open.png")), tr("Manage drafts..."), this); connect(m_showManagerAction, &QAction::triggered, this, &QWidget::show); m_draftsMenu = new QMenu("*drafts-menu*", this); m_draftsMenu->addMenu(m_loadMenu); m_draftsMenu->addAction(m_saveAction); m_draftsMenu->addSeparator(); m_draftsMenu->addAction(m_showManagerAction); // Widgets to manage drafts m_listWidget = new QListWidget(this); connect(m_listWidget, &QListWidget::currentRowChanged, this, &DraftsManager::onDraftSelectedFromList); m_previewLabel = new QLabel(this); m_previewLabel->setAlignment(Qt::AlignTop); m_deleteButton = new QPushButton(QIcon::fromTheme("edit-delete", QIcon(":/images/button-delete.png")), tr("&Delete selected draft"), this); connect(m_deleteButton, &QAbstractButton::clicked, this, &DraftsManager::deleteSelectedDraft); m_closeButton = new QPushButton(QIcon::fromTheme("window-close", QIcon(":/images/button-close.png")), tr("&Close"), this); connect(m_closeButton, &QAbstractButton::clicked, this, &QWidget::close); // ESC to cancel, too m_closeAction = new QAction(this); m_closeAction->setShortcut(QKeySequence(Qt::Key_Escape)); connect(m_closeAction, &QAction::triggered, this, &QWidget::close); this->addAction(m_closeAction); // Layouts m_buttonsLayout = new QHBoxLayout(); m_buttonsLayout->addWidget(m_deleteButton); m_buttonsLayout->addStretch(1); m_buttonsLayout->addWidget(m_closeButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->addWidget(m_listWidget, 2); m_mainLayout->addWidget(m_previewLabel, 1); m_mainLayout->addStretch(); m_mainLayout->addSpacing(4); m_mainLayout->addLayout(m_buttonsLayout); this->setLayout(m_mainLayout); loadDraftsFromFile(); qDebug() << "DraftsManager created"; } DraftsManager::~DraftsManager() { qDebug() << "DraftsManager destroyed"; } void DraftsManager::loadDraftsFromFile() { DataFile *dataFile = new DataFile(m_globalObject->getDataDirectory() + "/drafts/drafts.json", this); QVariantList draftsList = dataFile->loadData(); int itemCount = 0; foreach (QVariant draftVariant, draftsList) { QString draftId = draftVariant.toMap().value("id").toString(); if (!draftId.isEmpty()) // Don't load broken JSON { QAction *newAction = new QAction(draftId, this); newAction->setData(draftVariant); m_loadMenu->addAction(newAction); m_listWidget->addItem(draftId); ++itemCount; } } // Don't let the user try to access an empty menu, or delete from an empty list m_loadMenu->setDisabled(itemCount == 0); m_deleteButton->setDisabled(itemCount == 0); // Make first item in manager's list actually *look* selected m_listWidget->setCurrentRow(0); qDebug() << "Loaded " << itemCount << "items"; } void DraftsManager::saveDraftsToFile() { DataFile *dataFile = new DataFile(m_globalObject->getDataDirectory() + "/drafts/drafts.json", this); QVariantList draftsList; foreach (QAction *draftAction, m_loadMenu->actions()) { draftsList.append(draftAction->data().toMap()); } dataFile->saveData(draftsList); } void DraftsManager::saveDraft(QString title, QString body, QString type, QString attachment, QVariantMap audience, int position) { bool hadDraftId = true; if (m_currentDraftId.isEmpty()) { m_currentDraftId = this->generateDraftId(title, body); hadDraftId = false; } QVariantMap draftMap; draftMap.insert("id", m_currentDraftId); draftMap.insert("title", title); draftMap.insert("body", body); draftMap.insert("type", type); draftMap.insert("attachment", attachment); draftMap.insert("audience", audience); draftMap.insert("position", position); if (hadDraftId) { foreach (QAction *action, m_loadMenu->actions()) { if (action->text() == m_currentDraftId) { action->setData(draftMap); qDebug() << "Replaced draft:" << m_currentDraftId; } } } else { QAction *newAction = new QAction(m_currentDraftId, this); newAction->setData(draftMap); m_loadMenu->addAction(newAction); m_loadMenu->setEnabled(true); m_deleteButton->setEnabled(true); m_listWidget->addItem(newAction->text()); qDebug() << "Created new draft with ID:" << m_currentDraftId; } saveDraftsToFile(); // TMP / FIXME } /* * Generate a draft ID based on possible title, body and current date+time * */ QString DraftsManager::generateDraftId(QString title, QString body) { QString newId; if (title.isEmpty()) { newId = "_untitled_-"; } else { newId = title.left(16).trimmed() + "-"; } QString bodySnippet = MiscHelpers::htmlToPlainText(body).left(10).trimmed(); if (!bodySnippet.isEmpty()) { newId.append(bodySnippet + "-"); } newId = newId.toLower(); newId.replace(QRegExp("\\s+"), "_"); newId.append(QDate::currentDate().toString(Qt::ISODate) + "-" + QTime::currentTime().toString(Qt::ISODate)); return newId; } void DraftsManager::updateDraftId(QString id) { m_currentDraftId = id; } QMenu *DraftsManager::getDraftMenu() { return m_draftsMenu; } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////// SLOTS ////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void DraftsManager::onDraftSelectedFromMenu(QAction *selectedAction) { QVariantMap draftMap = selectedAction->data().toMap(); qDebug() << "Selected draft from menu:" << draftMap.value("id").toString(); emit draftSelected(draftMap.value("id").toString(), draftMap.value("title").toString(), draftMap.value("body").toString(), draftMap.value("type").toString(), draftMap.value("attachment").toString(), draftMap.value("audience").toMap(), draftMap.value("position").toInt()); // currentDraftId will be updated from Publisher, if draft is allowed to load } void DraftsManager::onDraftSelectedFromList(int row) { if (row == -1) // For instance, after deleting the last draft { return; // Avoid crashing } QVariantMap draftMap = m_loadMenu->actions().at(row)->data().toMap(); QString title = draftMap.value("title").toString().trimmed(); if (title.isEmpty()) { title = "*" + tr("Untitled draft") + "*"; } QString body = MiscHelpers::htmlToPlainText(draftMap.value("body").toString(), 50); m_previewLabel->setText("

" + title + "

" + body); } void DraftsManager::deleteSelectedDraft() { int selectedDraft = m_listWidget->currentRow(); if (selectedDraft != -1) { int confirm = QMessageBox::question(this, tr("Delete draft?"), tr("Are you sure you want to " "delete this draft?"), tr("&Yes, delete it"), tr("&No"), QString(), 1, 1); if (confirm != 0) { return; } QListWidgetItem *removedItem = m_listWidget->takeItem(selectedDraft); delete removedItem; // m_loadMenu's actions should be in sync, but don't delete blindly if (m_loadMenu->actions().count() > selectedDraft) // For last item, 1 > 0 { QAction *selectedAction = m_loadMenu->actions().at(selectedDraft); QVariantMap draftMap = selectedAction->data().toMap(); // If we're removing the draft currently being edited, detach from it... if (draftMap.value("id").toString() == m_currentDraftId) { m_currentDraftId.clear(); // by clearing draft ID } delete selectedAction; } if (m_listWidget->count() == 0) { m_deleteButton->setDisabled(true); m_previewLabel->clear(); m_loadMenu->setDisabled(true); } } saveDraftsToFile(); // TMP FIXME, should be scheduled } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////// PROTECTED //////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void DraftsManager::showEvent(QShowEvent *event) { emit windowShown(); if (m_listWidget->currentRow() == -1) { // Ensure something's selected m_listWidget->setCurrentRow(0); } event->accept(); } void DraftsManager::hideEvent(QHideEvent *event) { QSettings settings; if (settings.isWritable()) { settings.setValue("DraftsManager/draftsWindowSize", this->size()); } event->accept(); } dianara-v1.4.1/src/filtereditor.h0000664000175000017500000000461013210122430015027 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include #include "filterchecker.h" class FilterEditor : public QWidget { Q_OBJECT public: explicit FilterEditor(FilterChecker *filterChecker, QWidget *parent = 0); ~FilterEditor(); void loadFilters(); signals: public slots: void onFilterTextChanged(QString text); void onFilterRowChanged(int row); void addFilter(); void removeFilter(); void saveFilters(); protected: virtual void closeEvent(QCloseEvent *event); virtual void hideEvent(QHideEvent *event); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_topLayout; QGroupBox *m_newFilterGroupBox; QVBoxLayout *m_middleLayout; QGroupBox *m_currentFiltersGroupBox; QHBoxLayout *m_bottomLayout; QAction *m_closeAction; QLabel *m_explanationLabel; QComboBox *m_actionTypeComboBox; QComboBox *m_filterTypeComboBox; QLineEdit *m_filterWordsLineEdit; QPushButton *m_addFilterButton; QListWidget *m_filtersListWidget; QPushButton *m_removeFilterButton; QPushButton *m_saveButton; QPushButton *m_cancelButton; QString m_ruleTemplateString; FilterChecker *m_filterChecker; }; #endif // FILTEREDITOR_H dianara-v1.4.1/src/pumpcontroller.h0000644000175000017500000003260513205643266015446 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include #include #include #include #include #include #include #include #include #include #include /// For JSON parsing #include #include #include // For OAuth authentication #include #include "mischelpers.h" #include "asperson.h" #include "asobject.h" class PumpController : public QObject { Q_OBJECT public: enum requestTypes { NoRequest, ClientRegistrationRequest, TokenRequest, UserProfileRequest, UpdateProfileRequest, UpdateEmailRequest, FollowingListRequest, FollowersListRequest, ListsListRequest, SiteUserListRequest, CreatePersonListRequest, DeletePersonListRequest, PersonListRequest, AddMemberToListRequest, RemoveMemberFromListRequest, CreateGroupRequest, DeleteGroupRequest, JoinGroupRequest, LeaveGroupRequest, MainTimelineRequest, DirectTimelineRequest, ActivityTimelineRequest, FavoritesTimelineRequest, UserTimelineRequest, PostLikesRequest, PostCommentsRequest, PostSharesRequest, MinorFeedMainRequest, MinorFeedDirectRequest, MinorFeedActivityRequest, PublishPostRequest, LikePostRequest, CommentPostRequest, SharePostRequest, UnsharePostRequest, DeletePostRequest, UpdatePostRequest, UpdateCommentRequest, CheckContactRequest, FollowContactRequest, UnfollowContactRequest, AvatarRequest, ImageRequest, MediaRequest, UploadFileRequest, UploadMediaForPostRequest, UploadAvatarRequest, PublishAvatarRequest }; explicit PumpController(QObject *parent = 0); ~PumpController(); void setProxyConfig(QNetworkProxy::ProxyType proxyType, QString hostname, int port, bool useAuth, QString user, QString password); bool needsProxyPassword(); void setProxyPassword(QString password); void updateApiUrls(); void setPostsPerPageMain(int ppp); void setPostsPerPageOther(int ppp); void setNewUserId(QString userId); void setUserCredentials(QString userId); QString currentUserId(); QString currentUsername(); QString currentServerScheme(); QString currentServerDomain(); QString currentFollowersUrl(); int currentFollowingCount(); int currentFollowersCount(); bool currentlyAuthorized(); void getUserProfile(QString userId); void updateUserProfile(QString avatarUrl, QString fullName, QString hometown, QString bio); void updateUserEmail(QString newEmail, QString password); void enqueueAvatarForDownload(QString url); void enqueueImageForDownload(QString url); void getAvatar(QString avatarUrl); void getImage(QString imageUrl); QNetworkReply *getMedia(QString mediaUrl); void notifyAvatarStored(QString avatarUrl, QString avatarFilename); void notifyImageStored(QString imageUrl); void notifyImageFailed(QString imageUrl); void getContactList(QString listType, int offset=0); void getSiteUserList(); bool userInFollowing(QString contactId); void updateInternalFollowingIdList(QStringList idList); void removeFromInternalFollowingList(QString id); void getListsList(); void createPersonList(QString name, QString description); void deletePersonList(QString listId); void getPersonList(QString url); void addPersonToList(QString listId, QString personId); void removePersonFromList(QString listId, QString personId); void createGroup(QString name, QString summary, QString description); void joinGroup(QString id); void leaveGroup(QString id); void getPostLikes(QString postLikesUrl); void getPostComments(QString postCommentsUrl, QString postId); void getPostShares(QString postSharesUrl); void getFeed(requestTypes feedType, int itemCount, QString url = "", int feedOffset = 0); static QStringList getFeedNameAndPath(int feedType); QString getFeedApiUrl(int feedType); QNetworkRequest prepareRequest(QString url, QOAuth::HttpMethod method, requestTypes requestType, QOAuth::ParamMap paramMap = QOAuth::ParamMap(), QString contentTypeString="application/json"); QByteArray prepareJSON(QVariantMap jsonVariantMap); QVariantMap parseJSON(QByteArray rawData, bool *parsedOk); QNetworkReply *uploadFile(QString filename, QString contentType, requestTypes uploadType = UploadFileRequest); bool urlIsInOurHost(QString url); void addCommentUrlToSeenList(QString id, QString url); QString commentsUrlForPost(QString id); void showTransientMessage(QString message); void showStatusMessageAndLogIt(QString message, QString url=""); void showObjectSnippetAndLogIt(QString message, QVariantMap jsonMap, QString messageWhenTitled=""); void setIgnoreSslErrors(bool state); void setIgnoreSslInImages(bool state); void setNoHttpsMode(); void setSilentFollows(bool state); void setSilentLists(bool state); void setSilentLikes(bool state); void updatePostsEverSeen(QVariantMap postMap); QVariantMap getPostsEverSeen(); signals: void openingAuthorizeUrl(QUrl url, bool browserLaunched); void authorizationSucceeded(); void authorizationFailed(QString errorTitle, QString errorMessage); void authorizationStatusChanged(bool authorized); void initializationStepChanged(int step); void initializationCompleted(); void profileReceived(QString avatarURL, QString fullName, QString hometown, QString bio, QString email); void contactListReceived(QString listType, QVariantList contactList, int totalReceivedCount); void siteUserListReceived(QVariantList contactList, int totalItems); void contactVerified(QString userId, int httpCode, bool requestTimedOut, QString serverVersion); void contactFollowed(ASPerson *contact); void contactUnfollowed(ASPerson *contact); void cannotFollowNow(QString userId); void followingListChanged(); void listsListReceived(QVariantList listsList); void personListReceived(QVariantList personList, QString listUrl); void personAddedToList(QString id, QString name, QString avatarUrl); void personRemovedFromList(QString id); void mainTimelineReceived(QVariantList postList, QString previousLink, QString nextLink, int totalItems); void directTimelineReceived(QVariantList postList, QString previousLink, QString nextLink, int totalItems); void activityTimelineReceived(QVariantList postList, QString previousLink, QString nextLink, int totalItems); void favoritesTimelineReceived(QVariantList postList, QString previousLink, QString nextLink, int totalItems); void userTimelineReceived(QVariantList postList, QString previousLink, QString nextLink, int totalItems, QString url); void userTimelineFailed(); void timelineFailed(int requestType); void likesReceived(QVariantList likesList, QString originatingPostURL); void commentsReceived(QVariantList commentsList, QString originatingPostURL); void commentsNotReceived(QString requestedUrl); void minorFeedMainReceived(QVariantList activitiesList, QString previousLink, QString nextLink, int totalItemCount); void minorFeedDirectReceived(QVariantList activitiesList, QString previousLink, QString nextLink, int totalItemCount); void minorFeedActivityReceived(QVariantList activitiesList, QString previousLink, QString nextLink, int totalItemCount); void minorFeedFailed(int requestType); void avatarPictureReceived(QByteArray pictureData, QString pictureUrl); void imageReceived(QByteArray pictureData, QString pictureUrl); void imageFailed(QString imageUrl); void downloadCompleted(QString fileUrl); void downloadFailed(QString fileUrl); void avatarStored(QString avatarUrl, QString avatarFilename); void imageStored(QString imageUrl); void postPublished(); void postPublishingFailed(); void likeSet(); void commentPosted(QString parentPostId); void commentPostingFailed(QString parentPostId); void userDidSomething(); void avatarUploaded(QString url); void showErrorNotification(QString message); void currentJobChanged(QString message); void transientStatusBarMessage(QString message); void logMessage(QString message, QString url=""); public slots: void requestFinished(QNetworkReply *reply); void sslErrorsHandler(QNetworkReply *reply, QList errorList); void getToken(); void authorizeApplication(QString verifierCode); void getInitialData(); void postNote(QVariantMap audienceMap, QString postText, QString postTitle); QNetworkReply *postMedia(QVariantMap audienceMap, QString postText, QString postTitle, QString mediaFilename, QString mediaType, QString mimeContentType); void postMediaStepTwo(QString id); void postAvatarStepTwo(QString id); void updatePost(QString id, QString type, QString content, QString title); void likePost(QString postId, QString postType, QString authorId, bool like); void addComment(QString comment, QString postId, QString postType); void updateComment(QString id, QString content, QString inReplyToId); void sharePost(QString postId, QString postType); void unsharePost(QString postId, QString postType); void deletePost(QString postId, QString postType); void followContact(QString address); void followVerifiedContact(QString address); void unfollowContact(QString address); void onValidationTimeout(); private: QNetworkAccessManager m_nam; QByteArray m_userAgentString; // QOAuth-related QOAuth::Interface *m_qoauth; bool m_applicationAuthorized; QString m_clientId; QString m_clientSecret; QByteArray m_token; QByteArray m_tokenSecret; QString m_userId; // Full Webfinger address, user@host.tld QString m_userName; QString m_serverDomain; QString m_apiBaseUrl; QString m_apiFeedUrl; QString m_serverScheme; bool m_proxyUsesAuth; QString m_userFollowersUrl; int m_userFollowingCount; int m_userFollowersCount; QStringList m_followingIdList; int m_totalReceivedFollowing; int m_totalReceivedFollowers; QVariantMap m_postsEverSeen; QTimer *m_initialDataTimer; int m_initialDataStep; int m_initialDataAttempts; bool m_haveProfile; bool m_haveFollowing; bool m_haveFollowers; bool m_havePersonLists; bool m_haveMainTL; bool m_haveDirectTL; bool m_haveActivityTL; bool m_haveFavoritesTL; bool m_haveMainMF; bool m_haveDirectMF; bool m_haveActivityMF; // For multi-step operations in posts QString m_currentPostTitle; QString m_currentPostDescription; QVariantMap m_currentPostAudience; QString m_currentPostType; // For multi-step verify+follow operations QNetworkReply *m_webfingerCheckReply; QTimer *m_webfingerCheckTimer; bool m_webfingerCheckTimedOut; QString m_addressPendingToFollow; // Avatars / Images queue QStringList m_pendingAvatarsList; QStringList m_pendingImagesList; // Settings stuff int m_postsPerPageMain; // FIXME: get rid of these, query globalObject int m_postsPerPageOther; bool m_ignoreSslErrors; bool m_ignoreSslInImages; bool m_silentFollows; bool m_silentListsHandling; bool m_silentLikes; }; #endif // PUMPCONTROLLER_H dianara-v1.4.1/src/contactlist.h0000664000175000017500000000436513210107150014673 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "pumpcontroller.h" #include "globalobject.h" #include "contactcard.h" class ContactList : public QWidget { Q_OBJECT public: explicit ContactList(PumpController *pumpController, GlobalObject *globalObject, QString listType, QWidget *parent = 0); ~ContactList(); void clearListContents(); void setListContents(QVariantList contactList); QString getContactsStringForExport(); signals: void contactCountChanged(int difference); public slots: void filterList(QString filterText); void addSingleContact(ASPerson *contact); void removeSingleContact(ASPerson *contact); void focusFilterField(); private: QVBoxLayout *m_mainLayout; QVBoxLayout *m_contactsLayout; QHBoxLayout *m_filterLayout; QWidget *m_contactsWidget; QScrollArea *m_contactsScrollArea; QLabel *m_filterIcon; QLineEdit *m_filterLineEdit; QLabel *m_matchesCountLabel; QPushButton *m_clearFilterButton; QAction *m_focusFilterAction; QList m_contactsInList; bool m_isFollowing; PumpController *m_pumpController; GlobalObject *m_globalObject; }; #endif // CONTACTLIST_H dianara-v1.4.1/src/helpwidget.cpp0000644000175000017500000004304313216304632015037 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "helpwidget.h" HelpWidget::HelpWidget(QWidget *parent) : QWidget(parent) { this->setWindowTitle(tr("Basic Help") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("system-help")); this->setWindowFlags(Qt::Window); this->setMinimumSize(320, 240); QSettings settings; this->resize(settings.value("HelpWidget/helpWindowSize", QSize(640, 700)).toSize()); ///////////////////////////////////////////////////////////////// Help text QString helpText; // Table of contents const QString sectionStarting = tr("Getting started"); const QString sectionSettings = tr("Settings"); const QString sectionTimelines = tr("Timelines"); const QString sectionPosting = tr("Posting"); const QString sectionContacts = tr("Managing contacts"); const QString sectionKeyboard = tr("Keyboard controls"); const QString sectionCommandLine = tr("Command line options"); helpText.append("

" + tr("Contents") + "

" "" "
"); // Actual contents helpText.append("" "

" + sectionStarting + "

"); helpText.append(tr("The first time you start Dianara, you should see " "the Account Configuration dialog. There, enter " "your Pump.io address as name@server and press " "the Get Verifier Code button.") + "

"); helpText.append(tr("Then, your usual web browser should load the authorization " "page in your Pump.io server. There, you'll have to copy " "the full VERIFIER code, and paste it into Dianara's second field. " "Then press Authorize Application, and once it's confirmed, press " "Save Details.") + "

"); helpText.append(tr("At this point, your profile, contact lists and timelines " "will be loaded.") + "

"); helpText.append(tr("You should take a look at the Program Configuration window, " "under the Settings - Configure Dianara menu. There are " "several interesting options there.") + "

"); helpText.append(tr("Keep in mind that there are a lot of places in Dianara " "where you can get more information by hovering over some " "text or button with your mouse, and waiting for the " "tooltip to appear.") + "

"); helpText.append(tr("If you're new to Pump.io, take a look at this guide:") + " " + tr("Pump.io User Guide") + ""); helpText.append("

"); helpText.append("" "

" + sectionSettings + "

"); helpText.append(tr("You can configure several things to your liking in the " "settings, like the time interval between timeline " "updates, how many posts per page you want, highlight " "colors, notifications or how the system tray icon looks.") + "

"); helpText.append(tr("Here, you can also activate the option to always publish " "your posts as Public by default. You can always change " "that at the moment of posting.")); helpText.append("

"); helpText.append("" "

" + sectionTimelines + "

"); helpText.append(tr("There are seven timelines:") + "
    " + "
  • " + tr("The main timeline, where you'll see all the stuff " "posted or shared by the people you follow.") + "
  • " + "
  • " + tr("Messages timeline, where you'll see messages sent " "to you specifically. These messages might have been " "sent to other people too.") + "
  • " + "
  • " + tr("Activity timeline, where you'll see your own posts, " "or posts shared by you.") + "
  • " + "
  • " + tr("Favorites timeline, where you'll see the posts and " "comments you've liked. This can be used as a " "bookmark system.") + "
  • " + "
" + "
"); helpText.append(tr("The fifth timeline is the minor timeline, also known " "as the Meanwhile. This is visible on the left side, " "though it can be hidden. Here you'll see minor activities " "done by everyone you follow, such as comment actions, " "liking posts or following people.", "LEFT SIDE should change to RIGHT SIDE on RTL languages") + "
" + tr("The sixth and seventh timelines are also minor " "timelines, similar to the Meanwhile, but containing " "only activities directly addressed to you (Mentions) " "and activities done by you (Actions).") + "

" + tr("These activities might have a '+' button in them. " "Press it to open the post they're referencing. " "Also, as in many other places, you can hover with " "your mouse to see relevant information in the tooltip.") + "

" + tr("New messages appear highlighted in a different color. " "You can mark them as read just by clicking on any " "empty parts of the message.")); helpText.append("

"); helpText.append("" "

" + sectionPosting + "

"); helpText.append(tr("You can post notes by clicking in the text field at " "the top of the window or by pressing Control+N. " "Setting a title for your post is optional, but " "highly recommended, as it will help to " "better identify references to your post in " "the minor feed, e-mail notifications, etc.") + "

"); helpText.append(tr("It is possible to attach images, audio, video, and " "general files, like PDF documents, to your post.") + "
"); helpText.append("
" " " " " "" "
" "

"); helpText.append(tr("You can use the Format button to add formatting to " "your text, like bold or italics. Some of these options " "require text to be selected before they are used.") + "

"); helpText.append(tr("You can select who will see your post by using the " "To and Cc buttons.") + " " + tr("If you add a specific person to the 'To' list, they " "will receive your message in their direct messages tab.") + "
"); helpText.append(tr("You can also type '@' and the first characters of the " "name of a contact to bring up a popup menu with " "matching choices.") + " " + tr("Choose one with the arrow keys and press Enter to " "complete the name. This will add that person to the " "recipients list.") + "

"); helpText.append(tr("You can create private messages by adding specific " "people to these lists, and unselecting the Followers " "or the Public options.")); helpText.append("

"); helpText.append("" "

" + sectionContacts + "

"); helpText.append(tr("You can see the lists of people you follow, and who " "follow you from the Contacts tab.") + " " + tr("There, you can also manage person lists, used mainly " "to send posts to specific groups of people.") + " " + tr("There is a text field at the top, where you can " "directly enter addresses of new contacts to " "follow them.") + "

" + tr("Under the 'Neighbors' tab you'll see some resources " "to find people, and have the option to browse the " "latest registered users from your server directly.") + "


"); helpText.append(tr("You can find a list with some Pump.io users " "and other information here:") + "
" "" + tr("Users by language") + " - " + tr("Followers of Pump.io Community account") + ""); helpText.append("

"); helpText.append("" "

" // FIXME TMP + sectionKeyboard + "

"); helpText.append(tr("The most common actions found on the menus have " "keyboard shortcuts written next to them, like F5 " "or Control+N.") + "

" + tr("Besides that, you can use:") + "
    " + "
  • " + tr("Control+Up/Down/PgUp/PgDown/Home/End to move " "around the timeline.") + "
  • " + "
  • " + tr("Control+Left/Right to jump one page in the " "timeline.") + "
  • " + "
  • " + tr("Control+G to go to any page in the timeline " "directly.") + "
  • " + "
  • " + tr("Control+1/2/3 to switch between the minor " "feeds.") + "
  • " + "
  • " + tr("Control+Enter to post, when you're done composing " "a note or a comment. If the note is empty, you can " "cancel it by pressing ESC.") + "
  • " + "
  • " + tr("While composing a note, press Enter to jump from the " "title to the message body. Also, pressing the Up " "arrow while you're at the start of the message, jumps " "back to the title.") + "
  • " + "
  • " + tr("Control+Enter to finish creating a list of recipients " "for a post, in the 'To' or 'Cc' lists.") + "
  • " + "
"); helpText.append("

"); helpText.append("" "

" + sectionCommandLine + "

"); helpText.append(tr("You can use the --config parameter to run the program " "with a different configuration. This can be useful to " "use two or more different accounts. You can even run two " "instances of Dianara at the same time.") + "

"); helpText.append(tr("Use the --debug parameter to have extra information " "in your terminal window, about what the program is doing.") + "

"); helpText.append(tr("If your server does not support HTTPS, you can use the " "--nohttps parameter.") + "


"); helpText.append(tr("Dianara offers a D-Bus interface that allows some " "control from other applications.") + " " + tr("The interface is at %1, and you can " "access it with tools such as %2 or %3. It " "offers methods like %4 and %5.") .arg("org.nongnu.dianara") .arg("qdbus").arg("dbus-send") .arg("'toggle'").arg("'post'") + "
" + tr("If you use an alternate configuration, with " "something like '--config otherconf', then the " "interface will be at org.nongnu.dianara_otherconf.")); helpText.append("

"); ///////////////////////////////////////////////////////////////// Help text m_helpTextBrowser = new QTextBrowser(this); m_helpTextBrowser->setReadOnly(true); m_helpTextBrowser->setOpenExternalLinks(true); m_helpTextBrowser->setText(helpText); m_closeButton = new QPushButton(QIcon::fromTheme("window-close", QIcon(":/images/button-close.png")), tr("&Close"), this); connect(m_closeButton, &QAbstractButton::clicked, this, &QWidget::hide); QList closeShortcuts; closeShortcuts << QKeySequence(Qt::Key_Escape); closeShortcuts << QKeySequence(Qt::Key_F1); m_closeAction = new QAction(this); m_closeAction->setShortcuts(closeShortcuts); connect(m_closeAction, &QAction::triggered, this, &QWidget::hide); this->addAction(m_closeAction); m_layout = new QVBoxLayout(); m_layout->setContentsMargins(2, 2, 2, 2); m_layout->addWidget(m_helpTextBrowser); m_layout->addWidget(m_closeButton, 0, Qt::AlignRight); this->setLayout(m_layout); qDebug() << "HelpWidget created"; } HelpWidget::~HelpWidget() { qDebug() << "HelpWidget destroyed"; } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////// PROTECTED //////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void HelpWidget::closeEvent(QCloseEvent *event) { this->hide(); event->ignore(); } void HelpWidget::hideEvent(QHideEvent *event) { QSettings settings; if (settings.isWritable()) { settings.setValue("HelpWidget/helpWindowSize", this->size()); } event->accept(); } dianara-v1.4.1/src/userposts.cpp0000644000175000017500000001627613207024600014754 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "userposts.h" UserPosts::UserPosts(QString userId, QString userName, QIcon userAvatar, QString userOutbox, PumpController *pumpController, GlobalObject *globalObject, FilterChecker *filterChecker, QWidget *parent) : QWidget(parent) { m_pumpController = pumpController; m_globalObject = globalObject; connect(m_globalObject, &GlobalObject::programShuttingDown, this, &QWidget::close); this->setWindowFlags(Qt::Window); this->setWindowModality(Qt::NonModal); m_timelineTitle = tr("Posts by %1").arg(userName); this->setWindowTitle(m_timelineTitle + " - Dianara"); this->setWindowIcon(userAvatar); this->setMinimumSize(200, 300); QSettings settings; this->resize(settings.value("UserTimeline/userTimelineSize", QSize(560, 720)).toSize()); m_timeline = new TimeLine(PumpController::UserTimelineRequest, m_pumpController, m_globalObject, filterChecker, this); m_scrollArea = new QScrollArea(this); m_scrollArea->setContentsMargins(1, 1, 1, 1); m_scrollArea->setWidget(m_timeline); m_scrollArea->setWidgetResizable(true); m_scrollArea->setFrameStyle(QFrame::Box | QFrame::Raised); m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(m_timeline, &TimeLine::scrollTo, this, &UserPosts::scrollTimelineTo); m_infoLabel = new QLabel(tr("Loading..."), this); m_infoLabel->setTextFormat(Qt::RichText); m_closeButton = new QPushButton(QIcon::fromTheme("window-close", QIcon(":/images/button-close.png")), tr("&Close"), this); connect(m_closeButton, &QAbstractButton::clicked, this, &QWidget::close); // PumpController's connections connect(m_pumpController, &PumpController::userTimelineReceived, this, &UserPosts::fillTimeLine); connect(m_pumpController, &PumpController::userTimelineFailed, this, &UserPosts::onTimelineFailed); connect(m_pumpController, &PumpController::commentsReceived, m_timeline, &TimeLine::setCommentsInPost); connect(m_timeline, &TimeLine::timelineRendered, this, &UserPosts::notifyTimelineUpdate); // Initial load m_timeline->setCustomUrl(userOutbox); m_timeline->setDisabled(true); m_timeline->clearTimeLineContents(); // Hack to get comments properly resized // FIXME: On first load, should set showMessage=false so initial "Requesting..." is not erased // Not doing that for 1.3.1 due to problems m_pumpController->getFeed(PumpController::UserTimelineRequest, m_globalObject->getPostsPerPageMain(), userOutbox); // First part of message shown at the bottom m_userInfoString = userName + " - " + userId; // Layout m_bottomLayout = new QHBoxLayout(); m_bottomLayout->addWidget(m_infoLabel); m_bottomLayout->addStretch(1); m_bottomLayout->addWidget(m_closeButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->setContentsMargins(2, 2, 2, 2); m_mainLayout->addWidget(m_scrollArea); m_mainLayout->addSpacing(6); m_mainLayout->addLayout(m_bottomLayout); this->setLayout(m_mainLayout); qDebug() << "UserPosts timeline container created for" << userName; } UserPosts::~UserPosts() { qDebug() << "UserPosts timeline container destroyed"; } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////// SLOTS ////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// /* * Set contents of timeline only if the user timeline received matches this one. * Necessary when opening a user's timeline from another user's timeline * */ void UserPosts::fillTimeLine(QVariantList postList, QString previousLink, QString nextLink, int totalItems, QString url) { if (m_timelineUrl.isEmpty()) // Set it the first time { m_timelineUrl = url; } if (url == m_timelineUrl) { m_timeline->setTimeLineContents(postList, previousLink, nextLink, totalItems); } } void UserPosts::notifyTimelineUpdate() { const QString message = tr("Received '%1'.").arg(m_timelineTitle); m_globalObject->setStatusMessage(message); m_globalObject->logMessage(message); QString postCount = QLocale::system().toString(m_timeline->getTotalPosts()); m_infoLabel->setText(m_userInfoString + " — " //// Long dash + tr("%1 posts").arg(postCount)); m_timeline->resizePosts(QList(), true); // Resize all posts } // Set error messages if timeline fails to load void UserPosts::onTimelineFailed() { const QString message = tr("Error loading the timeline"); m_infoLabel->setText(message + "."); m_timeline->showMessage(message); } // React to Control+PgUp/PgDn, etc. sent from TimeLine() void UserPosts::scrollTimelineTo(QAbstractSlider::SliderAction sliderAction) { m_scrollArea->verticalScrollBar()->triggerAction(sliderAction); } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////////// PROTECTED //////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void UserPosts::resizeEvent(QResizeEvent *event) { m_timeline->resizePosts(QList(), true); // resizeAll=true event->accept(); } void UserPosts::closeEvent(QCloseEvent *event) { QSettings settings; if (settings.isWritable()) { settings.setValue("UserTimeline/userTimelineSize", this->size()); } delete m_timeline; this->deleteLater(); this->hide(); event->ignore(); } void UserPosts::keyPressEvent(QKeyEvent *event) { // ESC to close, if there are no comments in progress if (event->key() == Qt::Key_Escape && !m_timeline->commentingOnAnyPost()) { event->accept(); this->close(); } else { event->ignore(); } } dianara-v1.4.1/src/audienceselector.cpp0000664000175000017500000004116413216304071016222 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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; this->m_pumpController = pumpController; QString titlePart; if (this->selectorType == "to") { titlePart = tr("'To' List"); } else { titlePart = tr("'Cc' List"); } this->setWindowTitle(titlePart + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("system-users", QIcon(":/images/button-users.png"))); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::WindowModal); this->setMinimumSize(440, 340); QSettings settings; QSize savedWindowsize = settings.value("AudienceSelector/" "audienceWindowSize").toSize(); if (savedWindowsize.isValid()) { this->resize(savedWindowsize); } // Upper-left side, all contacts, with filter this->peopleWidget = new PeopleWidget(tr("&Add to Selected") + " >>", PeopleWidget::EmbeddedWidget, m_pumpController, this); connect(peopleWidget, &PeopleWidget::contactSelected, this, &AudienceSelector::copyToSelected); connect(peopleWidget, &PeopleWidget::addButtonPressed, this, &AudienceSelector::copyToSelected); this->allGroupboxLayout = new QVBoxLayout(); allGroupboxLayout->setContentsMargins(0, 0, 0, 0); allGroupboxLayout->addWidget(peopleWidget); allContactsGroupbox = new QGroupBox(tr("All Contacts"), this); allContactsGroupbox->setLayout(allGroupboxLayout); // Upper-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.", "ON THE LEFT should change to ON THE " "RIGHT in RTL languages"), this); explanationLabel->setWordWrap(true); selectedListWidget = new QListWidget(this); selectedListWidget->setDragDropMode(QListView::DragDrop); selectedListWidget->setDefaultDropAction(Qt::MoveAction); selectedListWidget->setSelectionMode(QListView::ExtendedSelection); selectedListWidget->setIconSize(QSize(48, 48)); this->clearSelectedListButton = new QPushButton(QIcon::fromTheme("edit-clear-list", QIcon(":/images/button-delete.png")), tr("Clear &List"), this); connect(clearSelectedListButton, &QAbstractButton::clicked, selectedListWidget, &QListWidget::clear); selectedGroupboxLayout = new QVBoxLayout(); selectedGroupboxLayout->addWidget(explanationLabel); selectedGroupboxLayout->addSpacing(8); selectedGroupboxLayout->addWidget(selectedListWidget); selectedGroupboxLayout->addWidget(clearSelectedListButton, 0, Qt::AlignLeft); this->selectedListGroupbox = new QGroupBox(tr("Selected People"), this); selectedListGroupbox->setLayout(selectedGroupboxLayout); this->upperLayout = new QHBoxLayout(); upperLayout->addWidget(allContactsGroupbox, 3); upperLayout->addWidget(selectedListGroupbox, 4); // Lower part doneButton = new QPushButton(QIcon::fromTheme("dialog-ok", QIcon(":/images/button-save.png")), tr("&Done"), this); connect(doneButton, &QAbstractButton::clicked, this, &AudienceSelector::setAudience); cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("&Cancel"), this); connect(cancelButton, &QAbstractButton::clicked, this, &QWidget::close); // Layout buttonsLayout = new QHBoxLayout(); buttonsLayout->setAlignment(Qt::AlignRight); buttonsLayout->addWidget(doneButton); buttonsLayout->addWidget(cancelButton); this->mainLayout = new QVBoxLayout(); mainLayout->addLayout(upperLayout); mainLayout->addLayout(buttonsLayout); this->setLayout(mainLayout); // Ctrl+Enter is the same as the "Done" button doneAction = new QAction(this); QList doneShortcuts; doneShortcuts << QKeySequence("Ctrl+Return") << QKeySequence("Ctrl+Enter"); doneAction->setShortcuts(doneShortcuts); connect(doneAction, &QAction::triggered, this, &AudienceSelector::setAudience); this->addAction(doneAction); // ESC is the same as the "Cancel" button cancelAction = new QAction(this); cancelAction->setShortcut(Qt::Key_Escape); connect(cancelAction, &QAction::triggered, this, &QWidget::close); this->addAction(cancelAction); ///////////////////////////////////////////////////////////////////////////// // Setup To/Cc menu to be shown on the Publisher m_publicAction = new QAction(tr("Public"), this); m_publicAction->setCheckable(true); connect(m_publicAction, &QAction::toggled, this, &AudienceSelector::onPublicToggled); m_followersAction = new QAction(tr("Followers"), this); m_followersAction->setCheckable(true); connect(m_followersAction, &QAction::toggled, this, &AudienceSelector::onFollowersToggled); m_listsMenu = new QMenu(tr("Lists"), this); m_listsMenu->setDisabled(true); // Disabled until lists are received, if any connect(m_listsMenu, &QMenu::triggered, this, &AudienceSelector::onListToggled); m_selectorMenu = new QMenu(QStringLiteral("to-menu"), this); m_selectorMenu->addAction(m_publicAction); m_selectorMenu->addAction(m_followersAction); m_selectorMenu->addMenu(m_listsMenu); m_selectorMenu->addSeparator(); m_selectorMenu->addAction(tr("People..."), this, SLOT(show())); qDebug() << "AudienceSelector created" << titlePart; } AudienceSelector::~AudienceSelector() { qDebug() << "AudienceSelector destroyed"; } /* * Reset lists and widgets to default status * */ void AudienceSelector::resetLists() { this->peopleWidget->resetWidget(); this->selectedListWidget->clear(); restoreSelected(); } void AudienceSelector::deletePrevious() { foreach (QListWidgetItem *item, previousItems) { delete item; } previousItems.clear(); } void AudienceSelector::saveSelected() { qDebug() << "AudienceSelector::saveSelected()"; // Clear and delete all first this->deletePrevious(); int totalItems = this->selectedListWidget->count(); for (int counter = 0; counter < totalItems; ++counter) { this->previousItems.append(selectedListWidget->item(counter)->clone()); } } void AudienceSelector::restoreSelected() { qDebug() << "AudienceSelector::restoreSelected()"; foreach (QListWidgetItem *item, this->previousItems) { this->selectedListWidget->addItem(item->clone()); } } QMenu *AudienceSelector::getSelectorMenu() { return m_selectorMenu; } void AudienceSelector::setDefaultAudience(bool toPublic) { // Check "public" if "public posts" is set in the preferences m_publicAction->setChecked(toPublic); // Cc: Followers by default m_followersAction->setChecked(this->selectorType == QStringLiteral("cc")); // Uncheck the person lists foreach (QAction *action, m_listsMenu->actions()) { action->setChecked(false); } // Clear individual recipients deletePrevious(); resetLists(); } void AudienceSelector::clearPublicAndFollowers() { setPublic(false); setFollowers(false); } void AudienceSelector::setPublic(bool state) { m_publicAction->setChecked(state); } bool AudienceSelector::isPublicSelected() { return m_publicAction->isChecked(); } void AudienceSelector::setFollowers(bool state) { m_followersAction->setChecked(state); } bool AudienceSelector::isFollowersSelected() { return m_followersAction->isChecked(); } void AudienceSelector::setListsMenu(QVariantList newLists) { // First, clear the menu m_listsMenu->clear(); // clear() should delete the existing actions if (newLists.length() > 0) // If there are some lists, enable the menu { m_listsMenu->setEnabled(true); } foreach (QVariant list, newLists) { const QVariantMap listMap = list.toMap(); QAction *listAction = new QAction(listMap.value("displayName").toString(), this); listAction->setCheckable(true); listAction->setData(listMap.value("id")); m_listsMenu->addAction(listAction); } } void AudienceSelector::checkListWithId(QString id) { foreach (QAction *listAction, m_listsMenu->actions()) { if (listAction->data().toString() == id) { qDebug() << "Checking matching list:" << listAction->text(); listAction->setChecked(true); emit audienceChanged(); break; } } } QString AudienceSelector::updatedAudienceLabels() { QString audienceString; int individualsCount = this->selectedListWidget->count(); for (int counter=0; counter < individualsCount; ++counter) { QListWidgetItem *item = selectedListWidget->item(counter); audienceString.append("data(Qt::UserRole + 3).toString() + "\">" + item->data(Qt::UserRole + 1).toString() + ", "); } if (!audienceString.isEmpty()) { audienceString.remove(-2, 2); // Remove last comma audienceString.append(QStringLiteral("
")); } // Person lists foreach (QAction *action, m_listsMenu->actions()) { if (action->isChecked()) { audienceString.append(QString::fromUtf8("\342\236\224 ") // arrow sign in front + action->text() + "
"); } } // Public if (m_publicAction->isChecked()) { audienceString.append("+" + tr("Public") + "
"); } // Followers if (m_followersAction->isChecked()) { audienceString.append("+" + tr("Followers") + "
"); } return audienceString; } /* * Create an array of key:value maps, listing who will receive a post, like: * * { * "objectType": "collection", * "id": "http://activityschema.org/collection/public" * } * * { * "objectType": "group", * "id": "https://pump.example/api/user/group/someGroupId123" * } * * { * "objectType": "person", * "id": "acct:somecontact@pumpserver.example" * } * */ QVariantList AudienceSelector::getAudienceList(bool *onlyToFollowers) { QVariantList audienceList; QVariantMap audienceItemMap; // Public is checked if (isPublicSelected()) { audienceItemMap.clear(); audienceItemMap.insert("objectType", "collection"); audienceItemMap.insert("id", "http://activityschema.org/collection/public"); audienceList.append(audienceItemMap); *onlyToFollowers = false; } // Followers is checked if (isFollowersSelected()) { audienceItemMap.clear(); audienceItemMap.insert("objectType", "collection"); audienceItemMap.insert("id", m_pumpController->currentFollowersUrl()); audienceList.append(audienceItemMap); } // Individual people const int individualsCount = this->selectedListWidget->count(); for (int counter=0; counter < individualsCount; ++counter) { QListWidgetItem *item = selectedListWidget->item(counter); audienceItemMap.clear(); audienceItemMap.insert("objectType", "person"); audienceItemMap.insert("id", "acct:" + item->data(Qt::UserRole + 2).toString()); audienceList.append(audienceItemMap); *onlyToFollowers = false; } // Lists foreach (QAction *listAction, m_listsMenu->actions()) { if (listAction->isChecked()) { audienceItemMap.clear(); audienceItemMap.insert("objectType", "collection"); audienceItemMap.insert("id", listAction->data()); audienceList.append(audienceItemMap); *onlyToFollowers = false; } } // Groups -- FIXME -- TODO return audienceList; } int AudienceSelector::getRecipientsCount() { return this->selectedListWidget->count(); } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// SLOTS ////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// /* * Copy a contact to the list of Selected * * The contact string comes in a SIGNAL from PeopleWidget * */ void AudienceSelector::copyToSelected(QIcon contactIcon, QString contactString, QString contactName, QString contactId, QString contactUrl) { if (!contactString.isEmpty()) { int itemExists = selectedListWidget->findItems(contactString, Qt::MatchExactly).size(); if (itemExists == 0) { QListWidgetItem *item = new QListWidgetItem(contactIcon, contactString); item->setData(Qt::UserRole + 1, contactName); item->setData(Qt::UserRole + 2, contactId); item->setData(Qt::UserRole + 3, contactUrl); this->selectedListWidget->addItem(item); } else { qDebug() << "AudienceSelector::copyToSelected() " "ignoring already added recipient"; } } } /* * The "Done" button: emit signal with list of selected people * */ void AudienceSelector::setAudience() { saveSelected(); // To restore the list later, if the dialog is shown again emit audienceChanged(); this->hide(); // Don't close(), because that resets the lists =) } void AudienceSelector::onListToggled(QAction *listAction) { qDebug() << "List checked:" << listAction->isChecked() << this->selectorType << listAction->text(); emit audienceChanged(); } void AudienceSelector::onPublicToggled(bool checked) { emit audienceChanged(); if (checked) { emit publicSelected(); } } void AudienceSelector::onFollowersToggled(bool checked) { emit audienceChanged(); if (checked) { emit followersSelected(); } } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////// PROTECTED //////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void AudienceSelector::closeEvent(QCloseEvent *event) { this->resetLists(); this->hide(); event->accept(); } void AudienceSelector::hideEvent(QHideEvent *event) { QSettings settings; if (settings.isWritable()) { settings.setValue("AudienceSelector/audienceWindowSize", this->size()); } event->accept(); } dianara-v1.4.1/src/avatarbutton.h0000664000175000017500000000463613206616406015075 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 AVATARBUTTON_H #define AVATARBUTTON_H #include #include #include #include #include #include "asperson.h" #include "pumpcontroller.h" #include "globalobject.h" #include "mischelpers.h" class AvatarButton : public QToolButton { Q_OBJECT public: explicit AvatarButton(ASPerson *person, PumpController *pumpController, GlobalObject *globalObject, QSize avatarSize, QWidget *parent = 0); ~AvatarButton(); void setGenericAvatarIcon(); void updateAvatarIcon(QString filename); void createAvatarMenu(); void syncFollowState(bool firstTime=false); void setFollowUnfollow(); void addSeparatorToMenu(); void addActionToMenu(QAction *action); signals: public slots: void openAuthorProfileInBrowser(); void followUser(); void unfollowUser(); void sendMessageToUser(); void browseUserMessages(); void redrawAvatar(QString avatarUrl, QString avatarFilename); private: QMenu *m_avatarMenu; QAction *m_avatarMenuIdAction; QAction *m_avatarMenuProfileAction; QAction *m_avatarMenuFollowAction; QAction *m_avatarMenuMessageAction; QAction *m_avatarMenuBrowseAction; int m_iconWidth; QString m_authorId; QString m_authorName; QString m_authorUrl; QString m_authorAvatarUrl; QString m_authorOutbox; bool m_authorFollowed; PumpController *m_pumpController; GlobalObject *m_globalObject; }; #endif // AVATARBUTTON_H dianara-v1.4.1/src/configdialog.h0000644000175000017500000001414313133757141015001 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include #include #include #include #include #include #include #include #include "globalobject.h" #include "fontpicker.h" #include "colorpicker.h" #include "notifications.h" #include "proxydialog.h" class ConfigDialog : public QWidget { Q_OBJECT public: ConfigDialog(GlobalObject *globalObject, QString dataDirectory, int updateInterval, int tabsPosition, bool tabsMovable, FDNotifications *notifier, QWidget *parent); ~ConfigDialog(); void createGeneralPage(int updateInterval, int tabsPosition, bool tabsMovable); void createFontsPage(); void createColorsPage(); void createTimelinesPage(); void createPostsPage(); void createComposerPage(); void createPrivacyPage(); void createNotificationsPage(QSettings *settings); void createSystrayPage(QSettings *settings); QComboBox *newAvatarComboBox(); void syncNotifierOptions(); QString checkNotifications(int notificationStyle); void setPublicPosts(bool value); signals: void configurationChanged(); void filterEditorRequested(); public slots: void saveConfiguration(); void pickCustomIconFile(); void showDemoNotification(int notificationStyle); void toggleNotificationDetails(int currentOption); void toggleCustomIconButton(int currentOption); protected: virtual void closeEvent(QCloseEvent *event); virtual void hideEvent(QHideEvent *event); private: QVBoxLayout *mainLayout; QHBoxLayout *topLayout; QListWidget *categoriesListWidget; QStackedWidget *categoriesStackedWidget; // Page 1, general options QWidget *generalOptionsWidget; QFormLayout *generalOptionsLayout; QSpinBox *updateIntervalSpinbox; QComboBox *tabsPositionCombobox; QCheckBox *tabsMovableCheckbox; QPushButton *proxyConfigButton; ProxyDialog *proxyDialog; QPushButton *filterEditorButton; // Page 2, fonts QWidget *fontOptionsWidget; QVBoxLayout *fontOptionsLayout; FontPicker *fontPicker1; FontPicker *fontPicker2; FontPicker *fontPicker3; FontPicker *fontPicker4; // Page 3, colors QWidget *colorOptionsWidget; QVBoxLayout *colorOptionsLayout; ColorPicker *colorPicker1; ColorPicker *colorPicker2; ColorPicker *colorPicker3; ColorPicker *colorPicker4; ColorPicker *colorPicker5; ColorPicker *colorPicker6; // Page 4, timelines options QWidget *timelinesOptionsWidget; QFormLayout *timelinesOptionsLayout; QSpinBox *postsPerPageMainSpinbox; QSpinBox *postsPerPageOtherSpinbox; QCheckBox *showDeletedCheckbox; QCheckBox *hideDuplicatesCheckbox; QCheckBox *jumpToNewCheckbox; QComboBox *minorFeedSnippetsCombobox; QSpinBox *snippetLimitSpinbox; QSpinBox *snippetLimitHlSpinbox; QComboBox *mfAvatarSizeCombobox; QComboBox *mfIconTypeCombobox; // Page 5, posts options QWidget *postsOptionsWidget; QFormLayout *postsOptionsLayout; QComboBox *postAvatarSizeCombobox; QComboBox *commentAvatarSizeCombobox; QCheckBox *showExtendedSharesCheckbox; QCheckBox *showExtraInfoCheckbox; QCheckBox *postHLAuthorCommentsCheckbox; QCheckBox *postHLOwnCommentsCheckbox; QCheckBox *postIgnoreSslInImages; QCheckBox *postFullImagesCheckbox; // Page 6, composer options QWidget *composerOptionsWidget; QFormLayout *composerOptionsLayout; QCheckBox *publicPostsCheckbox; QCheckBox *useFilenameAsTitleCheckbox; QCheckBox *showCharacterCounterCheckbox; // Page 7, privacy options QWidget *privacyOptionsWidget; QFormLayout *privacyOptionsLayout; QCheckBox *silentFollowsCheckbox; QCheckBox *silentListsCheckbox; QCheckBox *silentLikesCheckbox; // Page 8, notifications options QWidget *notificationOptionsWidget; QFormLayout *notificationOptionsLayout; QComboBox *notificationStyleCombobox; QSpinBox *notificationDurationSpinbox; QCheckBox *notificationPersistentCheckbox; QCheckBox *notificationTaskbarCheckbox; QLabel *notificationsStatusLabel; QCheckBox *notifyNewTLCheckbox; QCheckBox *notifyHLTLCheckbox; QCheckBox *notifyNewMWCheckbox; QCheckBox *notifyHLMWCheckbox; QCheckBox *notifyErrorsCheckbox; // Page 9, system tray options QWidget *systrayOptionsWidget; QFormLayout *systrayOptionsLayout; QComboBox *systrayIconTypeCombobox; QPushButton *systrayCustomIconButton; QString systrayCustomIconFN; QString systrayIconLastUsedDir; QCheckBox *systrayHideCheckbox; // Widgets below the tab widget QLabel *dataDirectoryLabel; QHBoxLayout *buttonsLayout; QPushButton *saveConfigButton; QPushButton *cancelButton; QAction *closeAction; FDNotifications *fdNotifier; GlobalObject *globalObj; }; #endif // CONFIGDIALOG_H dianara-v1.4.1/src/contactcard.h0000664000175000017500000000474413207640706014651 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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" #include "globalobject.h" #include "asperson.h" #include "timestamp.h" class ContactCard : public QFrame { Q_OBJECT public: ContactCard(PumpController *pumpController, GlobalObject *globalObject, ASPerson *asPerson, QWidget *parent = 0); ~ContactCard(); void setButtonToFollow(); void setButtonToUnfollow(); bool setAvatar(QString avatarFilename); QString getNameAndIdString(); QString getId(); signals: public slots: void followContact(); void unfollowContact(); void openProfileInBrowser(); void setMessagingModeForContact(); void browseContactPosts(); void redrawAvatar(QString avatarUrl, QString avatarFilename); private: QHBoxLayout *m_mainLayout; QVBoxLayout *m_rightLayout; QLabel *m_avatarLabel; QLabel *m_userInfoLabel; QPushButton *m_followButton; QPushButton *m_optionsButton; QMenu *m_optionsMenu; QAction *m_openProfileAction; QAction *m_sendMessageAction; QAction *m_browsePostsAction; //QMenu *m_addToListMenu; QString m_contactName; QString m_contactId; QString m_contactUrl; QString m_contactAvatarUrl; QString m_contactOutbox; PumpController *m_pumpController; GlobalObject *m_globalObject; }; #endif // CONTACTCARD_H dianara-v1.4.1/src/minorfeed.cpp0000644000175000017500000005413013221241162014644 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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::requestTypes minorFeedType, PumpController *pumpController, GlobalObject *globalObject, FilterChecker *filterChecker, QWidget *parent) : QFrame(parent) { m_feedType = minorFeedType; m_pumpController = pumpController; connect(m_pumpController, &PumpController::minorFeedFailed, this, &MinorFeed::onUpdateFailed); m_globalObject = globalObject; m_filterChecker = filterChecker; this->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); this->setContentsMargins(0, 0, 0, 0); // Layout for the items m_itemsLayout = new QVBoxLayout(); m_itemsLayout->setContentsMargins(0, 0, 0, 0); m_itemsLayout->setSpacing(1); // Very small spacing // Separator frame, to mark where new items end m_separatorFrame = new QFrame(this); m_separatorFrame->setFrameStyle(QFrame::HLine); m_separatorFrame->setMinimumHeight(28); m_separatorFrame->setContentsMargins(0, 8, 0, 8); m_separatorFrame->hide(); // Button to get newer items (pending stuff) m_getPendingButton = new QPushButton(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), QStringLiteral("*get pending activities*"), this); m_getPendingButton->setFlat(true); connect(m_getPendingButton, &QAbstractButton::clicked, this, &MinorFeed::updateFeed); m_getPendingButton->hide(); // Button to get more items (older stuff) m_getOlderButton = new QPushButton(QIcon::fromTheme("list-add", QIcon(":/images/list-add.png")), tr("Older Activities"), this); m_getOlderButton->setFlat(true); m_getOlderButton->setToolTip("" + tr("Get previous minor activities")); connect(m_getOlderButton, &QAbstractButton::clicked, this, &MinorFeed::getMoreActivities); // Set disabled initially; will be enabled when contents are set m_getOlderButton->setDisabled(true); // Main layout m_mainLayout = new QVBoxLayout(); m_mainLayout->setContentsMargins(1, 1, 1, 1); m_mainLayout->addWidget(m_getPendingButton); m_mainLayout->addLayout(m_itemsLayout, 1); m_mainLayout->addSpacing(8); m_mainLayout->addStretch(); m_mainLayout->addWidget(m_getOlderButton); this->setLayout(m_mainLayout); // Demo activity, stating that there's nothing to show QVariantMap demoGenerator; demoGenerator.insert("displayName", QStringLiteral("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, QString(), this); MinorFeedItem *demoFeedItem = new MinorFeedItem(demoActivity, false, // Not highlighted by filter m_pumpController, m_globalObject, this); demoFeedItem->setItemAsNew(false, false); // Do not inform the feed m_itemsLayout->addWidget(demoFeedItem); m_itemsInFeed.append(demoFeedItem); m_firstLoad = true; m_gettingNew = true; // First time should be true m_unreadItemsCount = 0; m_highlightedItemsCount = 0; m_pendingToReceiveNextTime = 0; // Remember what was the newest activity last time QSettings settings; settings.beginGroup("MinorFeedStates"); switch (m_feedType) { case PumpController::MinorFeedMainRequest: m_previousNewestActivityId = settings.value("previousNewestItemIdMain") .toString(); m_fullFeedItemCount = settings.value("totalItemsMain").toInt(); m_feedBatchItemCount = 50; break; case PumpController::MinorFeedDirectRequest: m_previousNewestActivityId = settings.value("previousNewestItemIdDirect") .toString(); m_fullFeedItemCount = settings.value("totalItemsDirect").toInt(); m_feedBatchItemCount = 20; break; case PumpController::MinorFeedActivityRequest: m_previousNewestActivityId = settings.value("previousNewestItemIdActivity") .toString(); m_fullFeedItemCount = settings.value("totalItemsActivity").toInt(); m_feedBatchItemCount = 20; break; default: qDebug() << "MinorFeed created with wrong type:" << m_feedType; } settings.endGroup(); // Sync all avatar's follow state when there are changes in the Following list connect(m_pumpController, &PumpController::followingListChanged, this, &MinorFeed::updateAvatarFollowStates); qDebug() << "MinorFeed created"; } MinorFeed::~MinorFeed() { QSettings settings; settings.beginGroup("MinorFeedStates"); switch (m_feedType) { case PumpController::MinorFeedMainRequest: settings.setValue("previousNewestItemIdMain", m_previousNewestActivityId); settings.setValue("totalItemsMain", m_fullFeedItemCount); break; case PumpController::MinorFeedDirectRequest: settings.setValue("previousNewestItemIdDirect", m_previousNewestActivityId); settings.setValue("totalItemsDirect", m_fullFeedItemCount); break; case PumpController::MinorFeedActivityRequest: settings.setValue("previousNewestItemIdActivity", m_previousNewestActivityId); settings.setValue("totalItemsActivity", m_fullFeedItemCount); break; default: qDebug() << "MinorFeed destructor: feed type was wrong"; } settings.endGroup(); qDebug() << "MinorFeed destroyed; Type:" << m_feedType; } void MinorFeed::clearContents() { qDebug() << "MinorFeed::clearContents()"; foreach (MinorFeedItem *feedItem, m_itemsInFeed) { m_itemsLayout->removeWidget(feedItem); delete feedItem; } m_itemsInFeed.clear(); m_itemsLayout->removeWidget(m_separatorFrame); m_separatorFrame->hide(); } /* * Remove oldest items, to avoid ever-increasing memory usage * Called after updating the feed, only when getting newer items * * At the very least, keep as many items as were received in last update * */ void MinorFeed::removeOldItems(int minimumToKeep) { int maxItems = qMax(m_feedBatchItemCount * 2, // TMP FIXME minimumToKeep); if (m_itemsInFeed.count() <= maxItems) { // Not too many items yet return; } int itemCounter = 0; foreach (MinorFeedItem *feedItem, m_itemsInFeed) { if (itemCounter >= maxItems) { if (!feedItem->isNew()) // Don't remove if it's unread (optional?) { m_itemsLayout->removeWidget(feedItem); m_itemsInFeed.removeOne(feedItem); delete feedItem; } } ++itemCounter; } // Update "next" link manually, based on the last item present in the feed QByteArray lastItemId = m_itemsInFeed.last()->getActivityId().toLocal8Bit(); lastItemId = lastItemId.toPercentEncoding(); // Needs to be percent-encoded m_nextLink = m_pumpController->getFeedApiUrl(m_feedType) + "?before=" + lastItemId; } void MinorFeed::insertSeparator(int position) { m_itemsLayout->insertWidget(position, m_separatorFrame); m_separatorFrame->show(); } void MinorFeed::markAllAsRead() { foreach (MinorFeedItem *feedItem, m_itemsInFeed) { feedItem->setItemAsNew(false, // Mark as not new false); // Don't inform the feed } m_unreadItemsCount = 0; m_highlightedItemsCount = 0; } /* * Update fuzzy timestamps in all items * */ void MinorFeed::updateFuzzyTimestamps() { foreach (MinorFeedItem *feedItem, m_itemsInFeed) { feedItem->setFuzzyTimeStamp(); } } void MinorFeed::syncActivityWithTimelines(ASActivity *activity) { //////////////////////////////////////////////////////////// An edited post if (activity->getVerb() == "update") { if (ASObject::canDisplayObject(activity->object()->getType())) { emit objectUpdated(activity->object()); } } //////////////////////////////////////////// A new comment posted to a post if (activity->getVerb() == "post") { ASObject *activityObject = activity->object(); // Check if the object has proper author info; if not, use activity's author if (activityObject->author()->getId().isEmpty()) { activityObject->updateAuthorFromPerson(activity->author()); } // FIXME: this needs some checking... // v1.3.0b1+11: seems OK emit objectReplyAdded(activityObject); } ////////////////////////////////////////////////////////////// A liked post if (activity->getVerb() == "favorite" || activity->getVerb() == "like") { // FIXME: handle liking of comments (1.4.x) if (ASObject::canDisplayObject(activity->object()->getType())) { emit objectLiked(activity->object()->getId(), activity->object()->getType(), activity->author()->getId(), activity->author()->getNameWithFallback(), activity->author()->getUrl()); } } /////////////////////////////////////////////////////////// An unliked post if (activity->getVerb() == "unfavorite" || activity->getVerb() == "unlike") { // FIXME: handle unliking of comments (1.4.x) if (ASObject::canDisplayObject(activity->object()->getType())) { emit objectUnliked(activity->object()->getId(), activity->object()->getType(), activity->author()->getId()); } } //////////////////////////////////////////////////////////// A deleted post if (activity->getVerb() == "delete") { if (ASObject::canDisplayObject(activity->object()->getType())) { emit objectDeleted(activity->object()); } } // Sync "follow" and "stop-following" } /******************************************************************************/ /******************************************************************************/ /********************************** SLOTS *************************************/ /******************************************************************************/ /******************************************************************************/ /* * Get the latest activities * */ void MinorFeed::updateFeed() { m_gettingNew = true; m_getOlderButton->setDisabled(true); m_pumpController->getFeed(m_feedType, 200, // Maximum item count allowed by API m_prevLink); } /* * Get additional older activities * */ void MinorFeed::getMoreActivities() { m_gettingNew = false; m_getOlderButton->setDisabled(true); m_pumpController->getFeed(m_feedType, m_feedBatchItemCount, m_nextLink); } void MinorFeed::setFeedContents(QVariantList activitiesList, QString previous, QString next, int totalItemCount) { if (m_firstLoad) { m_prevLink = previous; m_nextLink = next; this->clearContents(); } else { if (m_gettingNew) { if (!previous.isEmpty()) { m_prevLink = previous; } } else { if (!next.isEmpty()) { m_nextLink = next; } } } qDebug() << "Current MinorFeed prev/next links:" << m_prevLink << m_nextLink; const int activitiesListSize = activitiesList.size(); int totalItemDifference = -1; // TMP FIXME; for cases where it's unknown if (m_gettingNew) { // Check how many new items we should we expecting totalItemDifference = totalItemCount - m_fullFeedItemCount; m_fullFeedItemCount = totalItemCount; qDebug() << "MinorFeed::setFeedContents(); Should load" << totalItemDifference << "items this time ###"; // Check how many more items need to be received, if more than max are pending m_pendingToReceiveNextTime += totalItemDifference; m_pendingToReceiveNextTime -= activitiesListSize; if (m_pendingToReceiveNextTime > 0) { if (m_firstLoad) { // The difference in pending items is in the older activities, so doesn't count m_pendingToReceiveNextTime = 0; // TODO: On first load, could display the "pending" number at the "older" button } else { // Button at the top to fetch the pending activities, even more new stuff m_getPendingButton->setText(tr("Get %1 newer", "As in: Get 3 newer (activities)") .arg(m_pendingToReceiveNextTime)); m_getPendingButton->show(); } } else { m_pendingToReceiveNextTime = 0; // In case it was less than 0 m_getPendingButton->hide(); } // Hide "Get newer" button also if for some reason it's present and we if (activitiesListSize == 0) // confirm there's nothing else to receive { m_pendingToReceiveNextTime = 0; m_getPendingButton->hide(); } } // Remove the separator line m_itemsLayout->removeWidget(m_separatorFrame); m_separatorFrame->hide(); int newItemsCount = 0; int newHighlightedItemsCount = 0; int newFilteredItemsCount = 0; bool itemIsNew; bool itemHighlightedByFilter; 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 int insertedItemsCount = 0; bool needToInsertSeparator = false; QList activitiesToSync; QList allProcessedActivities; foreach (QVariant activityVariant, activitiesList) { itemIsNew = false; ASActivity *activity = new ASActivity(activityVariant.toMap(), m_pumpController->currentUserId(), this); allProcessedActivities.append(activity); const int filtered = m_filterChecker->validateActivity(activity); // If there is no reason to filter out the item, add it to the feed if (filtered != FilterChecker::FilterOut) { // Store activities in reverse order, for later processing (sync with TL) activitiesToSync.prepend(activity); // Determine which activities are new if (newestActivityId.isEmpty()) // Only first time, for newest item { if (m_gettingNew) { newestActivityId = activity->getId(); } else { newestActivityId = m_previousNewestActivityId; allNewItemsCounted = true; } } // Determine if this item is new, or if not anymore if (!allNewItemsCounted) { if (activity->getId() == m_previousNewestActivityId) { allNewItemsCounted = true; if (newItemsCount > 0) { needToInsertSeparator = true; } } else { // If activity is not ours, add it to the count if (activity->author()->getId() != m_pumpController->currentUserId()) { ++newItemsCount; itemIsNew = true; } } } if (filtered == FilterChecker::Highlight) // kinda TMP { itemHighlightedByFilter = true; } else { itemHighlightedByFilter = false; } MinorFeedItem *newFeedItem = new MinorFeedItem(activity, itemHighlightedByFilter, m_pumpController, m_globalObject, this); newFeedItem->setItemAsNew(itemIsNew, false); // Don't inform the feed if (itemIsNew) { connect(newFeedItem, &MinorFeedItem::itemRead, this, &MinorFeed::decreaseNewItemsCount); if (newFeedItem->getItemHighlightType() != -1) { ++newHighlightedItemsCount; } } if (m_gettingNew) { if (needToInsertSeparator) // ------- { this->insertSeparator(insertedItemsCount); ++insertedItemsCount; needToInsertSeparator = false; } m_itemsLayout->insertWidget(insertedItemsCount, newFeedItem); m_itemsInFeed.insert(insertedItemsCount, newFeedItem); ++insertedItemsCount; } else // Not new, getting 'older', so add at the bottom { m_itemsLayout->addWidget(newFeedItem); m_itemsInFeed.append(newFeedItem); } } else { ++newFilteredItemsCount; // Since the item is not added to the feed, we need to delete the activity delete activity; // FIXME: should not delete, just hide } } // End foreach // The first time stuff is received from the server, there's no need to sync if (!m_firstLoad) { // Activities in activitiesToSync are stored in reverse foreach (ASActivity *activity, activitiesToSync) { // Send notifications to update objects in the timelines this->syncActivityWithTimelines(activity); } } // Cleanup all ASActivity objects, no longer needed foreach (ASActivity *activity, allProcessedActivities) { delete activity; } // If there were new items, and not already added, add separator: ------- if (newItemsCount > 0 && m_separatorFrame->isHidden()) { this->insertSeparator(insertedItemsCount); } if (!newestActivityId.isEmpty()) // If some items were received should be valid { m_previousNewestActivityId = newestActivityId; } m_unreadItemsCount += newItemsCount; m_highlightedItemsCount += newHighlightedItemsCount; qDebug() << "Minor feed updated"; if (m_gettingNew) // not when getting more, older ones { emit newItemsCountChanged(m_unreadItemsCount, m_highlightedItemsCount); emit newItemsReceived(m_feedType, newItemsCount, newHighlightedItemsCount, newFilteredItemsCount, m_pendingToReceiveNextTime); qDebug() << "New items:" << newItemsCount << "; New highlighted:" << newHighlightedItemsCount; // Clean up, keeping at least the ones that were just received if (activitiesListSize > 0) // but only if something was received { this->removeOldItems(activitiesListSize); } } else { emit newItemsReceived(m_feedType, activitiesListSize, // Actual number of received items -1, -1, -1); // -1 highlighted, filtered and pending // means these are old items } m_getOlderButton->setEnabled(true); m_firstLoad = false; } void MinorFeed::onUpdateFailed(int requestType) { if (requestType == m_feedType) { m_getOlderButton->setEnabled(true); } } void MinorFeed::decreaseNewItemsCount(bool wasHighlighted) { --m_unreadItemsCount; if (wasHighlighted) { --m_highlightedItemsCount; } emit newItemsCountChanged(m_unreadItemsCount, m_highlightedItemsCount); } void MinorFeed::updateAvatarFollowStates() { foreach (MinorFeedItem *feedItem, m_itemsInFeed) { feedItem->syncAvatarFollowState(); } } dianara-v1.4.1/src/ivgraphicsview.h0000664000175000017500000000121513201141311015362 0ustar janjan#ifndef IVGRAPHICSVIEW_H #define IVGRAPHICSVIEW_H #include #include #include class IvGraphicsView : public QGraphicsView { Q_OBJECT public: explicit IvGraphicsView(QGraphicsScene *scene, QWidget *parent = 0); ~IvGraphicsView(); double getScaleToFit(int width, int height, int angle); signals: void zoomInRequested(); void zoomOutRequested(); void zoomableModeRequested(); void doubleClicked(); public slots: protected: virtual void wheelEvent(QWheelEvent *event); virtual void mouseDoubleClickEvent(QMouseEvent *event); private: }; #endif // IVGRAPHICSVIEW_H dianara-v1.4.1/src/ivgraphicsview.cpp0000664000175000017500000000415113211520151015723 0ustar janjan#include "ivgraphicsview.h" IvGraphicsView::IvGraphicsView(QGraphicsScene *scene, QWidget *parent) : QGraphicsView(parent) { this->setParent(parent); this->setScene(scene); this->setDragMode(QGraphicsView::ScrollHandDrag); this->setTransformationAnchor(QGraphicsView::AnchorUnderMouse); this->setResizeAnchor(QGraphicsView::AnchorUnderMouse); qDebug() << "IvGraphicsView created"; } IvGraphicsView::~IvGraphicsView() { qDebug() << "IvGraphicsView destroyed"; } /* * Calculate scale factor (or zoom level), to scale like fitInView() * */ double IvGraphicsView::getScaleToFit(int width, int height, int angle) { double scaleLevel; double imageWidth = qMax(width, 1); double imageHeight = qMax(height, 1); if (qAbs(angle) == 90 || qAbs(angle) == 270) // Partially turned to either side { qSwap(imageWidth, imageHeight); } double viewportWidth = qMax(this->viewport()->width(), 1); double viewportHeight = qMax(this->viewport()->height(), 1); double imageRatio = imageWidth / imageHeight; double viewportRatio = viewportWidth / viewportHeight; if (imageRatio > viewportRatio) { scaleLevel = viewportWidth / imageWidth; } else { scaleLevel = viewportHeight / imageHeight; } // TODO: round scaleLevel to .00/.05/.10 return qBound(0.05, scaleLevel, 5.0); } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////////// PROTECTED //////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void IvGraphicsView::wheelEvent(QWheelEvent *event) { // Only for vertical axis changes if (event->angleDelta().y() != 0) { emit zoomableModeRequested(); if (event->angleDelta().y() > 0) { emit zoomInRequested(); } else { emit zoomOutRequested(); } event->accept(); } } void IvGraphicsView::mouseDoubleClickEvent(QMouseEvent *event) { emit doubleClicked(); event->accept(); } dianara-v1.4.1/src/fontpicker.cpp0000644000175000017500000000573713202667446015071 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "fontpicker.h" FontPicker::FontPicker(QString description, QString initialFontString, QWidget *parent) : QWidget(parent) { QFont initialFont; initialFont.fromString(initialFontString); if (initialFont.family().isEmpty()) // Invalid, so use standard font { m_currentFont = QFont(); } else // Valid, so use it { m_currentFont = initialFont; } m_descriptionLabel = new QLabel(description, this); m_descriptionLabel->setWordWrap(true); m_sampleLineEdit = new QLineEdit(this); m_sampleLineEdit->setReadOnly(true); updateFontSample(); m_button = new QPushButton(QIcon::fromTheme("list-add-font", QIcon(":/images/list-add.png")), tr("Change..."), this); connect(m_button, &QAbstractButton::clicked, this, &FontPicker::selectFont); m_layout = new QHBoxLayout(); m_layout->addWidget(m_descriptionLabel, 2); m_layout->addWidget(m_sampleLineEdit, 5); m_layout->addWidget(m_button, 1); this->setLayout(m_layout); qDebug() << "FontPicker created"; } FontPicker::~FontPicker() { qDebug() << "FontPicker destroyed"; } void FontPicker::updateFontSample() { m_sampleLineEdit->setFont(m_currentFont); m_sampleLineEdit->setText(m_currentFont.toString()); m_sampleLineEdit->setCursorPosition(0); } QString FontPicker::getFontInfo() { return m_currentFont.toString(); } //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////// SLOTS ///////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// void FontPicker::selectFont() { bool fontOk = false; QFont newFont = QFontDialog::getFont(&fontOk, m_currentFont, this, tr("Choose a font")); if (fontOk) { m_currentFont = newFont; updateFontSample(); } } dianara-v1.4.1/src/helpwidget.h0000664000175000017500000000275213202670501014504 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 HELPWIDGET_H #define HELPWIDGET_H #include #include #include #include #include #include #include #include #include class HelpWidget : public QWidget { Q_OBJECT public: explicit HelpWidget(QWidget *parent = 0); ~HelpWidget(); signals: public slots: protected: virtual void closeEvent(QCloseEvent *event); virtual void hideEvent(QHideEvent *event); private: QVBoxLayout *m_layout; QTextBrowser *m_helpTextBrowser; QPushButton *m_closeButton; QAction *m_closeAction; }; #endif // HELPWIDGET_H dianara-v1.4.1/src/proxydialog.h0000664000175000017500000000374213206625277014727 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 PROXYDIALOG_H #define PROXYDIALOG_H #include #include #include #include #include #include #include #include #include #include #include #include class ProxyDialog : public QWidget { Q_OBJECT public: explicit ProxyDialog(int proxyType, QString hostname, QString port, bool useAuth, QString user, QString password, QWidget *parent = 0); ~ProxyDialog(); signals: public slots: void toggleAuth(bool state); void saveSettings(); private: QVBoxLayout *m_mainLayout; QFormLayout *m_fieldsLayout; QHBoxLayout *m_buttonsLayout; QComboBox *m_proxyTypeComboBox; QLineEdit *m_hostnameLineEdit; QLineEdit *m_portLineEdit; QCheckBox *m_authCheckBox; QLineEdit *m_userLineEdit; QLineEdit *m_passwordLineEdit; QLabel *m_passwordNoteLabel; QPushButton *m_saveButton; QPushButton *m_cancelButton; QAction *m_closeAction; }; #endif // PROXYDIALOG_H dianara-v1.4.1/src/timestamp.h0000664000175000017500000000250013032056645014352 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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.4.1/src/asactivity.h0000664000175000017500000000470513202664367014545 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include "asobject.h" #include "asperson.h" #include "mischelpers.h" class ASActivity : public QObject { Q_OBJECT public: explicit ASActivity(QVariantMap activityMap, QString userId, QObject *parent = 0); ~ASActivity(); ASPerson *author(); ASObject *object(); ASObject *target(); ASPerson *personObject(); 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 generateTooltip(); QString generateSnippet(int charLimit); void setFilterMatches(QVariantMap newFilterMatches); QVariantMap getFilterMatches(); signals: public slots: private: ASPerson *m_author; ASObject *m_object; ASObject *m_target; ASPerson *m_personObject; QString m_id; QString m_verb; QString m_generator; QString m_createdAt; QString m_updatedAt; QString m_content; QString m_recipientsToString; QString m_recipientsCcString; QStringList m_recipientsIdList; QString m_ownUserId; bool m_shared; QString m_sharedByName; QString m_sharedById; QString m_sharedByAvatar; QVariantMap m_filterMatches; }; #endif // ASACTIVITY_H dianara-v1.4.1/src/filtermatcheswidget.cpp0000644000175000017500000001001313210105662016724 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "filtermatcheswidget.h" FilterMatchesWidget::FilterMatchesWidget(QVariantMap filterMatchesMap, QWidget *parent) : QFrame(parent) { this->setFrameStyle(QFrame::StyledPanel | QFrame::Raised); QString filterMatchInfoString; QString matchingStuff; matchingStuff = tagList(filterMatchesMap.value("matchingContent").toStringList()); if (!matchingStuff.isEmpty()) { // FIXME: Maybe replace "category" strings with symbols filterMatchInfoString.append(tr("Content", "The contents of the post matched") + ": " + matchingStuff + " "); } matchingStuff = tagList(filterMatchesMap.value("matchingAuthor").toStringList()); if (!matchingStuff.isEmpty()) { filterMatchInfoString.append(" " + tr("Author") + ": " + matchingStuff + " "); } matchingStuff = tagList(filterMatchesMap.value("matchingGenerator").toStringList()); if (!matchingStuff.isEmpty()) { filterMatchInfoString.append(" " + tr("App", "Application, short if possible") + ": " + matchingStuff + " "); } matchingStuff = tagList(filterMatchesMap.value("matchingDescription").toStringList()); if (!matchingStuff.isEmpty()) { filterMatchInfoString.append(" " + tr("Description") + ": " + matchingStuff); } m_contentLabel = new QLabel("" + filterMatchInfoString, this); m_contentLabel->setWordWrap(true); m_contentLabel->setAlignment(Qt::AlignCenter); m_layout = new QHBoxLayout(); m_layout->addWidget(m_contentLabel); this->setLayout(m_layout); qDebug() << "FilterMatchesWidget created"; } FilterMatchesWidget::~FilterMatchesWidget() { qDebug() << "FilterMatchesWidget destroyed"; } QString FilterMatchesWidget::tagList(QStringList tagsList) { QString tagListString; foreach (QString tag, tagsList) { // Non-breakable spaces needed in several places tagListString.append("  " + tag + "  " "  "); // Space needed after, for wordwrap } return tagListString; } dianara-v1.4.1/src/notifications.cpp0000644000175000017500000001555713210104326015555 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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(QObject *parent) : QObject(parent) { qDebug() << "Creating FreeDesktop Notifier"; m_notificationsAvailable = false; // Init as false, detect later m_notificationType = FDNotifications::SystemNotifications; m_notificationDuration = 4000; // Milliseconds #ifdef QT_DBUS_LIB m_busConnection = new QDBusConnection(QDBusConnection::sessionBus()); if (m_busConnection->isConnected()) { QDBusReply busReply = m_busConnection->interface() ->registeredServiceNames(); qDebug() << "Listing D-Bus services..."; if (busReply.isValid()) { foreach (QString serviceName, busReply.value()) { if (serviceName == "org.freedesktop.Notifications") { qDebug() << "org.freedesktop.Notifications D-Bus service found!"; m_notificationsAvailable = true; } } } } else { qDebug() << "D-Bus unavailable!"; m_notificationsAvailable = false; } #endif qDebug() << "FreeDesktop Notifier created"; } FDNotifications::~FDNotifications() { #ifdef QT_DBUS_LIB delete m_busConnection; #endif qDebug() << "FreeDesktop Notifier destroyed"; } bool FDNotifications::areNotificationsAvailable() { qDebug() << "Are notifications available?" << m_notificationsAvailable; return m_notificationsAvailable; } void FDNotifications::setNotificationOptions(int style, int duration, bool notifyNewTL, bool notifyHLTL, bool notifyNewMW, bool notifyHLMW, bool notifyErr) { m_notificationType = style; m_notificationDuration = duration * 1000; // To milliseconds m_notifyNewTimeline = notifyNewTL; m_notifyHLTimeline = notifyHLTL; m_notifyNewMeanwhile = notifyNewMW; m_notifyHLMeanwhile = notifyHLMW; m_notifyErrors = notifyErr; qDebug() << "Notification type set to" << m_notificationType << "-- FD.o/Qt/none; Duration:" << m_notificationDuration; qDebug() << "Things to notify:" << m_notifyNewTimeline << m_notifyHLTimeline << m_notifyNewMeanwhile << m_notifyHLMeanwhile << m_notifyErrors; } void FDNotifications::setCurrentUserId(QString newId) { m_currentUserId = newId; } bool FDNotifications::getNotifyNewTimeline() { return m_notifyNewTimeline; } bool FDNotifications::getNotifyHLTimeline() { return m_notifyHLTimeline; } bool FDNotifications::getNotifyNewMeanwhile() { return m_notifyNewMeanwhile; } bool FDNotifications::getNotifyHLMeanwhile() { return m_notifyHLMeanwhile; } bool FDNotifications::getNotifyErrors() { return m_notifyErrors; } /////////////////////////////////////////////////////////////////////// ////////////////////////////// SLOTS ////////////////////////////////// /////////////////////////////////////////////////////////////////////// void FDNotifications::showMessage(QString message) { // if notifications are disabled if (m_notificationType == FDNotifications::NoNotifications) { return; } const QString notificationTitle = "Dianara - " + m_currentUserId; // If FD.org notifications are not available, or Qt's Balloon ones are selected if (!m_notificationsAvailable || m_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(""); message.replace("
", "\n"); emit showFallbackNotification(notificationTitle, message, m_notificationDuration); return; } // Only when building with D-Bus support #ifdef QT_DBUS_LIB message.replace("\n", "
"); // use HTML newlines // HTML newlines break notifications in Xfce; \n works, but is not actually // shown as newline in Plasma 4's notifications. // However, \n works fine in Plasma 5's notifications. QDBusMessage dBusMessage = QDBusMessage::createMethodCall( "org.freedesktop.Notifications", "/org/freedesktop/Notifications", QString(), QStringLiteral("Notify")); // --- D-Bus 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 << QStringLiteral("Dianara"); // app_name arguments << uint(0); // replaces_id if (QIcon::hasThemeIcon("dianara")) { arguments << QStringLiteral("dianara"); // app_icon, if "dianara" icon is in theme } else { // app_icon otherwise arguments << QStringLiteral("dialog-information"); } QStringList actions; // Handled on MainWindow::onNotificationAction() actions << QString("dianara_%1_show").arg(qApp->applicationPid()) << tr("Show"); arguments << notificationTitle; arguments << message; arguments << actions; arguments << QVariantMap(); // hints arguments << m_notificationDuration; // timeout, in milliseconds dBusMessage.setArguments(arguments); qDebug() << "Sending DBUS call to org.freedesktop.Notifications Notify()"; m_busConnection->asyncCall(dBusMessage); #endif } dianara-v1.4.1/src/asobject.h0000664000175000017500000001013313211035501014125 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include "asperson.h" #include "timestamp.h" class ASObject : public QObject { Q_OBJECT public: explicit ASObject(QVariantMap objectMap, QObject *parent = 0); ~ASObject(); void updateAuthorFromPerson(ASPerson *person); ASPerson *author(); QString getId(); QString getType(); static QString getTranslatedType(QString typeString); QString getUrl(); QString getCreatedAt(); QString getUpdatedAt(); QString getLocationName(); QString getLocationFormatted(); QString getLocationCountry(); QString getLocationTooltip(); QString getDeletedTime(); QString getDeletedOnString(); static QString makeDeletedOnString(QString deletionTime); bool isLiked(); QString getTitle(); QString getSummary(); QString getContent(); QString getImageUrl(); QString getSmallImageUrl(); int getImageWidth(); int getImageHeight(); QString getAudioUrl(); QString getVideoUrl(); QString getFileUrl(); QString getMimeType(); QString getAttachmentPureUrl(); int getMemberCount(); QString getMemberUrl(); int getLikesCount(); int getCommentsCount(); int getSharesCount(); bool hasProxiedUrls(); QVariantList getLastLikesList(); QVariantList getLastCommentsList(); QVariantList getLastSharesList(); QString getLikesUrl(); QString getCommentsUrl(); QString getSharesUrl(); QVariantMap getOriginalObject(); QVariantMap getInReplyTo(); QString getInReplyToId(); // This one to deprecate static QString personStringFromList(QVariantList variantList, int count=-1); static QVariantMap simplePersonMapFromList(QVariantList variantList); static void addOnePersonToSimpleMap(QString personId, QString personName, QString personUrl, QVariantMap *personMap); static QString personStringFromSimpleMap(QVariantMap personMap, int totalCount); static bool canDisplayObject(QString objectType); signals: public slots: private: ASPerson *m_author; QString m_id; QString m_type; QString m_url; QString m_createdAt; QString m_updatedAt; QString m_locationName; QString m_locationFormatted; QString m_locationCountry; QString m_deleted; bool m_liked; QString m_title; QString m_summary; QString m_content; QString m_imageUrl; QString m_smallImageUrl; int m_imageWidth; int m_imageHeight; QString m_audioUrl; QString m_videoUrl; QString m_fileUrl; QString m_mimeType; QString m_attachmentPureUrl; // To have filename with extension when using a proxyURL // Properties for group objects int m_memberCount; QString m_memberUrl; int m_likesCount; int m_commentsCount; int m_sharesCount; QVariantList m_lastLikesList; QVariantList m_lastCommentsList; QVariantList m_lastSharesList; QString m_likesUrl; QString m_commentsUrl; QString m_sharesUrl; bool m_proxiedUrls; QVariantMap m_originalObjectMap; QVariantMap m_inReplyToMap; }; #endif // ASOBJECT_H dianara-v1.4.1/src/composer.cpp0000644000175000017500000010214713212033651014527 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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(GlobalObject *globalObject, bool forPublisher, QWidget *parent) : QTextEdit(parent) { m_globalObject = globalObject; m_forPublisher = forPublisher; this->setAcceptRichText(true); this->setTabChangesFocus(true); m_clickToPostString = tr("Click here or press Control+N to post a note..."); // A menu to insert some Unicode symbols m_symbolsMenu = new QMenu(tr("Symbols"), this); m_symbolsMenu->setIcon(QIcon::fromTheme("character-set")); m_symbolsMenu->addAction(QString::fromUtf8("\342\230\272")); // Smiling face m_symbolsMenu->addAction(QString::fromUtf8("\342\230\271")); // Sad face m_symbolsMenu->addAction(QString::fromUtf8("\342\231\245")); // Heart m_symbolsMenu->addAction(QString::fromUtf8("\342\231\253")); // Musical note m_symbolsMenu->addAction(QString::fromUtf8("\342\230\225")); // Coffee m_symbolsMenu->addAction(QString::fromUtf8("\342\234\224")); // Check mark m_symbolsMenu->addAction(QString::fromUtf8("\342\234\230")); // Ballot X m_symbolsMenu->addAction(QString::fromUtf8("\342\230\205")); // Black star m_symbolsMenu->addAction(QString::fromUtf8("\342\254\205")); // Arrow to the left m_symbolsMenu->addAction(QString::fromUtf8("\342\236\241")); // Arrow to the right m_symbolsMenu->addAction(QString::fromUtf8("\342\231\273")); // Recycling symbol m_symbolsMenu->addAction(QString::fromUtf8("\342\210\236")); // Infinity connect(m_symbolsMenu, &QMenu::triggered, this, &Composer::insertSymbol); m_toolsMenu = new QMenu(tr("Formatting"), this); m_toolsMenu->addAction(tr("Normal"), this, SLOT(makeNormal())); m_toolsMenu->addAction(QIcon::fromTheme("format-text-bold"), tr("Bold"), this, SLOT(makeBold()), QKeySequence("Ctrl+B")); m_toolsMenu->addAction(QIcon::fromTheme("format-text-italic"), tr("Italic"), this, SLOT(makeItalic()), QKeySequence("Ctrl+I")); m_toolsMenu->addAction(QIcon::fromTheme("format-text-underline"), tr("Underline"), this, SLOT(makeUnderline()), QKeySequence("Ctrl+U")); m_toolsMenu->addAction(QIcon::fromTheme("format-text-strikethrough"), tr("Strikethrough"), this, SLOT(makeStrikethrough())); m_toolsMenu->addSeparator(); m_toolsMenu->addAction(QIcon::fromTheme("format-font-size-more"), tr("Header"), this, SLOT(makeHeader()), QKeySequence("Ctrl+H")); m_toolsMenu->addAction(QIcon::fromTheme("format-list-unordered"), tr("List"), this, SLOT(makeList())); m_toolsMenu->addAction(QIcon::fromTheme("insert-table"), tr("Table"), this, SLOT(makeTable())); m_toolsMenu->addAction(QIcon::fromTheme("format-justify-fill"), tr("Preformatted block"), this, SLOT(makePreformatted())); m_toolsMenu->addAction(QIcon::fromTheme("format-text-italic"), tr("Quote block"), this, SLOT(makeQuote()), QKeySequence("Ctrl+O")); m_toolsMenu->addSeparator(); m_toolsMenu->addAction(QIcon::fromTheme("insert-link"), tr("Make a link"), this, SLOT(makeLink()), QKeySequence("Ctrl+L")); m_toolsMenu->addAction(QIcon::fromTheme("insert-image"), tr("Insert an image from a web site"), this, SLOT(insertImage()), QKeySequence("Ctrl+P")); m_toolsMenu->addAction(QIcon::fromTheme("insert-horizontal-rule"), tr("Insert line"), this, SLOT(insertLine()), QKeySequence("Ctrl+Shift+L")); m_toolsMenu->addSeparator(); m_toolsMenu->addMenu(m_symbolsMenu); m_toolsButton = new QPushButton(QIcon::fromTheme("format-list-ordered", QIcon(":/images/button-configure.png")), tr("&Format", "Button for text formatting and related options"), this); m_toolsButton->setMenu(m_toolsMenu); m_toolsButton->setToolTip("" + tr("Text Formatting Options")); // Extra action for the context menu, paste as plaintext m_pastePlaintextAction = new QAction(QIcon::fromTheme("edit-paste"), tr("Paste Text Without Formatting"), this); m_pastePlaintextAction->setShortcut(QKeySequence("Ctrl+Shift+V")); connect(m_pastePlaintextAction, &QAction::triggered, this, &Composer::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(m_pastePlaintextAction); // Nick completion stuff m_nickCompleter = new QCompleter(m_globalObject->getNickCompletionModel(), this); m_nickCompleter->setWidget(this); m_nickCompleter->setMaxVisibleItems(15); m_nickCompleter->setCompletionColumn(0); m_nickCompleter->setCaseSensitivity(Qt::CaseInsensitive); m_nickCompleter->setModelSorting(QCompleter::CaseInsensitivelySortedModel); connect(m_nickCompleter, SIGNAL(activated(QModelIndex)), // Old-style connect() this, SLOT(insertCompletedNick(QModelIndex))); // due to signal overload m_popupTableView = new QTableView(this); m_popupTableView->horizontalHeader()->hide(); m_popupTableView->verticalHeader()->hide(); m_popupTableView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_popupTableView->setAlternatingRowColors(true); m_popupTableView->setSelectionBehavior(QAbstractItemView::SelectRows); m_popupTableView->setSelectionMode(QAbstractItemView::SingleSelection); m_popupTableView->setEditTriggers(QAbstractItemView::NoEditTriggers); m_nickCompleter->setPopup(m_popupTableView); if (m_forPublisher) // Publisher mode { this->setToolTip("" + tr("Type a message here to post it")); this->setPlaceholderText(m_clickToPostString); } else // or Commenter mode { this->setPlaceholderText(tr("Type a comment here")); } qDebug() << "Composer box created"; } Composer::~Composer() { qDebug() << "Composer box destroyed"; } void Composer::erase() { this->clear(); if (m_forPublisher) { this->setPlaceholderText(m_clickToPostString); } } void Composer::insertLink(QString url, QString title) { bool prettyLink = true; if (title.isEmpty()) { prettyLink = false; title = url; } this->insertHtml("" + title + ""); // Space after, when there's no text after the link, // or the link is inserted without selecting text before, // so what the user types next is regular text if (this->textCursor().atEnd() || !prettyLink) { this->insertHtml(" "); // Could use this->makeNormal(), but has some drawbacks } } void Composer::requestCompletion(QString partialNick) { m_nickCompleter->setCompletionPrefix(partialNick); m_popupTableView->ensurePolished(); m_popupTableView->resizeColumnsToContents(); m_popupTableView->resizeRowsToContents(); m_popupTableView->ensurePolished(); int tableWidth = m_popupTableView->columnWidth(0) + m_popupTableView->columnWidth(1) + 2; if (tableWidth > this->width() + 2) { tableWidth = this->width() + 2; m_popupTableView->setColumnWidth(0, (tableWidth / 4) * 3); m_popupTableView->setColumnWidth(1, tableWidth / 4); } m_popupTableView->setMinimumWidth(tableWidth); int rows = qMin(m_popupTableView->model()->rowCount(), 15); int rowHeight = m_popupTableView->rowHeight(0) + 4; m_popupTableView->setMinimumHeight(rows * rowHeight); // Popup might get disabled at some point when Composer is used in a Post m_popupTableView->setEnabled(true); m_nickCompleter->complete(QRect(this->cursorRect().x(), this->cursorRect().y() + 24, tableWidth, 1)); } /* * Hide placeholder message * */ void Composer::hideInfoMessage() { this->setPlaceholderText(QString()); } QPushButton *Composer::getToolsButton() { return m_toolsButton; } /* * Enable or disable the Ctrl+Shift+V action to paste without format * * Needed to avoid this conflict between Publisher and Commenters: * * QAction::eventFilter: Ambiguous shortcut overload: Ctrl+Shift+V * */ void Composer::setPlainPasteEnabled(bool state) { m_pastePlaintextAction->setEnabled(state); } /*****************************************************************************/ /****************************** PROTECTED ************************************/ /*****************************************************************************/ /* * Send a signal when getting focus * */ void Composer::focusInEvent(QFocusEvent *event) { emit focusReceived(); // inform Publisher() or Commenter() that we have focus QTextEdit::focusInEvent(event); // process standard event: allows context menu qDebug() << "Composer box got focus"; } /* * Same signal when having something dropped into the widget * */ void Composer::dropEvent(QDropEvent *event) { qDebug() << "Composer received a drag-and-drop:" << event->mimeData()->text(); emit focusReceived(); QList urls = event->mimeData()->urls(); // Something with a URL was dropped, probably a local file if (!urls.isEmpty()) { qDebug() << "** Dropped URLs:" << urls; QString fileUrl = urls.first().toLocalFile(); if (urls.first().isLocalFile()) // Local, but might be a directory { if (QFileInfo(fileUrl).isFile()) { qDebug() << "**** It's a local file:" << fileUrl; if (urls.count() == 1) { // Clear possible prior error messages emit errorHappened(QString()); } else { // Notify user that only one file can be attached emit errorHappened(tr("You can attach only one file.")); } emit fileDropped(fileUrl); } else { qDebug() << "**** Dropped a folder!"; emit errorHappened(tr("You cannot drop folders here, " "only a single file.")); } return; } } QTextEdit::dropEvent(event); } void Composer::keyPressEvent(QKeyEvent *event) { // Allow cancelling/accepting the autocompletion popup if (m_nickCompleter->popup()->isVisible()) { if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return || event->key() == Qt::Key_Escape) { event->ignore(); return; } } // 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, cancelling post"; this->cancelPost(); } } else if (event->key() == Qt::Key_Up && event->modifiers() != Qt::ShiftModifier) { QTextCursor currentCursor = this->textCursor(); if (currentCursor.movePosition(QTextCursor::Up)) // true=cursor is able to go up { QTextEdit::keyPressEvent(event); } else { qDebug() << "KEYPRESS UP: Can't go up, will focus on TITLE field"; emit focusTitleRequested(); } } else { QTextEdit::keyPressEvent(event); QTextCursor currentCursor = this->textCursor(); currentCursor.select(QTextCursor::WordUnderCursor); if (currentCursor.document()->characterAt(currentCursor.selectionStart() - 1) == QChar('@')) { // Show completer this->requestCompletion(currentCursor.selectedText()); } else { // Hide it, if it was visible m_nickCompleter->popup()->hide(); } } event->accept(); } /* * For custom context menu * */ void Composer::contextMenuEvent(QContextMenuEvent *event) { m_customContextMenu = this->createStandardContextMenu(); // 'Formating' menu before default context menu m_customContextMenu->insertMenu(m_customContextMenu->actions().at(0), m_toolsMenu); m_customContextMenu->insertSeparator(m_customContextMenu->actions().at(1)); // And options added after default context menu m_customContextMenu->addSeparator(); m_customContextMenu->addAction(m_pastePlaintextAction); m_pastePlaintextAction->setEnabled(this->canPaste()); m_customContextMenu->exec(event->globalPos()); // FIXME: Possible leak... should delete m_customContextMenu? m_customContextMenu->deleteLater(); event->accept(); } /* * Intervene when pasting, so we can turn link-looking text into real * HTML links, and turn direct links to images into embedded images * */ void Composer::insertFromMimeData(const QMimeData *source) { /* * Prevent larjona's crash. * * Apparently the QMessageBox will mess with the 'source' pointer * if the contents of the clipboard came from another program. * This causes a segfault when selecting "insert as link". * * To avoid it, all data needed from 'source' will be stored before showing * the message box. * * https://gitlab.com/dianara/dianara-dev/issues/33 * */ QString pastedHtml = source->html(); QString pastedText = source->text().trimmed(); bool sourceHasHtml = source->hasHtml(); // First, check if it's just a link to an image, to offer embedding it if (pastedText.startsWith("http://") || pastedText.startsWith("https://")) { // If link looks like an image, ask the user how to insert it if (pastedText.endsWith(".png", Qt::CaseInsensitive) || pastedText.endsWith(".jpg", Qt::CaseInsensitive) || pastedText.endsWith(".jpeg", Qt::CaseInsensitive) || pastedText.endsWith(".gif", Qt::CaseInsensitive)) { int insertionType= QMessageBox::question(this, tr("Insert as image?"), tr("The link you are pasting " "seems to point to an image.") + "\n\n\n", tr("Insert as visible image"), // Default option (Enter) tr("Insert as link"), // ESC option QString(), 0, 1); if (insertionType == 0) // Default button, insert as image { this->insertHtml("" "

"); return; } } } // If it wasn't just an image link, or offer to embed was rejected, continue // WARNING: Avoid accessing 'source' at this point, since the QMessageBox // might have messed with it QTextDocument textDocument; if (sourceHasHtml) { textDocument.setHtml(pastedHtml); qDebug() << "Parsing pasted RICH (HTML) text for links..."; } else { textDocument.setPlainText(pastedText); qDebug() << "Parsing pasted PLAIN text for links..."; } QTextCursor cursor = QTextCursor(&textDocument); do { cursor.select(QTextCursor::WordUnderCursor); QString currentWord = cursor.selection().toPlainText() .trimmed(); // Strip possible zero-widths qDebug() << "WORD:" << currentWord; if (currentWord == "http" || currentWord == "https" || currentWord == "ftp" || currentWord == "ftps" || currentWord == "sftp") { qDebug() << "Text might be a link"; qDebug() << "Cursor is anchor? " << cursor.charFormat().isAnchor(); if (!cursor.charFormat().isAnchor()) { qDebug() << "URL-looking text which isn't a link; " "Replacing with proper link!"; // Select text until end of pasted text, or a space/line break while (cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor)) { if (textDocument.characterAt(cursor.position()).isSpace()) { break; } } QString linkFound = cursor.selection().toPlainText(); if (linkFound.startsWith("http://") || linkFound.startsWith("https://") || linkFound.startsWith("ftp://") || linkFound.startsWith("ftps://") || linkFound.startsWith("sftp://")) { // Turn current selection into a link to the same as the visible text QTextCharFormat format = cursor.charFormat(); format.setAnchorHref(linkFound); format.setAnchor(true); cursor.setCharFormat(format); qDebug() << "Linkified: " << linkFound; } } } } while (cursor.movePosition(QTextCursor::NextWord)); this->insertHtml(textDocument.toHtml() + " "); } /*****************************************************************************/ /******************************** 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() { 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(); } void Composer::makeList() { QString selectedText = this->textCursor().selection().toHtml(); selectedText = MiscHelpers::cleanupHtml(selectedText); if (selectedText.isEmpty()) { this->textCursor().insertList(QTextListFormat::ListDisc); } else { // Capture only the HTML of each line by itself QRegExp pRE("

(.*)

"); pRE.setMinimal(true); QString listHtml = "
    "; int pos = 0; while ((pos = pRE.indexIn(selectedText, pos)) != -1) { listHtml.append("
  • " + pRE.cap(1) + "
  • "); pos += pRE.matchedLength(); } listHtml.append("

"); this->textCursor().removeSelectedText(); this->insertHtml(listHtml); this->textCursor().deletePreviousChar(); // Delete undesired extra newline } this->setFocus(); } void Composer::makeTable() { const QString dialogTitle = tr("Table Size"); bool inputOk = false; int rows = QInputDialog::getInt(this, dialogTitle, tr("How many rows (height)?") + " " // Make the dialog a little wider than necessary + QString::fromUtf8("\342\207\225") // up-down arrow + "\n\n", 5, 1, 10, 1, &inputOk); if (inputOk) // Rows dialog wasn't cancelled { int columns = QInputDialog::getInt(this, dialogTitle, tr("How many columns (width)?") + " " + QString::fromUtf8("\342\207\224") // left-right arrow + "\n\n", 4, 1, 10, 1, &inputOk); if (inputOk) // Columns dialog wasn't cancelled either { QTextTableFormat tableFormat; tableFormat.setCellPadding(2); tableFormat.setCellSpacing(4); this->textCursor().insertTable(rows, columns, tableFormat); } } 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 + "”
" "
"); this->textCursor().deletePreviousChar(); // Delete undesired extra newline } // 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; bool validLink = false; bool dialogOk = true; while (!validLink) { if (selectedText.isEmpty()) { link = QInputDialog::getText(this, tr("Insert a link"), tr("Type or paste a web address " "here.\n" "You could also select some " "text first, to turn it into " "a link.") + "\n\n", QLineEdit::Normal, "http://", &dialogOk); } else { QString shortenedText = MiscHelpers::elidedText(selectedText, 40); 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("'" + shortenedText + "'") + "\n\n", QLineEdit::Normal, "http://", &dialogOk); } if (!dialogOk) { this->setFocus(); return; } link = link.trimmed(); // Remove possible spaces before or after if (link.startsWith("http://") || link.startsWith("https://") || link.startsWith("ftp://") || link.startsWith("ftps://") || link.startsWith("sftp://") || link.startsWith("mailto:")) { validLink = true; } if (!validLink) { int choice = QMessageBox::warning(this, tr("Invalid link"), tr("The text you entered does " "not look like a link.") + "

" + tr("It should start with " "one of these types:", "It = the link, from " "previous sentence") + "
" "
    " "
  • http://
  • " "
  • https://
  • " "
  • ftp://
  • " "
  • ftps://
  • " "
  • sftp://
  • " "
  • mailto:
  • " "
" "


", tr("&Use it anyway"), tr("&Enter it again"), tr("&Cancel link"), 1, 2); if (choice == 0) { validLink = true; } else if (choice == 2) { this->setFocus(); return; } } } this->textCursor().removeSelectedText(); this->insertLink(link, selectedText); 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" "The link must point to the image file directly.") + "\n\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 inserting this->insertPlainText(clipboardContents); } /* * Insert the selected nick chosen from the autocomplete list. * Notify the parent object about it so the user can be added * to To/Cc. * */ void Composer::insertCompletedNick(QModelIndex nickData) { QString nickId = nickData.data(Qt::UserRole + 1).toString(); QString nickName = nickData.data().toString(); QString nickUrl = nickData.data(Qt::UserRole + 2).toString(); // Abort if there's no ID; this can happen if the ID column is clicked if (nickId.isEmpty()) { qDebug() << "*** AUTOCOMPLETER: ID IS EMPTY! (clicked 2nd column?)"; return; } QTextCursor textCursor = this->textCursor(); textCursor.select(QTextCursor::WordUnderCursor); textCursor.removeSelectedText(); this->insertHtml("" + nickName + ""); this->makeNormal(); // Send signal for Publisher(), to add to the "To" field emit nickInserted(nickId, nickName, nickUrl, "to"); } /* * Cancel editing of the post, clear it, return to minimum mode * */ void Composer::cancelPost() { int cancelConfirmed = 2; int defaultButton = 1; // Initially planning for 2 buttons if (this->document()->isEmpty()) { cancelConfirmed = 0; // Cancelling doesn't need confirmation if it's empty } else { QString saveDraftString; if (m_forPublisher) { saveDraftString = tr("Yes, but saving a &draft"); defaultButton = 2; // Since in this case there are 3 buttons } cancelConfirmed = QMessageBox::question(this, tr("Cancel message?"), tr("Are you sure you want to " "cancel this message?"), tr("&Yes, cancel it"), saveDraftString, tr("&No"), defaultButton, defaultButton); } if (cancelConfirmed == 0) { this->erase(); // Emit signal to make Publisher go back to minimum mode emit editingCancelled(); qDebug() << "Post cancelled"; } else if (cancelConfirmed == 1) // Save draft { emit cancelSavingDraftRequested(); } } dianara-v1.4.1/src/audienceselector.h0000664000175000017500000000652013202610437015665 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #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(); void deletePrevious(); void saveSelected(); void restoreSelected(); QMenu *getSelectorMenu(); void setDefaultAudience(bool toPublic); void clearPublicAndFollowers(); void setPublic(bool state); bool isPublicSelected(); void setFollowers(bool state); bool isFollowersSelected(); void setListsMenu(QVariantList newLists); void checkListWithId(QString id); QString updatedAudienceLabels(); QVariantList getAudienceList(bool *onlyToFollowers); int getRecipientsCount(); signals: void audienceChanged(); void publicSelected(); void followersSelected(); public slots: void copyToSelected(QIcon contactIcon, QString contactString, QString contactName, QString contactId, QString contactUrl); void setAudience(); void onListToggled(QAction *listAction); void onPublicToggled(bool checked); void onFollowersToggled(bool checked); protected: virtual void closeEvent(QCloseEvent *event); virtual void hideEvent(QHideEvent *event); private: QString selectorType; QVBoxLayout *mainLayout; QHBoxLayout *upperLayout; QVBoxLayout *allGroupboxLayout; QGroupBox *allContactsGroupbox; PeopleWidget *peopleWidget; QVBoxLayout *selectedGroupboxLayout; QGroupBox *selectedListGroupbox; QLabel *explanationLabel; QListWidget *selectedListWidget; QList previousItems; QPushButton *clearSelectedListButton; QHBoxLayout *buttonsLayout; QPushButton *doneButton; QPushButton *cancelButton; QMenu *m_selectorMenu; QAction *m_publicAction; QAction *m_followersAction; QMenu *m_listsMenu; QAction *doneAction; QAction *cancelAction; PumpController *m_pumpController; }; #endif // AUDIENCESELECTOR_H dianara-v1.4.1/src/filtereditor.cpp0000664000175000017500000003022213210122456015370 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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(FilterChecker *filterChecker, QWidget *parent) : QWidget(parent) { m_filterChecker = filterChecker; this->setWindowTitle(tr("Filter Editor") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("view-filter", QIcon(":/images/button-filter.png"))); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::ApplicationModal); this->setMinimumSize(520, 440); QSettings settings; QSize savedWindowsize = settings.value("FilterEditor/filterWindowSize") .toSize(); if (savedWindowsize.isValid()) { this->resize(savedWindowsize); } m_ruleTemplateString = tr("%1 if %2 contains: %3", "This explains a filter rule, like: " "Hide if Author ID contains JohnDoe"); QFont explanationFont; explanationFont.setPointSize(explanationFont.pointSize() - 1); m_explanationLabel = new QLabel(tr("Here you can set some rules for hiding or " "highlighting stuff. " "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. You could also highlight " "messages that contain your name.") + "\n", this); m_explanationLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); m_explanationLabel->setWordWrap(true); m_explanationLabel->setFont(explanationFont); m_actionTypeComboBox = new QComboBox(this); m_actionTypeComboBox->addItem(QIcon::fromTheme("edit-delete", QIcon(":/images/button-delete.png")), tr("Hide")); m_actionTypeComboBox->addItem(QIcon::fromTheme("format-fill-color", QIcon(":/images/button-online.png")), tr("Highlight")); m_actionTypeComboBox->setCurrentIndex(1); // #2 option (highlight) as default m_filterTypeComboBox = new QComboBox(this); m_filterTypeComboBox->addItem(QIcon::fromTheme("accessories-text-editor", QIcon(":/images/button-edit.png")), tr("Post Contents")); m_filterTypeComboBox->addItem(QIcon::fromTheme("user-identity", QIcon(":/images/no-avatar.png")), tr("Author ID")); m_filterTypeComboBox->addItem(QIcon::fromTheme("applications-other", QIcon(":/images/button-configure.png")), tr("Application")); m_filterTypeComboBox->addItem(QIcon::fromTheme("accessories-text-editor", QIcon(":/images/button-edit.png")), tr("Activity Description")); m_filterWordsLineEdit = new QLineEdit(this); m_filterWordsLineEdit->setPlaceholderText(tr("Keywords...")); m_filterWordsLineEdit->setClearButtonEnabled(true); m_addFilterButton = new QPushButton(QIcon::fromTheme("list-add", QIcon(":/images/list-add.png")), tr("&Add Filter"), this); m_addFilterButton->setDisabled(true); connect(m_addFilterButton, &QAbstractButton::clicked, this, &FilterEditor::addFilter); connect(m_filterWordsLineEdit, &QLineEdit::returnPressed, this, &FilterEditor::addFilter); connect(m_filterWordsLineEdit, &QLineEdit::textChanged, this, &FilterEditor::onFilterTextChanged); m_filtersListWidget = new QListWidget(this); m_filtersListWidget->setToolTip(tr("Filters in use")); m_filtersListWidget->setDragDropMode(QAbstractItemView::InternalMove); m_removeFilterButton = new QPushButton(QIcon::fromTheme("list-remove", QIcon(":/images/list-remove.png")), tr("&Remove Selected Filter"), this); m_removeFilterButton->setDisabled(true); connect(m_removeFilterButton, &QAbstractButton::clicked, this, &FilterEditor::removeFilter); connect(m_filtersListWidget, &QListWidget::currentRowChanged, this, &FilterEditor::onFilterRowChanged); m_saveButton = new QPushButton(QIcon::fromTheme("document-save", QIcon(":/images/button-save.png")), tr("&Save Filters"), this); connect(m_saveButton, &QAbstractButton::clicked, this, &FilterEditor::saveFilters); m_cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("&Cancel"), this); connect(m_cancelButton, &QAbstractButton::clicked, this, &QWidget::hide); m_closeAction = new QAction(this); m_closeAction->setShortcut(QKeySequence(Qt::Key_Escape)); connect(m_closeAction, &QAction::triggered, this, &QWidget::hide); this->addAction(m_closeAction); // Layout m_topLayout = new QHBoxLayout(); m_topLayout->addWidget(m_actionTypeComboBox); m_topLayout->addSpacing(2); m_topLayout->addWidget(new QLabel(tr("if"), this)); m_topLayout->addSpacing(2); m_topLayout->addWidget(m_filterTypeComboBox); m_topLayout->addSpacing(2); m_topLayout->addWidget(new QLabel(tr("contains"), this)); m_topLayout->addSpacing(2); m_topLayout->addWidget(m_filterWordsLineEdit, 1); m_topLayout->addSpacing(8); m_topLayout->addWidget(m_addFilterButton); m_newFilterGroupBox = new QGroupBox(tr("&New Filter"), this); m_newFilterGroupBox->setLayout(m_topLayout); m_middleLayout = new QVBoxLayout(); m_middleLayout->addWidget(m_filtersListWidget); m_middleLayout->addWidget(m_removeFilterButton, 0, Qt::AlignRight); m_currentFiltersGroupBox = new QGroupBox(tr("C&urrent Filters"), this); m_currentFiltersGroupBox->setLayout(m_middleLayout); m_bottomLayout = new QHBoxLayout(); m_bottomLayout->setAlignment(Qt::AlignRight); m_bottomLayout->addWidget(m_saveButton); m_bottomLayout->addWidget(m_cancelButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->addWidget(m_explanationLabel); m_mainLayout->addSpacing(4); m_mainLayout->addWidget(m_newFilterGroupBox); m_mainLayout->addSpacing(2); m_mainLayout->addWidget(m_currentFiltersGroupBox); m_mainLayout->addSpacing(8); m_mainLayout->addLayout(m_bottomLayout); this->setLayout(m_mainLayout); loadFilters(); qDebug() << "FilterEditor created"; } FilterEditor::~FilterEditor() { qDebug() << "FilterEditor destroyed"; } void FilterEditor::loadFilters() { QSettings settings; QVariantList filtersList = settings.value("Filters/currentFilters").toList(); foreach (QVariant filterItem, filtersList) { int actionType = filterItem.toMap().value("action").toInt(); int filterType = filterItem.toMap().value("type").toInt(); QString filterWords = filterItem.toMap().value("text").toString(); QVariantMap itemMap; itemMap.insert("action", actionType); itemMap.insert("type", filterType); itemMap.insert("text", filterWords); QListWidgetItem *item; item = new QListWidgetItem(m_ruleTemplateString .arg(m_actionTypeComboBox->itemText(actionType)) .arg("'" + m_filterTypeComboBox->itemText(filterType) + "'") .arg("'" + filterWords + "'")); // Note: Don't mess with spaces around the ruleString parameters, // since the structure is different in other languages item->setData(Qt::UserRole, itemMap); m_filtersListWidget->addItem(item); } // Let the FilterChecker know m_filterChecker->setFilters(filtersList); } /****************************************************************************/ /********************************* SLOTS ************************************/ /****************************************************************************/ void FilterEditor::onFilterTextChanged(QString text) { m_addFilterButton->setDisabled(text.isEmpty()); } void FilterEditor::onFilterRowChanged(int row) { m_removeFilterButton->setEnabled(row != -1); } void FilterEditor::addFilter() { const QString words = m_filterWordsLineEdit->text().trimmed(); if (words.isEmpty()) { return; } QVariantMap itemMap; itemMap.insert("action", m_actionTypeComboBox->currentIndex()); itemMap.insert("type", m_filterTypeComboBox->currentIndex()); itemMap.insert("text", words); QListWidgetItem *item; item = new QListWidgetItem(m_ruleTemplateString .arg(m_actionTypeComboBox->currentText()) .arg("'" + m_filterTypeComboBox->currentText() + "'") .arg("'" + words + "'")); // Just as in loadFilters(), don't mess with spaces around the parameters item->setData(Qt::UserRole, itemMap); m_filtersListWidget->addItem(item); m_filtersListWidget->setCurrentRow(m_filtersListWidget->count() - 1); m_filterWordsLineEdit->clear(); } void FilterEditor::removeFilter() { int selectedFilter = m_filtersListWidget->currentRow(); qDebug() << "FilterEditor::removeFilter()" << selectedFilter; if (selectedFilter != -1) { QListWidgetItem *removedItem = m_filtersListWidget->takeItem(selectedFilter); delete removedItem; } } void FilterEditor::saveFilters() { qDebug() << "FilterEditor::saveFilters()"; QVariantList filtersList; for (int counter = 0; counter != m_filtersListWidget->count(); ++counter) { QListWidgetItem *item = m_filtersListWidget->item(counter); filtersList.append(item->data(Qt::UserRole)); // data() holds a QVariantMap with the filter } qDebug() << "Filters list: " << filtersList; QSettings settings; settings.setValue("Filters/currentFilters", filtersList); // Send to the FilterChecker too m_filterChecker->setFilters(filtersList); this->hide(); } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////// PROTECTED //////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void FilterEditor::closeEvent(QCloseEvent *event) { this->hide(); event->ignore(); } void FilterEditor::hideEvent(QHideEvent *event) { QSettings settings; settings.setValue("FilterEditor/filterWindowSize", this->size()); event->accept(); } dianara-v1.4.1/src/proxydialog.cpp0000664000175000017500000001463113206625502015250 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "proxydialog.h" ProxyDialog::ProxyDialog(int proxyType, QString hostname, QString port, bool useAuth, QString user, QString password, QWidget *parent) : QWidget(parent) { this->setWindowTitle(tr("Proxy Configuration") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("preferences-system-network", QIcon(":/images/button-configure.png"))); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::WindowModal); this->setMinimumSize(460, 320); m_proxyTypeComboBox = new QComboBox(this); m_proxyTypeComboBox->addItem(tr("Do not use a proxy")); m_proxyTypeComboBox->addItem("SOCKS 5"); m_proxyTypeComboBox->addItem("HTTP"); m_proxyTypeComboBox->setCurrentIndex(proxyType); m_hostnameLineEdit = new QLineEdit(hostname, this); m_hostnameLineEdit->setPlaceholderText("example.org"); m_portLineEdit = new QLineEdit(port, this); m_portLineEdit->setPlaceholderText("1080, 8080..."); // Defaults for socks5 and http m_authCheckBox = new QCheckBox(this); m_authCheckBox->setChecked(useAuth); connect(m_authCheckBox, &QAbstractButton::toggled, this, &ProxyDialog::toggleAuth); m_userLineEdit = new QLineEdit(user, this); m_userLineEdit->setPlaceholderText(tr("Your proxy username")); m_passwordLineEdit = new QLineEdit(password, this); m_passwordLineEdit->setEchoMode(QLineEdit::Password); m_passwordNoteLabel = new QLabel(tr("Note: Password is not stored in a " "secure manner. If you wish, you can " "leave the field empty, and you'll be " "prompted for the password on startup."), this); m_passwordNoteLabel->setWordWrap(true); QFont noteFont; noteFont.setPointSize(noteFont.pointSize() - 1); m_passwordNoteLabel->setFont(noteFont); this->toggleAuth(useAuth); // Enable or disable initially // Bottom m_saveButton = new QPushButton(QIcon::fromTheme("document-save", QIcon(":/images/button-save.png")), tr("&Save"), this); connect(m_saveButton, &QAbstractButton::clicked, this, &ProxyDialog::saveSettings); m_cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("&Cancel"), this); connect(m_cancelButton, &QAbstractButton::clicked, this, &QWidget::hide); // ESC to close m_closeAction = new QAction(this); m_closeAction->setShortcut(QKeySequence(Qt::Key_Escape)); connect(m_closeAction, &QAction::triggered, this, &QWidget::hide); this->addAction(m_closeAction); //////////////////////////////////////////// Layout m_fieldsLayout = new QFormLayout(); m_fieldsLayout->addRow(tr("Proxy &Type"), m_proxyTypeComboBox); m_fieldsLayout->addRow(tr("&Hostname"), m_hostnameLineEdit); m_fieldsLayout->addRow(tr("&Port"), m_portLineEdit); m_fieldsLayout->addRow(tr("Use &Authentication"), m_authCheckBox); m_fieldsLayout->addRow(tr("&User"), m_userLineEdit); m_fieldsLayout->addRow(tr("Pass&word"), m_passwordLineEdit); m_fieldsLayout->addRow(QString(), m_passwordNoteLabel); m_buttonsLayout = new QHBoxLayout(); m_buttonsLayout->setAlignment(Qt::AlignRight); m_buttonsLayout->addWidget(m_saveButton); m_buttonsLayout->addWidget(m_cancelButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->addLayout(m_fieldsLayout); m_mainLayout->addSpacing(16); m_mainLayout->addStretch(1); m_mainLayout->addLayout(m_buttonsLayout); this->setLayout(m_mainLayout); qDebug() << "ProxyDialog created"; } ProxyDialog::~ProxyDialog() { qDebug() << "ProxyDialog destroyed"; } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// SLOTS ////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void ProxyDialog::toggleAuth(bool state) { m_userLineEdit->setEnabled(state); m_passwordLineEdit->setEnabled(state); } void ProxyDialog::saveSettings() { QSettings settings; settings.beginGroup("Configuration"); settings.setValue("proxyType", m_proxyTypeComboBox->currentIndex()); settings.setValue("proxyHostname", m_hostnameLineEdit->text()); settings.setValue("proxyPort", m_portLineEdit->text()); settings.setValue("proxyUseAuth", m_authCheckBox->isChecked()); if (!m_authCheckBox->isChecked()) // If no auth, clear saved username/passwd { m_userLineEdit->clear(); m_passwordLineEdit->clear(); } settings.setValue("proxyUser", m_userLineEdit->text()); // VERY TMP: Saving passwd as base64 for now; FIXME settings.setValue("proxyPassword", m_passwordLineEdit->text().toUtf8() .toBase64()); settings.endGroup(); this->hide(); qDebug() << "ProxyDialog::saveSettings()" << m_hostnameLineEdit->text() << m_portLineEdit->text(); } dianara-v1.4.1/src/datafile.h0000644000175000017500000000243713202667664014137 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 DATAFILE_H #define DATAFILE_H #include #include #include #include #include #include class DataFile : public QObject { Q_OBJECT public: explicit DataFile(QString filename, QObject *parent = 0); ~DataFile(); QVariantList loadData(); bool saveData(QVariantList list); signals: public slots: private: QFile *m_dataFile; }; #endif // DATAFILE_H dianara-v1.4.1/src/commenterblock.h0000644000175000017500000000725313204352160015353 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include "pumpcontroller.h" #include "globalobject.h" #include "composer.h" #include "asobject.h" #include "comment.h" class CommenterBlock : public QWidget { Q_OBJECT public: explicit CommenterBlock(PumpController *pumpController, GlobalObject *globalObject, QString parentId, QString parentAuthorId, bool parentStandalone, QWidget *parent = 0); ~CommenterBlock(); void clearComments(); void setComments(QVariantList commentsList, int commentCount); void appendComment(ASObject *object, bool justOne=false); void updateCommentFromObject(ASObject *object); void setCommentDeletedFromObject(ASObject *object); void updateShowAllLink(); void disableShowAllLink(); void updateFuzzyTimestamps(); void updateAvatarFollowStates(); void adjustCommentsWidth(); void adjustCommentsHeight(); void adjustCommentArea(); void redrawComments(); bool isFullMode(); int getCommentCount(); Composer *getComposer(); signals: void commentSent(QString commentText); void commentUpdated(QString commentId, QString commentText); void allCommentsRequested(); public slots: void setMinimumMode(); void setFullMode(QString initialText=""); void quoteComment(QString content); void editComment(QString id, QString content); void requestAllComments(); void onPostingCommentOk(QString postId); void onPostingCommentFailed(QString postId); void onCommentsNotReceived(QString id); void sendComment(); void scrollCommentsToBottom(); protected: virtual void resizeEvent(QResizeEvent *event); private: QVBoxLayout *m_mainLayout; QGridLayout *m_bottomLayout; QScrollArea *m_commentsScrollArea; QWidget *m_commentsWidget; QVBoxLayout *m_commentsLayout; QTimer *m_scrollToBottomTimer; QString m_parentPostId; QString m_reloadCommentsString; QString m_reloadErrorString; QLabel *m_showAllCommentsLinkLabel; QTimer *m_getAllCommentsTimer; // To get all comments after a delay Composer *m_commentComposer; QPushButton *m_toolsButton; QLabel *m_statusInfoLabel; QPushButton *m_commentButton; QPushButton *m_cancelButton; bool m_editingMode; QString m_editingCommentId; bool m_fullMode; QList m_commentsInBlock; int m_currentCommentCount; bool m_allCommentsShown; QString m_parentPostAuthorId; bool m_parentPostStandalone; PumpController *m_pumpController; GlobalObject *m_globalObject; }; #endif // COMMENTER_H dianara-v1.4.1/src/imageviewer.cpp0000644000175000017500000005537013214530031015205 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 url, QSize imageSize, QString title, QString suggestedFilename, bool isAnimated, QWidget *parent) : QWidget(parent) { if (title.isEmpty()) { title = "<" + tr("Untitled") + ">"; } this->setWindowTitle(tr("Image") + ": " + title + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("folder-image", QIcon(":/images/attached-image.png"))); this->setWindowFlags(Qt::Window); this->setMinimumSize(420, 420); m_imageUrl = url; m_suggestedFilename = suggestedFilename; m_imageIsAnimated = isAnimated; m_zoomLevel = 1.000; m_rotationAngle = 0.000; m_fitToWindow = false; m_loadingLabel = new QLabel(this); m_loadingLabel->setAlignment(Qt::AlignCenter); m_graphicsPixmapItem = new QGraphicsPixmapItem(nullptr); m_graphicsScene = new QGraphicsScene(this); // This label needs to NOT have a parent, to work properly inside the QGraphicsScene m_movieLabel = new QLabel(); // Will be deleted in destructor m_movieLabel->setCursor(Qt::OpenHandCursor); // To match the static GraphicsPixmapItem m_ivGraphicsView = new IvGraphicsView(m_graphicsScene, this); m_ivGraphicsView->setFrameStyle(QFrame::StyledPanel | QFrame::Raised); connect(m_ivGraphicsView, &IvGraphicsView::zoomableModeRequested, this, &ImageViewer::setFullMode); // Buttons m_saveButton = new QPushButton(QIcon::fromTheme("document-save-as", QIcon(":/images/button-save.png")), tr("&Save As..."), this); m_saveButton->setDisabled(true); connect(m_saveButton, &QAbstractButton::clicked, this, &ImageViewer::saveImage); if (m_imageIsAnimated) { m_movie = new QMovie(this); m_restartButton = new QPushButton(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), tr("&Restart", "Restart animation"), this); m_restartButton->setDisabled(true); connect(m_restartButton, &QAbstractButton::clicked, this, &ImageViewer::restartAnimation); } m_fitButton = new QPushButton(QIcon::fromTheme("zoom-fit-best"), tr("Fit", "As in: fit image to window"), this); connect(m_fitButton, &QAbstractButton::clicked, this, &ImageViewer::zoomToFit); m_zoomLabel = new QLabel("000%", this); m_zoomLabel->setAlignment(Qt::AlignCenter); m_zoomLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); m_zoomLabel->setMinimumWidth(m_zoomLabel->sizeHint().width()); m_zoomLabel->clear(); m_fullButton = new QPushButton(QIcon::fromTheme("zoom-original"), QStringLiteral("100%"), this); connect(m_fullButton, &QAbstractButton::clicked, this, &ImageViewer::zoomToFull); m_zoomInButton = new QPushButton(QIcon::fromTheme("zoom-in"), QStringLiteral("+"), this); m_zoomInButton->setShortcut(QKeySequence(Qt::Key_Plus)); m_zoomInButton->setDisabled(true); connect(m_zoomInButton, &QAbstractButton::clicked, this, &ImageViewer::zoomIn); connect(m_ivGraphicsView, &IvGraphicsView::zoomInRequested, m_zoomInButton, &QAbstractButton::click); m_zoomOutButton = new QPushButton(QIcon::fromTheme("zoom-out"), QStringLiteral("-"), this); m_zoomOutButton->setShortcut(QKeySequence(Qt::Key_Minus)); m_zoomOutButton->setDisabled(true); connect(m_zoomOutButton, &QAbstractButton::clicked, this, &ImageViewer::zoomOut); connect(m_ivGraphicsView, &IvGraphicsView::zoomOutRequested, m_zoomOutButton, &QAbstractButton::click); m_rotateLeftButton = new QPushButton(QIcon::fromTheme("object-rotate-left", QIcon(":/images/button-rotleft.png")), QString(), this); m_rotateLeftButton->setToolTip(tr("Rotate image to the left", "RTL: This actually means LEFT, anticlockwise")); m_rotateLeftButton->setDisabled(true); connect(m_rotateLeftButton, &QAbstractButton::clicked, this, &ImageViewer::rotateLeft); m_rotateRightButton = new QPushButton(QIcon::fromTheme("object-rotate-right", QIcon(":/images/button-rotright.png")), QString(), this); m_rotateRightButton->setToolTip(tr("Rotate image to the right", "RTL: This actually means RIGHT, clockwise")); m_rotateRightButton->setDisabled(true); connect(m_rotateRightButton, &QAbstractButton::clicked, this, &ImageViewer::rotateRight); m_infoLabel = new QLabel("~ ~ ~ ~ ~ ~ ~", this); m_infoLabel->setAlignment(Qt::AlignCenter); m_closeButton = new QPushButton(QIcon::fromTheme("window-close", QIcon(":/images/button-close.png")), tr("&Close"), this); connect(m_closeButton, &QAbstractButton::clicked, this, &QWidget::close); // This needs to be created after the buttons, since it uses info from them this->createContextMenu(); // Layout m_fitFullLayout = new QHBoxLayout(); m_fitFullLayout->setContentsMargins(0, 0, 0, 0); m_fitFullLayout->setSpacing(0); m_fitFullLayout->addWidget(m_fitButton); m_fitFullLayout->addWidget(m_zoomLabel); m_fitFullLayout->addWidget(m_fullButton); m_buttonsLayout = new QHBoxLayout(); m_buttonsLayout->addWidget(m_saveButton); if (m_imageIsAnimated) { m_buttonsLayout->addWidget(m_restartButton); } m_buttonsLayout->addLayout(m_fitFullLayout); m_buttonsLayout->addWidget(m_zoomInButton); m_buttonsLayout->addWidget(m_zoomOutButton); m_buttonsLayout->addWidget(m_rotateLeftButton); m_buttonsLayout->addWidget(m_rotateRightButton); m_buttonsLayout->addWidget(m_infoLabel, 1); m_buttonsLayout->addWidget(m_closeButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->addWidget(m_loadingLabel); m_mainLayout->addWidget(m_ivGraphicsView); m_mainLayout->addLayout(m_buttonsLayout); this->setLayout(m_mainLayout); this->updateButtons(); // For big enough images, "Fit mode" will be automatically enabled by timer m_autoFitTimer = new QTimer(this); m_autoFitTimer->setSingleShot(true); m_autoFitTimer->setInterval(50); connect(m_autoFitTimer, &QTimer::timeout, this, &ImageViewer::checkAutoFit); this->loadImage(m_imageUrl, imageSize); // Set initial window size according to image size and desktop (screen) size QDesktopWidget desktopWidget; int desktopWidth = desktopWidget.availableGeometry().width(); int desktopHeight = desktopWidget.availableGeometry().height(); qDebug() << "Available desktop area: " << desktopWidth << "x" << desktopHeight; int imageWidth = imageSize.width(); int imageHeight = imageSize.height(); // If image is already loaded, use its actual w/h values, just in case if (!m_originalPixmap.isNull()) { imageWidth = m_originalPixmap.width(); imageHeight = m_originalPixmap.height(); } int windowWidth = qMin(imageWidth + 200, // Some margin for the window itself, desktopWidth); // and extra width for the buttons int windowHeight = qMin(imageHeight + 160, desktopHeight); // Calculate optimal minimum width for the window, // in order for all the buttons to be readable int desiredWidth = m_saveButton->sizeHint().width() + m_fitButton->sizeHint().width() + m_zoomLabel->sizeHint().width() + m_fullButton->sizeHint().width() + m_zoomInButton->sizeHint().width() + m_zoomOutButton->sizeHint().width() + m_rotateLeftButton->sizeHint().width() + m_rotateRightButton->sizeHint().width() + m_infoLabel->sizeHint().width() + m_closeButton->sizeHint().width() + (m_buttonsLayout->spacing() * 12); // With a few extra spacings if (m_imageIsAnimated) { desiredWidth += m_restartButton->sizeHint().width() + m_buttonsLayout->spacing(); } qDebug() << "Sum of button sizeHints, minimal optimal width:" << desiredWidth; if (desiredWidth > windowWidth && desiredWidth < desktopWidth) { windowWidth = desiredWidth; } // Resize window to our desired size, bigger than the image if the image is small this->resize(windowWidth, windowHeight); // Ensure these are disabled until image is actually loaded m_fitButton->setDisabled(true); m_fullButton->setDisabled(true); m_zoomInButton->setDisabled(true); m_zoomOutButton->setDisabled(true); m_autoFitTimer->start(); qDebug() << "ImageViewer created"; } ImageViewer::~ImageViewer() { delete m_movieLabel; // Since it needed to have no parent delete m_graphicsPixmapItem; qDebug() << "ImageViewer destroyed"; } /* * Context menu entries are connected to the corresponding buttons simulated * click, to provide visual feedback. * * Icons will be taken from them where possible. * */ void ImageViewer::createContextMenu() { m_saveAction = new QAction(m_saveButton->icon(), tr("Save Image..."), this); m_saveAction->setShortcut(QKeySequence("Ctrl+S")); m_saveAction->setDisabled(true); connect(m_saveAction, &QAction::triggered, m_saveButton, &QAbstractButton::animateClick); // This one gets icon and name from Fit button initially, // and from 100% button when toggled m_toggleFitFullAction = new QAction(m_fitButton->icon(), m_fitButton->text(), this); m_toggleFitFullAction->setShortcut(QKeySequence(Qt::Key_F)); m_toggleFitFullAction->setDisabled(true); connect(m_toggleFitFullAction, &QAction::triggered, this, &ImageViewer::onFitFullToggled); connect(m_ivGraphicsView, &IvGraphicsView::doubleClicked, m_toggleFitFullAction, &QAction::trigger); // Name of this one comes from the rotate-left button's tooltip, already created m_rotateLeftAction = new QAction(m_rotateLeftButton->icon(), m_rotateLeftButton->toolTip(), this); m_rotateLeftAction->setShortcut(QKeySequence("Ctrl+Left")); m_rotateLeftAction->setDisabled(true); connect(m_rotateLeftAction, &QAction::triggered, m_rotateLeftButton, &QAbstractButton::animateClick); // Name of this action comes from the rotate-right button, already created m_rotateRightAction = new QAction(m_rotateRightButton->icon(), m_rotateRightButton->toolTip(), this); m_rotateRightAction->setShortcut(QKeySequence("Ctrl+Right")); m_rotateRightAction->setDisabled(true); connect(m_rotateRightAction, &QAction::triggered, m_rotateRightButton, &QAbstractButton::animateClick); m_closeAction = new QAction(m_closeButton->icon(), tr("Close Viewer"), this); m_closeAction->setShortcut(QKeySequence(Qt::Key_Escape)); connect(m_closeAction, &QAction::triggered, m_closeButton, &QAbstractButton::animateClick); m_contextMenu = new QMenu(QStringLiteral("*imageViewerMenu*"), this); m_contextMenu->addAction(m_saveAction); m_contextMenu->addSeparator(); m_contextMenu->addAction(m_toggleFitFullAction); m_contextMenu->addAction(m_rotateLeftAction); m_contextMenu->addAction(m_rotateRightAction); m_contextMenu->addSeparator(); m_contextMenu->addAction(m_closeAction); // Make the shortcuts work this->addAction(m_saveAction); this->addAction(m_toggleFitFullAction); this->addAction(m_rotateLeftAction); this->addAction(m_rotateRightAction); this->addAction(m_closeAction); } void ImageViewer::loadImage(QString url, QSize expectedSize) { m_originalFileUri = MiscHelpers::getCachedImageFilename(url); m_originalPixmap = QPixmap(m_originalFileUri); if (m_originalPixmap.isNull()) { m_ivGraphicsView->hide(); QString expectedResolution = MiscHelpers::resolutionString(expectedSize.width(), expectedSize.height()); m_loadingLabel->setText("" "


" "" + tr("Downloading full image...") + "" "

" "(" + expectedResolution + ")"); } else { m_loadingLabel->clear(); m_loadingLabel->hide(); m_ivGraphicsView->show(); QString resolution = MiscHelpers::resolutionString(m_originalPixmap.width(), m_originalPixmap.height()); QString imageDetails = resolution + " ~ " + MiscHelpers::fileSizeString(m_originalFileUri); m_infoLabel->setText(imageDetails); if (m_imageIsAnimated) { m_movie->setFileName(m_originalFileUri); m_movie->start(); m_movieLabel->setMovie(m_movie); m_labelProxyWidget = m_graphicsScene->addWidget(m_movieLabel); m_restartButton->setEnabled(true); } else { m_graphicsPixmapItem->setPixmap(m_originalPixmap); m_graphicsScene->addItem(m_graphicsPixmapItem); } this->drawImage(); m_saveButton->setEnabled(true); m_saveAction->setEnabled(true); m_rotateLeftButton->setEnabled(true); m_rotateRightButton->setEnabled(true); m_rotateLeftAction->setEnabled(true); m_rotateRightAction->setEnabled(true); m_toggleFitFullAction->setEnabled(true); m_autoFitTimer->start(); } } void ImageViewer::drawImage() { m_ivGraphicsView->resetTransform(); // Reset and re-scaled+re-rotated every time //// FIXME: This reset causes issues when zooming towards a corner, /// and then zooming towards the opposite corner m_ivGraphicsView->rotate(m_rotationAngle); if (m_fitToWindow) { m_zoomLevel = m_ivGraphicsView->getScaleToFit(m_originalPixmap.width(), m_originalPixmap.height(), m_rotationAngle); } else { toggleZoomButtons(); } m_ivGraphicsView->scale(m_zoomLevel, m_zoomLevel); m_zoomLabel->setText(QString("%1%").arg(m_zoomLevel * 100, 3, 'f', 0)); this->updateButtons(); } void ImageViewer::toggleZoomButtons() { m_zoomInButton->setDisabled(m_zoomLevel >= 5.00); // Some rounding issue sometimes makes this fail with <= 0.05, so... m_zoomOutButton->setDisabled(m_zoomLevel < 0.06); m_fullButton->setDisabled(m_zoomLevel == 1.0); } void ImageViewer::updateButtons() { if (m_fitToWindow) { m_fitButton->setDisabled(true); m_fullButton->setEnabled(true); m_zoomInButton->setVisible(false); m_zoomInButton->setEnabled(false); m_zoomOutButton->setVisible(false); m_zoomOutButton->setEnabled(false); m_toggleFitFullAction->setIcon(m_fullButton->icon()); m_toggleFitFullAction->setText(m_fullButton->text()); } else { m_fullButton->setDisabled(true); m_fitButton->setEnabled(true); m_zoomInButton->setVisible(true); m_zoomOutButton->setVisible(true); m_toggleFitFullAction->setIcon(m_fitButton->icon()); m_toggleFitFullAction->setText(m_fitButton->text()); this->toggleZoomButtons(); } } /****************************************************************************/ /************************************ SLOTS *********************************/ /****************************************************************************/ void ImageViewer::reloadImage(QString url) { if (url == m_imageUrl) { this->loadImage(url, QSize()); } } void ImageViewer::onImageFailed(QString url) { if (url == m_imageUrl) { m_loadingLabel->setText("" "


" "" + tr("Error downloading image!") + "

" + tr("Try again later.") + "
"); } } void ImageViewer::saveImage() { bool savedCorrectly; QString filename; filename = QFileDialog::getSaveFileName(this, tr("Save Image As..."), QDir::homePath() + "/" + m_suggestedFilename, tr("Image files") + " (*.jpg *.png);;" + tr("All files") + " (*)"); if (!filename.isEmpty()) { // Save pixmap from original file savedCorrectly = QPixmap(m_originalFileUri).save(filename); // FIXME: change this to directly copy the unmodified original instead? 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)); } } } void ImageViewer::restartAnimation() { if (m_imageIsAnimated) { m_movie->stop(); m_movie->start(); } } void ImageViewer::zoomToFit() { m_fitToWindow = true; this->drawImage(); } void ImageViewer::zoomToFull() { m_fitToWindow = false; m_zoomLevel = 1.000; // Reset this->drawImage(); } /* * To be called on wheelEvent, instead of zoomToFit, not resetting zoom level * */ void ImageViewer::setFullMode() { // Only if not already in full mode, to avoid weird effects with wheel events if (m_fitToWindow) { m_fitToWindow = false; this->drawImage(); } } void ImageViewer::zoomIn() { m_fitToWindow = false; if (m_zoomLevel < 5.0) { if (m_zoomLevel >= 1.0) { m_zoomLevel += 0.1; if (m_zoomLevel > 5.0) { m_zoomLevel = 5.0; } } else { m_zoomLevel += 0.05; } this->drawImage(); } } void ImageViewer::zoomOut() { m_fitToWindow = false; if (m_zoomLevel > 0.05) { if (m_zoomLevel >= 1.1) { m_zoomLevel -= 0.1; } else { m_zoomLevel -=0.05; if (m_zoomLevel < 0.05) { m_zoomLevel = 0.05; } } this->drawImage(); } } void ImageViewer::checkAutoFit() { if (m_originalPixmap.isNull()) { // Avoid messing with the buttons until the image is loaded return; } zoomToFit(); if (m_originalPixmap.width() < m_ivGraphicsView->viewport()->width() && m_originalPixmap.height() < m_ivGraphicsView->viewport()->height()) { zoomToFull(); } } void ImageViewer::rotateLeft() { m_rotationAngle -= 90.0; if (m_rotationAngle == -360.0) { m_rotationAngle = 0.0; } this->drawImage(); } void ImageViewer::rotateRight() { m_rotationAngle += 90.0; if (m_rotationAngle == 360.0) { m_rotationAngle = 0.0; } this->drawImage(); } void ImageViewer::onFitFullToggled() { if (m_fitToWindow) { m_fullButton->animateClick(); } else { m_fitButton->animateClick(); } } /****************************************************************************/ /********************************** PROTECTED *******************************/ /****************************************************************************/ void ImageViewer::closeEvent(QCloseEvent *event) { qDebug() << "ImageViewer::closeEvent(); hiding and destroying widget!"; event->ignore(); this->hide(); this->deleteLater(); } void ImageViewer::showEvent(QShowEvent *event) { qDebug() << "ImageViewer::showEvent()"; if (!m_originalPixmap.isNull()) { this->drawImage(); } event->accept(); } void ImageViewer::hideEvent(QHideEvent *event) { qDebug() << "ImageViewer::hideEvent()"; event->accept(); } void ImageViewer::resizeEvent(QResizeEvent *event) { qDebug() << "ImageViewer::resizeEvent()"; if (!m_originalPixmap.isNull()) { this->drawImage(); } event->accept(); } void ImageViewer::contextMenuEvent(QContextMenuEvent *event) { m_contextMenu->exec(event->globalPos()); } dianara-v1.4.1/src/comment.cpp0000644000175000017500000004542013211036027014341 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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, GlobalObject *globalObject, ASObject *commentObject, QWidget *parent) : QFrame(parent) { m_pumpController = pumpController; m_globalObject = globalObject; m_commentIsDeleted = false; m_commentHasImages = false; commentObject->setParent(this); // Reparent the passed object QSizePolicy sizePolicy; sizePolicy.setHeightForWidth(false); sizePolicy.setWidthForHeight(false); sizePolicy.setHorizontalPolicy(QSizePolicy::Ignored); sizePolicy.setVerticalPolicy(QSizePolicy::Maximum); // Previously QSizePolicy::Preferred this->setSizePolicy(sizePolicy); this->setMinimumSize(10, 10); // Ensure something's visible at any time this->setMaximumHeight(4096); m_commentId = commentObject->getId(); m_objectType = commentObject->getType(); m_commentAuthorId = commentObject->author()->getId(); bool commentIsOwn; if (m_commentAuthorId == m_pumpController->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 m_avatarButton = new AvatarButton(commentObject->author(), m_pumpController, m_globalObject, m_globalObject->getCommentAvatarSize(), this); QFont commentsFont; commentsFont.fromString(m_globalObject->getCommentsFont()); // Name, with ID as tooltip QFont metadataFont; metadataFont.setPointSize(metadataFont.pointSize() - 1); // 1 point less than default metadataFont.setBold(true); if (commentIsOwn) { metadataFont.setItalic(true); } m_fullNameLabel = new QLabel(commentObject->author()->getNameWithFallback(), this); m_fullNameLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); m_fullNameLabel->setFont(metadataFont); m_fullNameLabel->setToolTip(m_commentAuthorId); // Timestamps metadataFont.setBold(false); metadataFont.setItalic(true); m_timestampLabel = new HClabel(QString(), this); m_timestampLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); m_timestampLabel->setWordWrap(false); // ON by default in HClabel m_timestampLabel->setFont(metadataFont); // Like and Delete "buttons" metadataFont.setBold(true); metadataFont.setItalic(false); m_likeLabel = new QLabel(QStringLiteral("*like*"), this); m_likeLabel->setContextMenuPolicy(Qt::NoContextMenu); m_likeLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); m_likeLabel->setFont(metadataFont); m_likeLabel->setToolTip("" + tr("Like or unlike this comment")); connect(m_likeLabel, &QLabel::linkActivated, this, &Comment::likeComment); m_quoteLabel = new QLabel(" " + tr("Quote", "This is a verb, infinitive") + " ", this); // Spaces around the link help the linkHovered signal work better m_quoteLabel->setContextMenuPolicy(Qt::NoContextMenu); m_quoteLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); m_quoteLabel->setFont(metadataFont); m_quoteLabel->setToolTip(QStringLiteral("") + tr("Reply quoting this comment")); connect(m_quoteLabel, &QLabel::linkHovered, // FIXME: issues with this signal this, &Comment::saveCommentSelectedText); connect(m_quoteLabel, &QLabel::linkActivated, this, &Comment::quoteComment); m_editLabel = new QLabel("" + tr("Edit") + "", this); m_editLabel->setContextMenuPolicy(Qt::NoContextMenu); m_editLabel->setAlignment(Qt::AlignTop | Qt::AlignRight); m_editLabel->setFont(metadataFont); m_editLabel->setToolTip("" + tr("Modify this comment")); connect(m_editLabel, &QLabel::linkActivated, this, &Comment::editComment); m_deleteLabel = new QLabel("" + tr("Delete") + "", this); m_deleteLabel->setContextMenuPolicy(Qt::NoContextMenu); m_deleteLabel->setAlignment(Qt::AlignTop | Qt::AlignRight); m_deleteLabel->setFont(metadataFont); m_deleteLabel->setToolTip(QStringLiteral("") + tr("Erase this comment")); connect(m_deleteLabel, &QLabel::linkActivated, this, &Comment::deleteComment); // The likes count m_likesCountLabel = new HClabel(QString(), this); m_likesCountLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); metadataFont.setBold(false); m_likesCountLabel->setFont(metadataFont); // Main content, the comment itself m_contentLabel = new QLabel(this); m_contentLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); m_contentLabel->setFont(commentsFont); m_contentLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); // To help screen readers add Qt::TextSelectableByKeyboard flag too; // Not adding for now, because it doesn't really help much. This will work better with Qt5 m_contentLabel->setWordWrap(true); m_contentLabel->setOpenExternalLinks(true); m_contentLabel->setTextFormat(Qt::RichText); m_contentLabel->setSizePolicy(sizePolicy); m_contentLabel->setMaximumHeight(4096); connect(m_contentLabel, &QLabel::linkHovered, this, &Comment::showUrlInfo); // This is used to draw a colored vertical line, as a hint m_hintWidget = new QWidget(this); m_hintWidget->hide(); // Layout m_leftLayout = new QVBoxLayout(); m_leftLayout->setContentsMargins(0, 0, 0, 0); m_leftLayout->setAlignment(Qt::AlignLeft); m_leftLayout->addWidget(m_avatarButton, 0, Qt::AlignLeft | Qt::AlignTop); m_leftLayout->addWidget(m_likesCountLabel, 0, Qt::AlignHCenter | Qt::AlignTop); m_leftLayout->addStretch(); m_rightTopLayout = new QHBoxLayout(); m_rightTopLayout->addWidget(m_fullNameLabel, 0, Qt::AlignLeft); m_rightTopLayout->addWidget(m_timestampLabel, 0, Qt::AlignLeft); m_rightTopLayout->addWidget(m_likeLabel, 0, Qt::AlignLeft); m_rightTopLayout->addWidget(m_quoteLabel, 0, Qt::AlignLeft); m_rightTopLayout->addStretch(1); if (commentIsOwn) { m_rightTopLayout->addWidget(m_editLabel, 0, Qt::AlignRight); m_rightTopLayout->addWidget(m_deleteLabel, 0, Qt::AlignRight); } else { // Since these widgets are initialized with a parent, hide them when not needed m_editLabel->hide(); m_deleteLabel->hide(); } m_rightLayout = new QVBoxLayout(); m_rightLayout->addLayout(m_rightTopLayout, 0); m_rightLayout->addSpacing(4); // 4px vertical space separation m_rightLayout->addWidget(m_contentLabel, 1, Qt::AlignTop); m_rightLayout->addSpacing(1); // and 1px more as margin m_mainLayout = new QHBoxLayout(); m_mainLayout->setContentsMargins(2, 2, 2, 2); m_mainLayout->addWidget(m_hintWidget); m_mainLayout->addLayout(m_leftLayout); m_mainLayout->addLayout(m_rightLayout); this->setLayout(m_mainLayout); this->updateDataFromObject(commentObject); qDebug() << "Comment created" << m_commentId; } Comment::~Comment() { qDebug() << "Comment destroyed" << m_commentId; } /* * Fill in or replace data such as timestamps, title, contents, * number of likes and shares, etc, from object data * */ void Comment::updateDataFromObject(ASObject *object) { // Timestamps m_createdAt = object->getCreatedAt(); m_updatedAt = object->getUpdatedAt(); QString timestampTooltip = tr("Posted on %1") .arg(Timestamp::localTimeDate(m_createdAt)); if (m_createdAt != m_updatedAt) { timestampTooltip.append("

" + tr("Modified on %1") .arg(Timestamp::localTimeDate(m_updatedAt))); } m_timestampLabel->setToolTip(timestampTooltip); // Precise time on tooltip this->setFuzzyTimestamps(); this->setLikesCount(object->getLikesCount(), object->getLastLikesList()); m_commentIsLiked = object->isLiked(); this->fixLikeLabelText(); // The comment itself m_commentOriginalText = object->getContent(); m_pendingImagesList = MiscHelpers::htmlWithReplacedImages(m_commentOriginalText, 128); // Arbitrary (initial) width m_pendingImagesList.removeFirst(); // First one is the HTML with images replaced this->getPendingImages(); this->setCommentContents(); } void Comment::fixLikeLabelText() { if (m_commentIsLiked) { m_likeLabel->setText("" + tr("Unlike") +""); } else { m_likeLabel->setText("" + tr("Like") +""); } } void Comment::setLikesCount(int count, QVariantList namesVariantList) { if (count != 0) { QString likesString = ASObject::personStringFromList(namesVariantList, count); if (count == 1) { likesString = tr("%1 likes this comment", "Singular: %1=name of just " "1 person").arg(likesString); } else // Several people { likesString = tr("%1 like this comment", "Plural: %1=list of people like John, " "Jane, Smith").arg(likesString); } m_likesCountLabel->setBaseText(QString::fromUtf8("\342\231\245 ") // Heart symbol + QString::number(count)); // Set tooltip as HTML, so it gets wordwrapped m_likesCountLabel->setToolTip(QStringLiteral("") + likesString); m_likesCountLabel->show(); } else { m_likesCountLabel->clear(); m_likesCountLabel->setToolTip(QString()); m_likesCountLabel->hide(); } } void Comment::setFuzzyTimestamps() { QString timestamp = Timestamp::fuzzyTime(m_createdAt); if (m_createdAt != m_updatedAt) // Comment has been edited { timestamp.prepend(QStringLiteral("**")); // FIXME, somehow show edit time } m_timestampLabel->setBaseText(timestamp); } void Comment::syncAvatarFollowState() { m_avatarButton->syncFollowState(); } /* * Set the contents of the comment, parsing images, etc. * */ void Comment::setCommentContents() { const int imageWidth = m_contentLabel->width() - 20; // Kinda TMP QStringList commentImageList = MiscHelpers::htmlWithReplacedImages(m_commentOriginalText, imageWidth); if (commentImageList.size() > 1) // First is the HTML itself { m_commentHasImages = true; } // Comment's HTML with images replaced const QString commentContents = commentImageList.takeAt(0); m_contentLabel->setText(commentContents); } void Comment::onResize() { m_contentLabel->ensurePolished(); const int height = m_contentLabel->heightForWidth(m_contentLabel->width()); m_contentLabel->setMinimumHeight(height); /* Forcing maximum height cuts images and is not really needed when * images are present, thanks to fixes elsewhere, so enforce it only * when there are none * */ if (!m_commentHasImages) { m_contentLabel->setMaximumHeight(height); } } void Comment::getPendingImages() { if (!m_pendingImagesList.isEmpty()) { foreach (QString imageUrl, m_pendingImagesList) { m_pumpController->enqueueImageForDownload(imageUrl); } connect(m_pumpController, &PumpController::imageStored, this, &Comment::redrawImages); } } QString Comment::getObjectId() { return m_commentId; } /* * A thin line on left side as hint to indicate comment is yours * or from the author of the parent post * */ void Comment::setHint(QString color) { if (color.isEmpty()) { color = "palette(highlight)"; } // Transparent to your color to transparent gradient QString css = QString("QWidget " "{ background-color: " " qlineargradient(spread:pad, " " x1:0, y1:0, x2:1, y2:0, " " stop:0 rgba(0, 0, 0, 0), " " stop:0.25 %1, stop:0.75 %1, " " stop:1 rgba(0, 0, 0, 0) ); " "}").arg(color); m_hintWidget->setStyleSheet(css); m_hintWidget->setFixedWidth(4); // 4 px -- FIXME: should not be hardcoded m_hintWidget->show(); } void Comment::setCommentDeleted(QString deletedTime) { if (m_commentIsDeleted) { return; // Was already deleted, so just ignore } m_commentIsDeleted = true; if (!m_commentOriginalText.isEmpty()) { m_commentOriginalText.prepend("
"); // -------- } m_commentOriginalText.prepend("
" + deletedTime + "
"); qDebug() << "This comment was deleted on" << deletedTime; this->setCommentContents(); this->onResize(); this->setDisabled(true); } /****************************************************************************/ /******************************** SLOTS *************************************/ /****************************************************************************/ void Comment::likeComment(QString clickedLink) { if (clickedLink == "like://") { m_commentIsLiked = true; } else // unlike:// { m_commentIsLiked = false; } m_pumpController->likePost(m_commentId, m_objectType, m_commentAuthorId, m_commentIsLiked); this->fixLikeLabelText(); } /* * This will be called when hovering the "Quote" link * * Store the selected text, so it can be used when actually quoting * */ void Comment::saveCommentSelectedText() { m_commentSelectedText = m_contentLabel->selectedText(); // TMP / FIXME: issues here; the signal is not emitted every time qDebug() << "### Comment SELECTED TEXT: " << m_contentLabel->selectedText(); } /* * Take the contents of a comment and put them as a quote block * in the comment composer. * * If some text has been selected, saveCommentSelectedText() * will have it stored in a variable, used here. * */ void Comment::quoteComment() { QString quotedComment; if (m_commentSelectedText.isEmpty()) { // Quote full comment quotedComment = MiscHelpers::quotedText(m_fullNameLabel->text(), m_contentLabel->text()); } else { // Quote the selection only m_commentSelectedText.prepend("[...] "); m_commentSelectedText.append(" [...]"); quotedComment = MiscHelpers::quotedText(m_fullNameLabel->text(), m_commentSelectedText); m_commentSelectedText.clear(); } emit commentQuoteRequested(quotedComment); } void Comment::editComment() { emit commentEditRequested(m_commentId, m_commentOriginalText); } 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"), QString(), 1, 1); if (confirmation == 0) { qDebug() << "Deleting comment" << m_commentId; m_pumpController->deletePost(m_commentId, m_objectType); const QString timeNow = QDateTime::currentDateTimeUtc() .toString(Qt::ISODate); this->setCommentDeleted(ASObject::makeDeletedOnString(timeNow)); } else { qDebug() << "Confirmation cancelled, not deleting the comment"; } } /* * Show the URL of a link hovered in a comment * */ void Comment::showUrlInfo(QString url) { if (!url.isEmpty()) { m_pumpController->showTransientMessage(url); qDebug() << "Link hovered in comment:" << url; } else { m_pumpController->showTransientMessage(QString()); } } /* * Redraw comment contents after receiving downloaded images * */ void Comment::redrawImages(QString imageUrl) { if (m_pendingImagesList.contains(imageUrl)) { m_pendingImagesList.removeAll(imageUrl); if (m_pendingImagesList.isEmpty()) // If there are no more, disconnect { disconnect(m_pumpController, &PumpController::imageStored, this, &Comment::redrawImages); } // Redraw for every image setCommentContents(); } } /****************************************************************************/ /****************************** PROTECTED ***********************************/ /****************************************************************************/ /* * Ensure URL info in statusbar is hidden when the mouse leaves the comment * */ void Comment::leaveEvent(QEvent *event) { m_pumpController->showTransientMessage(QString()); event->accept(); } void Comment::resizeEvent(QResizeEvent *event) { //qDebug() << "Comment::resizeEvent()" // << event->oldSize() << ">" << event->size(); this->onResize(); event->accept(); } dianara-v1.4.1/src/images/0000755000175000017500000000000012157073165013447 5ustar janjandianara-v1.4.1/src/pageselector.cpp0000644000175000017500000002035413206632340015357 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "pageselector.h" PageSelector::PageSelector(QWidget *parent) : QWidget(parent) { this->setWindowTitle(tr("Jump to page") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("go-next-view-page", QIcon(":/images/button-next.png"))); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::ApplicationModal); m_initialPage = 1; // Top m_messageLabel = new QLabel(tr("Page number:"), this); m_pageNumberSpinbox = new QSpinBox(this); m_pageNumberSpinbox->setRange(1, 1); // 1 to totalPages, but initially 1-1 m_pageNumberSpinbox->setValue(1); connect(m_pageNumberSpinbox, &QAbstractSpinBox::editingFinished, this, &PageSelector::onPageNumberEntered); m_rangeLabel = new QLabel(this); // [ 1 ~ ### ] m_firstButton = new QPushButton(QIcon::fromTheme("go-first-view-page", QIcon(":/images/button-previous.png")), tr("&First", "As in: first page"), this); connect(m_firstButton, &QAbstractButton::clicked, this, &PageSelector::setToFirstPage); m_lastButton = new QPushButton(QIcon::fromTheme("go-last-view-page", QIcon(":/images/button-next.png")), tr("&Last", "As in: last page"), this); connect(m_lastButton, &QAbstractButton::clicked, this, &PageSelector::setToLastPage); // Middle m_newerButton = new QPushButton("< " + tr("Newer", "As in: newer pages"), this); connect(m_newerButton, &QAbstractButton::clicked, this, &PageSelector::decreasePage); m_pageNumberSlider = new QSlider(Qt::Horizontal, this); m_pageNumberSlider->setRange(1, 1); // Initially m_pageNumberSlider->setValue(1); m_pageNumberSlider->setTickPosition(QSlider::TicksBelow); // Make spinbox and slider keep in sync connect(m_pageNumberSlider, &QAbstractSlider::valueChanged, m_pageNumberSpinbox, &QSpinBox::setValue); // And also enable/disable buttons depending on current page connect(m_pageNumberSpinbox, SIGNAL(valueChanged(int)), // Old-style, overload this, SLOT(onPageNumberChanged(int))); m_olderButton = new QPushButton(tr("Older", "As in: older pages") + " >", this); connect(m_olderButton, &QAbstractButton::clicked, this, &PageSelector::increasePage); // Bottom m_goButton = new QPushButton(QIcon::fromTheme("go-next-view-page", QIcon(":/images/button-next.png")), " " + tr("&Go") + " >> ", this); m_goButton->setDisabled(true); connect(m_goButton, &QAbstractButton::clicked, this, &PageSelector::goToPage); m_closeButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("&Cancel"), this); connect(m_closeButton, &QAbstractButton::clicked, this, &QWidget::hide); m_closeAction = new QAction(this); m_closeAction->setShortcut(QKeySequence(Qt::Key_Escape)); connect(m_closeAction, &QAction::triggered, this, &QWidget::hide); this->addAction(m_closeAction); // Layout m_topLayout = new QHBoxLayout(); m_topLayout->addWidget(m_messageLabel); m_topLayout->addSpacing(4); m_topLayout->addWidget(m_pageNumberSpinbox); m_topLayout->addSpacing(4); m_topLayout->addWidget(m_rangeLabel); m_topLayout->addSpacing(48); m_topLayout->addStretch(1); m_topLayout->addWidget(m_firstButton); m_topLayout->addWidget(m_lastButton); m_middleLayout = new QHBoxLayout(); m_middleLayout->addWidget(m_newerButton); m_middleLayout->addSpacing(4); m_middleLayout->addWidget(m_pageNumberSlider); m_middleLayout->addSpacing(4); m_middleLayout->addWidget(m_olderButton); m_bottomLayout = new QHBoxLayout(); m_bottomLayout->setAlignment(Qt::AlignRight); m_bottomLayout->addWidget(m_goButton); m_bottomLayout->addWidget(m_closeButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->addLayout(m_topLayout); m_mainLayout->addSpacing(24); m_mainLayout->addStretch(1); m_mainLayout->addLayout(m_middleLayout); m_mainLayout->addStretch(2); m_mainLayout->addSpacing(32); m_mainLayout->addLayout(m_bottomLayout); this->setLayout(m_mainLayout); qDebug() << "PageSelector created"; } PageSelector::~PageSelector() { qDebug() << "PageSelector destroyed"; } /* * To be called from TimeLine() * * Set current page based on current Timeline page, and total page count * */ void PageSelector::showForPage(int currentPage, int totalPageCount) { m_initialPage = currentPage; m_pageNumberSpinbox->setRange(1, totalPageCount); m_pageNumberSpinbox->setValue(currentPage); m_pageNumberSlider->setRange(1, totalPageCount); // Value will get sync'ed from pageNumberSpinbox m_pageNumberSlider->setTickInterval(totalPageCount / 4); // One tick every 1/4th m_rangeLabel->setText(QString("[ 1 ~ %1 ]") .arg(QLocale::system().toString(totalPageCount))); // Enable/disable needed buttons this->onPageNumberChanged(currentPage); this->show(); m_pageNumberSpinbox->setFocus(); m_pageNumberSpinbox->selectAll(); // So page numbers can be entered immediately } //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////// SLOTS ///////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// void PageSelector::setToFirstPage() { m_pageNumberSpinbox->setValue(m_pageNumberSpinbox->minimum()); } void PageSelector::setToLastPage() { m_pageNumberSpinbox->setValue(m_pageNumberSpinbox->maximum()); } void PageSelector::decreasePage() { m_pageNumberSpinbox->setValue(m_pageNumberSpinbox->value() - 1); } void PageSelector::increasePage() { m_pageNumberSpinbox->setValue(m_pageNumberSpinbox->value() + 1); } void PageSelector::goToPage() { if (m_pageNumberSpinbox->value() != m_initialPage) { emit pageJumpRequested(m_pageNumberSpinbox->value()); this->hide(); } } void PageSelector::onPageNumberEntered() { // If spinbox still has focus, it means ENTER was pressed if (m_pageNumberSpinbox->hasFocus()) { this->goToPage(); } // If not, it means focus went somewhere else, // which also causes editingFinished() to be emitted } /* * Keep slider bar in sync with spinbox, and enable/disable certain buttons * depending on currently selected page * */ void PageSelector::onPageNumberChanged(int selectedPage) { m_pageNumberSlider->setValue(selectedPage); m_newerButton->setEnabled(selectedPage > 1); m_olderButton->setEnabled(selectedPage < m_pageNumberSpinbox->maximum()); m_firstButton->setDisabled(selectedPage == 1); m_lastButton->setDisabled(selectedPage == m_pageNumberSpinbox->maximum()); m_goButton->setDisabled(selectedPage == m_initialPage); } dianara-v1.4.1/src/mischelpers.cpp0000644000175000017500000004744413173644772015250 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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, all static functions } QString MiscHelpers::getCachedAvatarFilename(QString url) { QString localFilename; if (!url.isEmpty()) { localFilename = QStandardPaths::standardLocations(QStandardPaths::DataLocation).first(); 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:// } // Convert to percentEncoding before, to avoid having "/" and such in the filename QByteArray base64url = url.toUtf8().toPercentEncoding().toBase64(); base64url.truncate(255); // Limit filename length! Just in case the URL is VERY long // 255 chars is the limit in Ext3 FS localFilename = QStandardPaths::standardLocations(QStandardPaths::DataLocation).first(); localFilename.append("/images/"); localFilename.append(base64url); } return localFilename; } /* * Generate suggested filename for an attachment, * based on author ID, original extension, etc. * */ QString MiscHelpers::getSuggestedFilename(QString authorId, QString postType, QString postTitle, QString fileUrl, QString mimeType) { // Get original filename in server QString originalFilename = fileUrl.split("/").last(); QString suggestedFilename = authorId; suggestedFilename.append("_" + postType); if (!postTitle.trimmed().isEmpty()) { suggestedFilename.append("_" + postTitle); } suggestedFilename.replace("@", QStringLiteral("-")); // Avoid certain chars from author ID suggestedFilename.replace(".", QStringLiteral("-")); suggestedFilename.replace("/", QStringLiteral("-")); suggestedFilename.remove("?"); suggestedFilename.remove("!"); suggestedFilename.remove("*"); suggestedFilename.remove(":"); suggestedFilename.remove(";"); suggestedFilename.remove("\""); suggestedFilename.replace(" ", QStringLiteral("_")); // Avoid spaces in filename suggestedFilename.append("_" + originalFilename); // Something like A53r2w.png // Extension for 'file' attachments is given as .bin by server, for now if (postType == QStringLiteral("file")) // so determine a proper one based on mime info { QMimeDatabase mimeDb; QMimeType mime = mimeDb.mimeTypeForName(mimeType); QString suggestedExtension = mime.preferredSuffix(); if (!suggestedExtension.isEmpty()) { if (suggestedFilename.endsWith(QStringLiteral(".bin"))) { suggestedFilename.remove(-3, 3); } suggestedFilename.append(suggestedExtension); } } return suggestedFilename; } /* * Return MIME content type, like image/png, audio/ogg, etc. * */ QString MiscHelpers::getFileMimeType(QString fileUri) { qDebug() << "getFileMimeType() file:" << fileUri; QMimeDatabase mimeDb; QMimeType mimeType = mimeDb.mimeTypeForFile(fileUri); qDebug() << mimeType.name(); qDebug() << mimeType.aliases(); return mimeType.name(); // mime.aliases() is also interesting } /* * Return width of an image * */ int MiscHelpers::getImageWidth(QString fileURI) { QImageReader imageReader(fileURI); return imageReader.size().width(); } bool MiscHelpers::isImageAnimated(QString fileUri) { //qDebug() << "QMovie::supportedFormats()" << QMovie::supportedFormats(); bool isAnimated = false; QImageReader imageReader(fileUri); if (imageReader.supportsAnimation()) { QMovie movie; movie.setFileName(fileUri); qDebug() << "Image format SUPPORTS animation; Frames:" << movie.frameCount(); if (movie.frameCount() > 1) // FIXME: doesn't work with .MNG, so it returns 0 { qDebug() << "Image IS animated."; isAnimated = true; } } return isAnimated; } /* * Get a pair of icon-name + fallback-image for the given activity verb * */ QStringList MiscHelpers::iconsForActivity(QString verb) { QVariantMap iconMap; iconMap.insert("like", QStringList("emblem-favorite") << ":/images/button-like.png"); iconMap.insert("unlike", QStringList("emblem-favorite") << ":/images/button-like.png"); iconMap.insert("favorite", QStringList("emblem-favorite") << ":/images/button-like.png"); iconMap.insert("unfavorite", QStringList("emblem-favorite") << ":/images/button-like.png"); iconMap.insert("unshare", QStringList("dialog-cancel") << ":/images/button-cancel.png"); iconMap.insert("post", QStringList("mail-send") << ":/images/button-post.png"); iconMap.insert("update", QStringList("view-refresh") << ":/images/menu-refresh.png"); iconMap.insert("follow", QStringList("list-add-user") << ":/images/list-add.png"); iconMap.insert("stop-following", QStringList("list-remove-user") << ":/images/list-remove.png"); iconMap.insert("add", QStringList("list-add") << ":/images/list-add.png"); iconMap.insert("remove", QStringList("list-remove") << ":/images/list-remove.png"); iconMap.insert("create", QStringList("document-new") // FIXME; also -edit? << ":/images/button-edit.png"); iconMap.insert("delete", QStringList("edit-delete") << ":/images/button-delete.png"); iconMap.insert("join", QStringList("user-group-new") << ":/images/button-online.png"); iconMap.insert("leave", QStringList("user-group-delete") << ":/images/button-close.png"); QStringList missingIconList; missingIconList.append("dialog-information"); missingIconList.append(":/images/image-missing.png"); return iconMap.value(verb, missingIconList).toStringList(); } const QStringList MiscHelpers::imageExtensions() { return QStringList() << "png" << "jpg" << "jpeg" << "jpe" << "gif" << "mng" << "webp" << "svg"; } const QStringList MiscHelpers::audioExtensions() { return QStringList() << "ogg" << "oga" << "flac" << "opus" << "mka" << "mp3" << "mpga" << "wav" << "wma" << "asf" ; } const QStringList MiscHelpers::videoExtensions() { return QStringList() << "mkv" << "mpg" << "mpeg" << "mpe" << "mpv" << "ogm" << "ogg" << "mp4" << "avi" << "webm" << "mov" << "flv" << "3gp" << "wmv" << "asf"; } /* * Return a file filter string, usable in file dialogs, * such as: (*.png *.jpg *.gif);; * */ const QString MiscHelpers::fileFilterString(QStringList extensions) { QString filterString = " ("; foreach (QString extension, extensions) { filterString.append(" *." + extension + " "); } filterString.append(");;"); return filterString; } 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,2 MiB" * */ QString MiscHelpers::fileSizeString(QString fileURI) { QFileInfo fileInfo(fileURI); double fileSize = fileInfo.size(); QString sizeUnit = tr("bytes"); if (fileSize > 1024) // if > 1024 bytes, transform to KiB { fileSize /= 1024.0; sizeUnit = "KiB"; } if (fileSize > 1024) // if > 1024 KiB, transform to MiB { fileSize /= 1024.0; sizeUnit = "MiB"; } // Return with 0 padding and 2 decimal precision return QString("%1 %2").arg(QLocale::system().toString(fileSize, 'f', 2)) .arg(sizeUnit); } /* * Localized resolution string * */ QString MiscHelpers::resolutionString(int width, int height) { return QString("%1 x %2").arg(QLocale::system().toString(width)) .arg(QLocale::system().toString(height)); } /* * 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; newHtml.remove("\n"); // Remove in case misbehaving applications added any 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() << "#######\n\nregExp 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); // Protection: ensure the tag doesn't point to a huge video foreach (QString extension, videoExtensions()) { if (imgSrc.endsWith(extension)) { qDebug() << "*** Ignoring IMG tag that points to a video!! ***"; qDebug() << "*** " << imgSrc; imgSrc.clear(); // Avoid getting it into the download list break; } } // If not an empty string, originally or due to video extension, // add to the list, and replace HTML if (!imgSrc.isEmpty()) { // If the url had parameters, they _might_ have & in place of "&" imgSrc.replace("&", "&"); // Put them back // Check if http part (scheme) is missing from URL, and add it if (!imgSrc.startsWith("http")) { imgSrc.prepend("http:"); } // Add URL to list imageList.append(imgSrc); QString cachedImageFilename = getCachedImageFilename(imgSrc); int imageWidth = getImageWidth(cachedImageFilename); if (imageWidth <= 0) // If we still don't have a cached file, { imageWidth = 32; // set a typical icon size } // If width is bigger than the post, make it smaller to fit if (imageWidth > postWidth - 32) { // Some margins, to account for a scrollbar or a tab space before the image imageWidth = postWidth - 32; } newHtml.replace(stringPos, matchedLength, ""); } stringPos += matchedLength; // FIXME: error control } imageList.prepend(newHtml); // The modified HTML goes before the image list //qDebug() << "Returned HTML and images:\n" << imageList << "\n#################"; 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); QRegExp bodyRE(""); bodyRE.setMinimal(true); cleanHtml.remove(bodyRE); //////////////////////////////////////// Remove from links QRegExp linkStyleRE(".*"); linkStyleRE.setMinimal(true); QRegExp spanRE("(.*)"); // FIXME: remove ONLY color info spanRE.setMinimal(true); int pos = 0; while ((pos = linkStyleRE.indexIn(cleanHtml, pos)) != -1) { int removedTextOffset = 0; if (spanRE.indexIn(cleanHtml, pos) != -1) { // Replace the whole span tag by just what was inside cleanHtml.replace(spanRE.cap(0), spanRE.cap(1)); //qDebug() << "spanRE capture: " << spanRE.capturedTexts(); removedTextOffset = spanRE.cap(0).length() - spanRE.cap(1).length(); } pos += linkStyleRE.matchedLength() - removedTextOffset; } ////////////////////////////////////// Remove style from
      and
    1. QRegExp ulStyleRE("
        "); ulStyleRE.setMinimal(true); cleanHtml.replace(ulStyleRE, "
          "); QRegExp olStyleRE("
            "); olStyleRE.setMinimal(true); cleanHtml.replace(olStyleRE, "
              "); QRegExp liStyleRE("
            1. "); liStyleRE.setMinimal(true); cleanHtml.replace(liStyleRE, "
            2. "); // FIXME: Maybe try to remove background colors from

              elements // Remove these elements created by some QTextBlocks cleanHtml.remove(""); cleanHtml.remove(""); cleanHtml.remove(""); return cleanHtml.trimmed(); } /* * Remove only and from a HTML text * */ QString MiscHelpers::htmlWithoutLinks(QString originalHtml) { QString cleanHtml = originalHtml; QRegExp linksRE(""); linksRE.setMinimal(true); linksRE.setCaseSensitivity(Qt::CaseInsensitive); cleanHtml.remove(linksRE); cleanHtml.remove("", Qt::CaseInsensitive); return cleanHtml; } QString MiscHelpers::htmlToPlainText(QString html, int charLimit) { QTextDocument textDocument; textDocument.setHtml(html); QString plainText = textDocument.toPlainText().trimmed(); plainText.replace("\n", " "); plainText = plainText.trimmed(); if (charLimit != 0 && plainText.length() > charLimit) { plainText = plainText.left(charLimit).trimmed() + "..."; } return plainText; } /* * 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("<", "<"); // back to HTML entities content.replace(">", ">"); content.replace("\n", "
              "); // Important to replace this AFTER < and > QString quoteHtml = "» "+ author + ":" // >> + name "

              " "“" + content + "”" "

              "; return quoteHtml; } /* * Limit length of a string to charLimit chars, * removing characters from the middle * */ QString MiscHelpers::elidedText(QString text, int charLimit) { QString shortText = text; if (text.length() > charLimit) { charLimit -= 5; shortText = text.left(charLimit / 2); shortText.append(" ... "); shortText.append(text.right(charLimit / 2)); } return shortText; } /* * Generate the first part of the HTML of a post * that includes media attachments * */ QString MiscHelpers::mediaHtmlBase(QString postType, QString attachmentFilename, QString tooltipMessage, QString belowMessage, int imageWidth) { QString html = "" // First row, with gradient "" ""); // Second row, to add a message related to the image or attachment html.append("" "
              " ""); } else { html.append("=\"attachment:/\" >"); } html.append("" "
              " + belowMessage + "
              " "

              "); // Double line-break, because the content might not start with

              return html; } /* * Open URL in system's browser, showing a proper error message if that fails * */ bool MiscHelpers::openUrl(QUrl url, QWidget *parentWidget) { bool browserLaunched = QDesktopServices::openUrl(url); if (!browserLaunched) { QMessageBox::warning(parentWidget, tr("Error: Unable to launch browser"), tr("The default system web browser could not " "be executed.") + "

              " + tr("You might need to install the " "XDG utilities.")); } return browserLaunched; } dianara-v1.4.1/src/mainwindow.cpp0000644000175000017500000037463213216567534015106 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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::fromTheme("dianara", QIcon(":/icon/64x64/dianara.png"))); this->setMinimumSize(400, 400); // Ensure closing a dialog while the main window is hidden won't end the program qApp->setQuitOnLastWindowClosed(false); QSettings settings; firstRun = true; prepareDataDirectory(); // This sets this->dataDirectory reallyQuitProgram = false; trayIconAvailable = false; trayCurrentNewCount = 0; trayCurrentHLCount = 0; qDebug() << "System is:" << QSysInfo::prettyProductName() << QSysInfo::productType(); qDebug() << "Qt widget style in use:" << qApp->style()->objectName(); 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"); // TMP; FIXME } #if 0 // 1 to test the fallback icons QIcon::setThemeSearchPaths(QStringList() << "./"); QIcon::setThemeName("dianara-nonexistent-theme"); #endif #if 0 // 1 to test with a specific iconset QIcon::setThemeName("breeze"); #endif // Network control pumpController = new PumpController(this); // Global object to connect different classes directly globalObject = new GlobalObject(this); globalObject->setDataDirectory(this->dataDirectory); // Filter checker filterChecker = new FilterChecker(this); ////// GUI // User's profile editor, in its own window profileEditor = new ProfileEditor(pumpController, this); //////////////////////////////// Movable side panel, initially on left side avatarIconButton = new QPushButton(this); avatarIconButton->setIcon(QIcon(QPixmap(":/images/no-avatar.png") .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation))); avatarIconButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); avatarIconButton->setIconSize(QSize(64, 64)); avatarIconButton->setStyleSheet("QPushButton { border: 4px; " " padding: 4px } " "QPushButton:hover { border: 4px ridge " " palette(highlight);" " border-radius: 8px };"); connect(avatarIconButton, SIGNAL(clicked()), profileEditor, SLOT(show())); fullNameLabel = new QLabel("[ ----- ----- ]", this); fullNameLabel->setWordWrap(true); fullNameLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum); QFont userDetailsFont; userDetailsFont.setBold(true); userDetailsFont.setItalic(true); userDetailsFont.setPointSize(userDetailsFont.pointSize() - 2); userIdLabel = new QLabel(this); userIdLabel->setWordWrap(true); userIdLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum); userIdLabel->setFont(userDetailsFont); userDetailsFont.setBold(false); userDetailsFont.setItalic(false); userHometownLabel = new QLabel(this); userHometownLabel->setWordWrap(true); userHometownLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum); userHometownLabel->setFont(userDetailsFont); userInfoLayout = new QVBoxLayout(); userInfoLayout->addSpacing(2); userInfoLayout->addWidget(fullNameLabel); userInfoLayout->addWidget(userIdLabel); userInfoLayout->addWidget(userHometownLabel); leftTopLayout = new QHBoxLayout(); leftTopLayout->setContentsMargins(0, 0, 0, 0); leftTopLayout->addWidget(avatarIconButton, 0, Qt::AlignLeft); leftTopLayout->addSpacing(2); leftTopLayout->addLayout(userInfoLayout, 1); leftPanel = new QToolBox(this); // Will hold the minor feeds leftPanel->setContentsMargins(0, 0, 0, 0); //////// Meanwhile feed: inbox/minor meanwhileFeed = new MinorFeed(PumpController::MinorFeedMainRequest, pumpController, globalObject, filterChecker, this); connect(meanwhileFeed, SIGNAL(newItemsCountChanged(int,int)), this, SLOT(setMinorFeedTitle(int,int))); connect(meanwhileFeed, SIGNAL(newItemsReceived(PumpController::requestTypes,int,int,int,int)), this, SLOT(notifyMinorFeedUpdate(PumpController::requestTypes,int,int,int,int))); leftPanel->addItem(meanwhileFeed, QIcon::fromTheme("clock", QIcon(":/images/feed-clock.png")), QString()); this->setMinorFeedTitle(0, 0); // Set initial title (0 new, 0 HL) leftPanel->setItemToolTip(0, "" + tr("Minor activities done by everyone, such " "as replying to posts")); //////// Mentions feed: inbox/direct/minor mentionsFeed = new MinorFeed(PumpController::MinorFeedDirectRequest, pumpController, globalObject, filterChecker, this); connect(mentionsFeed, SIGNAL(newItemsCountChanged(int,int)), this, SLOT(setMentionsFeedTitle(int,int))); connect(mentionsFeed, SIGNAL(newItemsReceived(PumpController::requestTypes,int,int,int,int)), this, SLOT(notifyMinorFeedUpdate(PumpController::requestTypes,int,int,int,int))); leftPanel->addItem(mentionsFeed, QIcon::fromTheme("mail-folder-inbox", QIcon(":/images/feed-inbox.png")), QString()); this->setMentionsFeedTitle(0, 0); // Set initial title, with 0 new leftPanel->setItemToolTip(1, "" + tr("Minor activities addressed to you")); //////// Actions feed: feed/minor actionsFeed = new MinorFeed(PumpController::MinorFeedActivityRequest, pumpController, globalObject, filterChecker, this); connect(actionsFeed, SIGNAL(newItemsReceived(PumpController::requestTypes,int,int,int,int)), this, SLOT(notifyMinorFeedUpdate(PumpController::requestTypes,int,int,int,int))); leftPanel->addItem(actionsFeed, QIcon::fromTheme("mail-folder-outbox", QIcon(":/images/feed-outbox.png")), PumpController::getFeedNameAndPath(PumpController::MinorFeedActivityRequest) .first()); leftPanel->setItemToolTip(2, "" + tr("Minor activities done by you")); leftLayout = new QVBoxLayout(); leftLayout->setSpacing(0); // Minimal spacing leftLayout->setContentsMargins(0, 0, 0, 0); leftLayout->addLayout(leftTopLayout); // Avatar + user info leftLayout->addSpacing(2); // Some styles look bad without a minimum space here leftLayout->addWidget(leftPanel); // Meanwhile and other minor feeds // Shortcuts to change between the different minor feeds this->showMeanwhileFeed = new QAction(this); showMeanwhileFeed->setShortcut(QKeySequence("Ctrl+1")); connect(showMeanwhileFeed, SIGNAL(triggered()), this, SLOT(toggleMeanwhileFeed())); this->addAction(showMeanwhileFeed); this->showMentionsFeed = new QAction(this); showMentionsFeed->setShortcut(QKeySequence("Ctrl+2")); connect(showMentionsFeed, SIGNAL(triggered()), this, SLOT(toggleMentionsFeed())); this->addAction(showMentionsFeed); this->showActionsFeed = new QAction(this); showActionsFeed->setShortcut(QKeySequence("Ctrl+3")); connect(showActionsFeed, SIGNAL(triggered()), this, SLOT(toggleActionsFeed())); this->addAction(showActionsFeed); //////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////// TMP GROUP MANAGER STUFF //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// #ifdef GROUPSUPPORT GroupsManager *TMPGROUPSMANAGER = new GroupsManager(this->pumpController, this); QPushButton *TMPEDITGROUPSBUTTON = new QPushButton(QIcon::fromTheme("user-group-properties"), "*MANAGE GROUPS*", this); connect(TMPEDITGROUPSBUTTON, &QAbstractButton::clicked, TMPGROUPSMANAGER, &QWidget::show); this->leftLayout->addWidget(TMPEDITGROUPSBUTTON); #endif //////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////// TMP GROUP MANAGER STUFF //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// this->leftSideWidget = new QWidget(this); leftSideWidget->setLayout(leftLayout); this->sideDockWidget = new QDockWidget(this); sideDockWidget->setObjectName("sidePanelDock"); // Needed for saveState() sideDockWidget->setWidget(leftSideWidget); // Until window state is restored (including docks), ensure a decent default width sideDockWidget->setMinimumWidth(leftSideWidget->sizeHint().width() * 1.5); this->addDockWidget(Qt::LeftDockWidgetArea, sideDockWidget); // Empty widget to remove titlebar, when locking the interface this->sideDockTitleWidget = new QWidget(this); //////////////////////////////////////////////////////////// Right side publisher = new Publisher(pumpController, globalObject, this); // Passive notifications widget bannerNotification = new BannerNotification(this); bannerNotification->hide(); connect(bannerNotification, SIGNAL(updateRequested()), this, SLOT(onUpdateRequestViaBanner())); connect(bannerNotification, SIGNAL(bannerCancelled()), this, SLOT(onUpdateDelayedViaBanner())); /// START SETTING UP TIMELINES // Main timeline // mainTimeline = new TimeLine(PumpController::MainTimelineRequest, pumpController, globalObject, filterChecker, this); mainTimelineScrollArea = new QScrollArea(this); mainTimelineScrollArea->setContentsMargins(1, 1, 1, 1); mainTimelineScrollArea->setFrameStyle(QFrame::NoFrame); mainTimelineScrollArea->setWidget(mainTimeline); // Make timeline scrollable mainTimelineScrollArea->setWidgetResizable(true); mainTimelineScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(mainTimeline, SIGNAL(scrollTo(QAbstractSlider::SliderAction)), this, SLOT(scrollMainTimelineTo(QAbstractSlider::SliderAction))); connect(mainTimeline, SIGNAL(unreadPostsCountChanged(PumpController::requestTypes,int,int,int)), this, SLOT(setTimelineTabTitle(PumpController::requestTypes,int,int,int))); connect(mainTimeline, SIGNAL(timelineRendered(PumpController::requestTypes,int,int,int,int,int,int,int,QString)), this, SLOT(notifyTimelineUpdate(PumpController::requestTypes,int,int,int,int,int,int,int,QString))); // To ensure comment composer is visible, by scrolling to it connect(mainTimeline, &TimeLine::commentingOnPost, this, &MainWindow::scrollMainTimelineToWidget); // Direct timeline // directTimeline = new TimeLine(PumpController::DirectTimelineRequest, pumpController, globalObject, filterChecker, this); directTimelineScrollArea = new QScrollArea(this); directTimelineScrollArea->setContentsMargins(1, 1, 1, 1); directTimelineScrollArea->setFrameStyle(QFrame::NoFrame); directTimelineScrollArea->setWidget(directTimeline); directTimelineScrollArea->setWidgetResizable(true); directTimelineScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(directTimeline, SIGNAL(scrollTo(QAbstractSlider::SliderAction)), this, SLOT(scrollDirectTimelineTo(QAbstractSlider::SliderAction))); connect(directTimeline, SIGNAL(unreadPostsCountChanged(PumpController::requestTypes,int,int,int)), this, SLOT(setTimelineTabTitle(PumpController::requestTypes,int,int,int))); connect(directTimeline, SIGNAL(timelineRendered(PumpController::requestTypes,int,int,int,int,int,int,int,QString)), this, SLOT(notifyTimelineUpdate(PumpController::requestTypes,int,int,int,int,int,int,int,QString))); // To ensure comment composer is visible connect(directTimeline, SIGNAL(commentingOnPost(QWidget*)), this, SLOT(scrollDirectTimelineToWidget(QWidget*))); // Activity timeline // activityTimeline = new TimeLine(PumpController::ActivityTimelineRequest, pumpController, globalObject, filterChecker, this); activityTimelineScrollArea = new QScrollArea(this); activityTimelineScrollArea->setContentsMargins(1, 1, 1, 1); activityTimelineScrollArea->setFrameStyle(QFrame::NoFrame); activityTimelineScrollArea->setWidget(activityTimeline); // Make it scrollable activityTimelineScrollArea->setWidgetResizable(true); activityTimelineScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(activityTimeline, SIGNAL(scrollTo(QAbstractSlider::SliderAction)), this, SLOT(scrollActivityTimelineTo(QAbstractSlider::SliderAction))); connect(activityTimeline, SIGNAL(unreadPostsCountChanged(PumpController::requestTypes,int,int,int)), this, SLOT(setTimelineTabTitle(PumpController::requestTypes,int,int,int))); connect(activityTimeline, SIGNAL(timelineRendered(PumpController::requestTypes,int,int,int,int,int,int,int,QString)), this, SLOT(notifyTimelineUpdate(PumpController::requestTypes,int,int,int,int,int,int,int,QString))); // To ensure comment composer is visible connect(activityTimeline, SIGNAL(commentingOnPost(QWidget*)), this, SLOT(scrollActivityTimelineToWidget(QWidget*))); // Favorites timeline // favoritesTimeline = new TimeLine(PumpController::FavoritesTimelineRequest, pumpController, globalObject, filterChecker, this); favoritesTimelineScrollArea = new QScrollArea(this); favoritesTimelineScrollArea->setContentsMargins(1, 1, 1, 1); favoritesTimelineScrollArea->setFrameStyle(QFrame::NoFrame); favoritesTimelineScrollArea->setWidget(favoritesTimeline); favoritesTimelineScrollArea->setWidgetResizable(true); favoritesTimelineScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(favoritesTimeline, SIGNAL(scrollTo(QAbstractSlider::SliderAction)), this, SLOT(scrollFavoritesTimelineTo(QAbstractSlider::SliderAction))); connect(favoritesTimeline, SIGNAL(unreadPostsCountChanged(PumpController::requestTypes,int,int,int)), this, SLOT(setTimelineTabTitle(PumpController::requestTypes,int,int,int))); connect(favoritesTimeline, SIGNAL(timelineRendered(PumpController::requestTypes,int,int,int,int,int,int,int,QString)), this, SLOT(notifyTimelineUpdate(PumpController::requestTypes,int,int,int,int,int,int,int,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 contactManager = new ContactManager(pumpController, globalObject, this); tabWidget = new QTabWidget(this); /* TODO: Setting this size policy, the publisher could grow much bigger, * and tabWidget could have a bigger stretch factor * tabWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored); * ***/ tabWidget->addTab(mainTimelineScrollArea, QIcon::fromTheme("view-list-details", QIcon(":/images/feed-inbox.png")), QString()); this->setTimelineTabTitle(PumpController::MainTimelineRequest, 0, 0, 0); tabWidget->addTab(directTimelineScrollArea, QIcon::fromTheme("mail-message", QIcon(":/images/feed-inbox.png")), QString()); this->setTimelineTabTitle(PumpController::DirectTimelineRequest, 0, 0, 0); tabWidget->addTab(activityTimelineScrollArea, QIcon::fromTheme("user-home", QIcon(":/images/feed-outbox.png")), QString()); this->setTimelineTabTitle(PumpController::ActivityTimelineRequest, 0, 0, 0); tabWidget->addTab(favoritesTimelineScrollArea, QIcon::fromTheme("folder-favorites", QIcon(":/images/button-like.png")), QString()); this->setTimelineTabTitle(PumpController::FavoritesTimelineRequest, 0, 0, 0); tabWidget->addTab(contactManager, QIcon::fromTheme("system-users", QIcon(":/images/button-users.png")), tr("&Contacts")); tabWidget->setTabToolTip(4, "" // HTMLized for wordwrap + tr("The people you follow, the ones who " "follow you, and your person lists")); connect(tabWidget, &QTabWidget::currentChanged, this, &MainWindow::setTitleAndTrayInfo); rightSideWidget = new QWidget(this); rightLayout = new QVBoxLayout(); rightLayout->setSpacing(0); rightLayout->setContentsMargins(0, 1, 1, 1); rightLayout->addWidget(publisher, 8); // big stretch for full mode rightLayout->addWidget(tabWidget, 1); rightLayout->addWidget(bannerNotification); this->rightSideWidget->setLayout(rightLayout); this->setCentralWidget(rightSideWidget); // FreeDesktop.org notifications handler fdNotifier = new FDNotifications(this); connect(fdNotifier, SIGNAL(showFallbackNotification(QString,QString,int)), this, SLOT(showTrayFallbackMessage(QString,QString,int))); // D-Bus Interface, for remote control #ifdef QT_DBUS_LIB this->dbusInterface = new DBusInterface(this); QDBusConnection bus = QDBusConnection::sessionBus(); bus.registerObject("/Dianara", dbusInterface, QDBusConnection::ExportAllSlots); bus.registerService("org.nongnu." + qApp->applicationName().toLower()); bool connectToNotifyActions = bus.connect("", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "ActionInvoked", this, SLOT(onNotificationAction(uint,QString))); qDebug() << "Connection to org.freedesktop.Notifications::ActionInvoked:" << connectToNotifyActions; #endif // Timeline updates timer updateTimer = new QTimer(this); // Interval is set from loadSettings() connect(updateTimer, SIGNAL(timeout()), this, SLOT(onTimelineAutoupdate())); updateTimer->start(); // Timestamps refresh timer timestampsTimer = new QTimer(this); timestampsTimer->setInterval(60000); // 60 sec connect(timestampsTimer, SIGNAL(timeout()), this, SLOT(refreshAllTimestamps())); timestampsTimer->start(); // Delayed timeline resize timer delayedResizeTimer = new QTimer(this); delayedResizeTimer->setSingleShot(true); connect(delayedResizeTimer, SIGNAL(timeout()), this, SLOT(adjustTimelineSizes())); favoritesReloadTimer = new QTimer(this); favoritesReloadTimer->setSingleShot(true); favoritesReloadTimer->setInterval(300000); // 5 minutes connect(favoritesReloadTimer, SIGNAL(timeout()), this, SLOT(updateFavoritesTimeline())); /* * User-did-something timer * * Every time the user does something minor, such as commenting or * following a contact, the timer will be restarted. * * Some time after the user has stopped doing things, the timer * will update the Actions feed. * */ userDidSomethingTimer = new QTimer(this); userDidSomethingTimer->setSingleShot(true); userDidSomethingTimer->setInterval(300000); // 5 minutes connect(userDidSomethingTimer, SIGNAL(timeout()), this, SLOT(updateActionsFeed())); connect(pumpController, SIGNAL(userDidSomething()), userDidSomethingTimer, SLOT(start())); // Restart timer every time delayedScrollTimer = new QTimer(this); delayedScrollTimer->setSingleShot(true); connect(delayedScrollTimer, SIGNAL(timeout()), this, SLOT(scrollToNewPosts())); ////////////////// Load configuration from disk loadSettings(); //// External widgets which live in their own windows // Account wizard accountDialog = new AccountDialog(pumpController, this); connect(accountDialog, SIGNAL(userIdChanged(QString)), this, SLOT(updateUserID(QString))); // Configuration dialog configDialog = new ConfigDialog(this->globalObject, this->dataDirectory, this->updateInterval, this->tabsPosition, this->tabsMovable, this->fdNotifier, this); connect(configDialog, SIGNAL(configurationChanged()), this, SLOT(updateConfigSettings())); // Filter editor filterEditor = new FilterEditor(filterChecker, this); connect(configDialog, SIGNAL(filterEditorRequested()), filterEditor, SLOT(show())); // Log viewer; Created without a parent, so it can be a truly independent window logViewer = new LogViewer(nullptr); // under Plasma 5, for instance // NOTE: using nullptr in older compilers requires -std=c++0x, handled via .pro file connect(pumpController, SIGNAL(logMessage(QString,QString)), logViewer, SLOT(addToLog(QString,QString))); connect(globalObject, SIGNAL(messageForLog(QString,QString)), logViewer, SLOT(addToLog(QString,QString))); // Help widget; "Getting started", etc. helpWidget = new HelpWidget(nullptr); // Without a parent, same logic as logViewer /// ///////////////// Connections between Meanwhile feed and the timelines /// /// /// Object updated connect(meanwhileFeed, SIGNAL(objectUpdated(ASObject*)), mainTimeline, SLOT(updatePostsFromMinorFeed(ASObject*))); connect(meanwhileFeed, SIGNAL(objectUpdated(ASObject*)), directTimeline, SLOT(updatePostsFromMinorFeed(ASObject*))); connect(meanwhileFeed, SIGNAL(objectUpdated(ASObject*)), activityTimeline, SLOT(updatePostsFromMinorFeed(ASObject*))); // Object liked connect(meanwhileFeed, SIGNAL(objectLiked(QString,QString,QString,QString,QString)), mainTimeline, SLOT(addLikesFromMinorFeed(QString,QString,QString,QString,QString))); connect(meanwhileFeed, SIGNAL(objectLiked(QString,QString,QString,QString,QString)), directTimeline, SLOT(addLikesFromMinorFeed(QString,QString,QString,QString,QString))); connect(meanwhileFeed, SIGNAL(objectLiked(QString,QString,QString,QString,QString)), activityTimeline, SLOT(addLikesFromMinorFeed(QString,QString,QString,QString,QString))); // Object unliked connect(meanwhileFeed, SIGNAL(objectUnliked(QString,QString,QString)), mainTimeline, SLOT(removeLikesFromMinorFeed(QString,QString,QString))); connect(meanwhileFeed, SIGNAL(objectUnliked(QString,QString,QString)), directTimeline, SLOT(removeLikesFromMinorFeed(QString,QString,QString))); connect(meanwhileFeed, SIGNAL(objectUnliked(QString,QString,QString)), activityTimeline, SLOT(removeLikesFromMinorFeed(QString,QString,QString))); // Object got a new reply connect(meanwhileFeed, SIGNAL(objectReplyAdded(ASObject*)), mainTimeline, SLOT(addReplyFromMinorFeed(ASObject*))); connect(meanwhileFeed, SIGNAL(objectReplyAdded(ASObject*)), directTimeline, SLOT(addReplyFromMinorFeed(ASObject*))); connect(meanwhileFeed, SIGNAL(objectReplyAdded(ASObject*)), activityTimeline, SLOT(addReplyFromMinorFeed(ASObject*))); /// Object deleted connect(meanwhileFeed, SIGNAL(objectDeleted(ASObject*)), mainTimeline, SLOT(setPostsDeletedFromMinorFeed(ASObject*))); connect(meanwhileFeed, SIGNAL(objectDeleted(ASObject*)), directTimeline, SLOT(setPostsDeletedFromMinorFeed(ASObject*))); connect(meanwhileFeed, SIGNAL(objectDeleted(ASObject*)), activityTimeline, SLOT(setPostsDeletedFromMinorFeed(ASObject*))); // TODO: sync other timelines/feeds, too -- FIXME /// //////////////////////////////// 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,QString)), this, SLOT(storeAvatar(QByteArray,QString))); connect(pumpController, SIGNAL(imageReceived(QByteArray,QString)), this, SLOT(storeImage(QByteArray,QString))); connect(pumpController, SIGNAL(authorizationStatusChanged(bool)), this, SLOT(toggleWidgetsByAuthorization(bool))); connect(pumpController, SIGNAL(authorizationFailed(QString,QString)), this, SLOT(showAuthError(QString,QString))); connect(pumpController, SIGNAL(initializationCompleted()), this, SLOT(onInitializationComplete())); // After receiving timeline contents, update corresponding timeline connect(pumpController, SIGNAL(mainTimelineReceived(QVariantList,QString,QString,int)), mainTimeline, SLOT(setTimeLineContents(QVariantList,QString,QString,int))); connect(pumpController, SIGNAL(directTimelineReceived(QVariantList,QString,QString,int)), directTimeline, SLOT(setTimeLineContents(QVariantList,QString,QString,int))); connect(pumpController, SIGNAL(activityTimelineReceived(QVariantList,QString,QString,int)), activityTimeline, SLOT(setTimeLineContents(QVariantList,QString,QString,int))); connect(pumpController, SIGNAL(favoritesTimelineReceived(QVariantList,QString,QString,int)), favoritesTimeline, SLOT(setTimeLineContents(QVariantList,QString,QString,int))); // After receiving a main minor feed (Meanwhile, Mentions, Actions), update them connect(pumpController, SIGNAL(minorFeedMainReceived(QVariantList,QString,QString,int)), meanwhileFeed, SLOT(setFeedContents(QVariantList,QString,QString,int))); connect(pumpController, SIGNAL(minorFeedDirectReceived(QVariantList,QString,QString,int)), mentionsFeed, SLOT(setFeedContents(QVariantList,QString,QString,int))); connect(pumpController, SIGNAL(minorFeedActivityReceived(QVariantList,QString,QString,int)), actionsFeed, SLOT(setFeedContents(QVariantList,QString,QString,int))); // 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, schedule to reload favorites timeline connect(pumpController, SIGNAL(likeSet()), favoritesReloadTimer, SLOT(start())); // 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*** // Show notifications for events sent from pumpController connect(pumpController, SIGNAL(showErrorNotification(QString)), this, SLOT(showErrorPopup(QString))); // Update statusBar message from pumpController's infos connect(pumpController, SIGNAL(currentJobChanged(QString)), this, SLOT(setStatusBarMessage(QString))); connect(pumpController, SIGNAL(transientStatusBarMessage(QString)), this, SLOT(setTransientStatusMessage(QString))); // Update statusBar also from globalObject's requests connect(globalObject, SIGNAL(messageForStatusBar(QString)), this, SLOT(setStatusBarMessage(QString))); // Show a user's timeline when requested through GlobalObject connect(globalObject, SIGNAL(userTimelineRequested(QString,QString,QIcon,QString)), this, SLOT(showUserTimeline(QString,QString,QIcon,QString))); // Add menus and toolbar createMenus(); createToolbar(); loadMainWindowConfig(); // Info label in the right side of the menu bar this->menuInfoLabel = new QLabel(tr("Press F1 for help"), // Just initial text this); menuInfoLabel->setFont(userDetailsFont); menuInfoLabel->setAlignment(Qt::AlignRight); this->menuInfoLayout = new QHBoxLayout(); menuInfoLayout->addWidget(menuInfoLabel); this->menuInfoWidget = new QWidget(this); menuInfoWidget->setLayout(menuInfoLayout); this->menuBar()->setCornerWidget(menuInfoWidget); // If menu is being displayed outside the application, hide this, too this->menuInfoWidget->setVisible(!this->menuBar()->isNativeMenuBar()); // StatusBar stuff this->createStatusbarWidgets(); this->statusBar()->showMessage(tr("Initializing...")); // Add the system tray icon createTrayIcon(); settings.beginGroup("MainWindow"); // Now set the "view side panel" checkable menu to its saved state // This will also trigger the action to hide/show it viewSidePanel->setChecked(settings.value("viewSidePanel", true).toBool()); // Check view > toolbar if needed; HIDDEN by default viewToolbar->setChecked(settings.value("viewToolbar", false).toBool()); // Set the "view status bar" checkable menu to its saved state viewStatusBar->setChecked(settings.value("viewStatusBar", true).toBool()); // Lock or unlock the panels, locked by default viewLockPanels->setChecked(settings.value("viewLockPanels", true).toBool()); settings.endGroup(); initializationComplete = false; logViewer->addToLog(tr("Dianara started.")); logViewer->addToLog(tr("Running with Qt v%1.").arg(qVersion() + QStringLiteral(" (") + qApp->platformName() + QStringLiteral(")")) + " " + QSysInfo::prettyProductName() + "."); this->statusAccountButtonUsed = false; // If User ID is defined, set PumpController in motion if (!userID.isEmpty() && pumpController->currentlyAuthorized()) { pumpController->setUserCredentials(this->userID); fdNotifier->setCurrentUserId(this->userID); // getUserProfile() will be called from setUserCredentials() this->initializationProgressBar->show(); } else // Otherwise, just say so in the statusbar and offer a button there { const QString message = tr("Your account is not configured yet."); this->setStatusBarMessage(message); logViewer->addToLog(message); this->statusAccountButton = new QPushButton(QIcon::fromTheme("dialog-password", QIcon(":/images/button-password.png")), tr("Click here to configure " "your account"), this); connect(statusAccountButton, SIGNAL(clicked()), settingsAccount, SLOT(trigger())); this->statusBar()->insertPermanentWidget(0, statusAccountButton); this->statusAccountButtonUsed = true; } // Post-init timer postInitTimer = new QTimer(this); postInitTimer->setSingleShot(true); connect(postInitTimer, SIGNAL(timeout()), this, SLOT(postInit())); postInitTimer->start(1000); // Handle session manager asking to quit connect(qApp, SIGNAL(commitDataRequest(QSessionManager&)), this, SLOT(onSessionManagerQuitRequest(QSessionManager&))); qDebug() << "MainWindow created"; } MainWindow::~MainWindow() { qDebug() << "MainWindow destroyed"; } /* * Prepare the data directory. Create if necessary * */ void MainWindow::prepareDataDirectory() { dataDirectory = QStandardPaths::standardLocations(QStandardPaths::DataLocation).first(); qDebug() << "Data directory:" << this->dataDirectory; QDir dataDir; // Base directory 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 + "/audios")) { qDebug() << "Creating audios directory"; if (dataDir.mkpath(dataDirectory + "/audios")) { qDebug() << "Audios directory created"; } else { qDebug() << "Error creating audios directory!"; } } if (!dataDir.exists(dataDirectory + "/videos")) { qDebug() << "Creating videos directory"; if (dataDir.mkpath(dataDirectory + "/videos")) { qDebug() << "Videos directory created"; } else { qDebug() << "Error creating videos directory!"; } } if (!dataDir.exists(dataDirectory + "/drafts")) { qDebug() << "Creating drafts directory"; if (dataDir.mkpath(dataDirectory + "/drafts")) { qDebug() << "Drafts directory created"; } else { qDebug() << "Error creating drafts directory!"; } } // Avatars 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() { // Session sessionMenu = new QMenu(tr("&Session"), this); QString feedName; QString optionString = tr("Update %1"); // Main timeline feedName = PumpController::getFeedNameAndPath(PumpController::MainTimelineRequest).first(); sessionUpdateMainTimeline = new QAction(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), optionString.arg(feedName), this); sessionUpdateMainTimeline->setShortcut(QKeySequence(Qt::Key_F5)); sessionUpdateMainTimeline->setDisabled(true); // Disabled until authorization checked connect(sessionUpdateMainTimeline, SIGNAL(triggered()), mainTimeline, SLOT(goToFirstPage())); sessionMenu->addAction(sessionUpdateMainTimeline); // Direct messages feedName = PumpController::getFeedNameAndPath(PumpController::DirectTimelineRequest).first(); sessionUpdateDirectTimeline = new QAction(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), optionString.arg(feedName), this); sessionUpdateDirectTimeline->setShortcut(QKeySequence(Qt::Key_F6)); sessionUpdateDirectTimeline->setDisabled(true); connect(sessionUpdateDirectTimeline, SIGNAL(triggered()), directTimeline, SLOT(goToFirstPage())); sessionMenu->addAction(sessionUpdateDirectTimeline); // Activity feedName = PumpController::getFeedNameAndPath(PumpController::ActivityTimelineRequest).first(); sessionUpdateActivityTimeline = new QAction(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), optionString.arg(feedName), this); sessionUpdateActivityTimeline->setShortcut(QKeySequence(Qt::Key_F7)); sessionUpdateActivityTimeline->setDisabled(true); connect(sessionUpdateActivityTimeline, SIGNAL(triggered()), activityTimeline, SLOT(goToFirstPage())); sessionMenu->addAction(sessionUpdateActivityTimeline); // Favorites feedName = PumpController::getFeedNameAndPath(PumpController::FavoritesTimelineRequest).first(); sessionUpdateFavoritesTimeline = new QAction(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), optionString.arg(feedName), this); sessionUpdateFavoritesTimeline->setShortcut(QKeySequence(Qt::Key_F8)); sessionUpdateFavoritesTimeline->setDisabled(true); connect(sessionUpdateFavoritesTimeline, SIGNAL(triggered()), favoritesTimeline, SLOT(goToFirstPage())); sessionMenu->addAction(sessionUpdateFavoritesTimeline); sessionMenu->addSeparator(); // ---------- minor feeds // Meanwhile feedName = PumpController::getFeedNameAndPath(PumpController::MinorFeedMainRequest).first(); sessionUpdateMinorFeedMain = new QAction(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), optionString.arg(feedName), this); sessionUpdateMinorFeedMain->setShortcut(QKeySequence(Qt::Key_F2)); sessionUpdateMinorFeedMain->setDisabled(true); connect(sessionUpdateMinorFeedMain, SIGNAL(triggered()), meanwhileFeed, SLOT(updateFeed())); sessionMenu->addAction(sessionUpdateMinorFeedMain); // Mentions feedName = PumpController::getFeedNameAndPath(PumpController::MinorFeedDirectRequest).first(); sessionUpdateMinorFeedDirect = new QAction(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), optionString.arg(feedName), this); sessionUpdateMinorFeedDirect->setShortcut(QKeySequence(Qt::Key_F3)); sessionUpdateMinorFeedDirect->setDisabled(true); connect(sessionUpdateMinorFeedDirect, SIGNAL(triggered()), mentionsFeed, SLOT(updateFeed())); sessionMenu->addAction(sessionUpdateMinorFeedDirect); // Actions feedName = PumpController::getFeedNameAndPath(PumpController::MinorFeedActivityRequest).first(); sessionUpdateMinorFeedActivity = new QAction(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), optionString.arg(feedName), this); sessionUpdateMinorFeedActivity->setShortcut(QKeySequence(Qt::Key_F4)); sessionUpdateMinorFeedActivity->setDisabled(true); connect(sessionUpdateMinorFeedActivity, SIGNAL(triggered()), this, SLOT(updateActionsFeed())); // Go through this slot to stop the timer sessionMenu->addAction(sessionUpdateMinorFeedActivity); ////////////////////////////////////////////////////////// sessionMenu->addSeparator(); // ------ sessionAutoUpdates = new QAction(QIcon::fromTheme("clock", QIcon(":/images/feed-clock.png")), tr("Auto-update &Timelines"), this); sessionAutoUpdates->setCheckable(true); sessionAutoUpdates->setChecked(true); sessionAutoUpdates->setDisabled(true); // until initialization is complete connect(sessionAutoUpdates, SIGNAL(toggled(bool)), this, SLOT(toggleAutoUpdates(bool))); sessionMenu->addAction(sessionAutoUpdates); 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", QIcon(":/images/button-edit.png")), tr("&Post a Note"), this); sessionPostNote->setShortcut(QKeySequence("Ctrl+N")); connect(sessionPostNote, SIGNAL(triggered()), this, SLOT(startPost())); // Will show window if needed sessionMenu->addAction(sessionPostNote); sessionMenu->addSeparator(); sessionQuit = new QAction(QIcon::fromTheme("application-exit", QIcon(":/images/button-delete.png")), tr("&Quit"), this); sessionQuit->setShortcut(QKeySequence("Ctrl+Q")); connect(sessionQuit, SIGNAL(triggered()), this, SLOT(quitProgram())); sessionMenu->addAction(sessionQuit); this->menuBar()->addMenu(sessionMenu); // View viewMenu = new QMenu(tr("&View"), this); viewSidePanel = new QAction(QIcon::fromTheme("view-sidetree"), tr("Side Panel"), this); connect(viewSidePanel, SIGNAL(toggled(bool)), this, SLOT(toggleSidePanel(bool))); // Use View > Side Panel menu text as title for the dock when unlocked this->sideDockWidget->setWindowTitle(viewSidePanel->text()); viewSidePanel->setCheckable(true); viewSidePanel->setChecked(true); viewSidePanel->setShortcut(Qt::Key_F9); viewMenu->addAction(viewSidePanel); viewToolbar = new QAction(QIcon::fromTheme("configure-toolbars"), tr("&Toolbar"), this); connect(viewToolbar, SIGNAL(toggled(bool)), this, SLOT(toggleToolbar(bool))); viewToolbar->setCheckable(true); viewMenu->addAction(viewToolbar); 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); viewMenu->addSeparator(); 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); viewLogAction = new QAction(QIcon::fromTheme("text-x-log", QIcon(":/images/log.png")), tr("&Log"), this); connect(viewLogAction, SIGNAL(triggered()), logViewer, SLOT(toggleVisibility())); viewLogAction->setShortcut(Qt::Key_F12); viewMenu->addAction(viewLogAction); viewMenu->addSeparator(); // ------------------- viewLockPanels = new QAction(QIcon::fromTheme("object-locked"), tr("Locked Panels and Toolbars"), this); connect(viewLockPanels, SIGNAL(toggled(bool)), this, SLOT(toggleLockedPanels(bool))); viewLockPanels->setCheckable(true); // Can't set it checked at it at this point, since that also locks the toolbar viewMenu->addAction(viewLockPanels); this->menuBar()->addMenu(viewMenu); // Settings settingsMenu = new QMenu(tr("S&ettings"), this); settingsEditProfile = new QAction(QIcon::fromTheme("user-properties", QIcon(":/images/no-avatar.png")), tr("Edit &Profile"), this); settingsEditProfile->setShortcut(QKeySequence("Ctrl+Shift+P")); connect(settingsEditProfile, SIGNAL(triggered()), profileEditor, SLOT(show())); settingsMenu->addAction(settingsEditProfile); settingsAccount = new QAction(QIcon::fromTheme("dialog-password", QIcon(":/images/button-password.png")), tr("&Account"), this); settingsAccount->setShortcut(QKeySequence("Ctrl+Shift+A")); connect(settingsAccount, SIGNAL(triggered()), accountDialog, SLOT(show())); settingsMenu->addAction(settingsAccount); settingsMenu->addSeparator(); settingsFilters = new QAction(QIcon::fromTheme("view-filter", QIcon(":/images/button-filter.png")), tr("&Filters and Highlighting"), this); settingsFilters->setShortcut(QKeySequence("Ctrl+Shift+F")); connect(settingsFilters, SIGNAL(triggered()), filterEditor, SLOT(show())); settingsMenu->addAction(settingsFilters); settingsConfigure = new QAction(QIcon::fromTheme("configure", QIcon(":/images/button-configure.png")), tr("&Configure Dianara"), this); settingsConfigure->setShortcut(QKeySequence("Ctrl+Shift+S")); connect(settingsConfigure, SIGNAL(triggered()), configDialog, SLOT(show())); settingsMenu->addAction(settingsConfigure); this->menuBar()->addMenu(settingsMenu); this->menuBar()->addSeparator(); // Help helpMenu = new QMenu(tr("&Help"), this); helpBasicHelp = new QAction(QIcon::fromTheme("help-browser", QIcon(":/icon/64x64/dianara.png")), tr("Basic &Help"), this); helpBasicHelp->setShortcut(Qt::Key_F1); connect(helpBasicHelp, SIGNAL(triggered()), helpWidget, SLOT(show())); helpMenu->addAction(helpBasicHelp); helpShowWizard = new QAction(QIcon::fromTheme("tools-wizard", QIcon(":/images/button-online.png")), tr("Show Welcome Wizard"), this); connect(helpShowWizard, SIGNAL(triggered()), this, SLOT(showFirstRunWizard())); helpMenu->addAction(helpShowWizard); helpMenu->addSeparator(); // --- helpVisitWebsite = new QAction(QIcon::fromTheme("internet-web-browser", QIcon(":/images/button-download.png")), tr("Visit &Website"), this); connect(helpVisitWebsite, SIGNAL(triggered()), this, SLOT(visitWebSite())); helpMenu->addAction(helpVisitWebsite); helpVisitBugTracker = new QAction(QIcon::fromTheme("tools-report-bug", QIcon(":/images/button-edit.png")), tr("Report a &Bug"), // "or Suggest a Feature"? this); connect(helpVisitBugTracker, SIGNAL(triggered()), this, SLOT(visitBugTracker())); helpMenu->addAction(helpVisitBugTracker); helpMenu->addSeparator(); // --- helpVisitPumpGuide = new QAction(QIcon::fromTheme("help-contents", QIcon(":/images/button-download.png")), tr("Pump.io User &Guide"), this); connect(helpVisitPumpGuide, SIGNAL(triggered()), this, SLOT(visitPumpGuide())); helpMenu->addAction(helpVisitPumpGuide); helpVisitPumpTips = new QAction(QIcon::fromTheme("help-hint", QIcon(":/images/button-download.png")), tr("Some Pump.io &Tips"), this); connect(helpVisitPumpTips, SIGNAL(triggered()), this, SLOT(visitTips())); helpMenu->addAction(helpVisitPumpTips); helpVisitPumpUserList = new QAction(QIcon::fromTheme("system-users", QIcon(":/images/button-users.png")), tr("List of Some Pump.io &Users"), this); connect(helpVisitPumpUserList, SIGNAL(triggered()), this, SLOT(visitUserList())); helpMenu->addAction(helpVisitPumpUserList); helpVisitPumpStatus = new QAction(QIcon::fromTheme("network-server", QIcon(":/images/button-online.png")), tr("Pump.io &Network Status Website"), this); connect(helpVisitPumpStatus, SIGNAL(triggered()), this, SLOT(visitPumpStatus())); helpMenu->addAction(helpVisitPumpStatus); helpMenu->addSeparator(); // --- helpAbout = new QAction(QIcon(":/icon/64x64/dianara.png"), tr("About &Dianara"), this); connect(helpAbout, SIGNAL(triggered()), this, SLOT(aboutDianara())); helpMenu->addAction(helpAbout); this->menuBar()->addMenu(helpMenu); ///// Context menu for the tray icon trayTitleSeparatorAction = new QAction("Dianara", this); trayTitleSeparatorAction->setSeparator(true); trayShowWindowAction = new QAction(QIcon(":/icon/64x64/dianara.png"), "*show-window*", this); connect(trayShowWindowAction, SIGNAL(triggered()), this, SLOT(toggleMainWindow())); trayContextMenu = new QMenu("Tray Context Menu", this); trayContextMenu->setSeparatorsCollapsible(false); trayContextMenu->addAction(trayTitleSeparatorAction); // Acts as title trayContextMenu->addAction(trayShowWindowAction); trayContextMenu->addSeparator(); trayContextMenu->addAction(sessionUpdateMainTimeline); trayContextMenu->addAction(sessionUpdateMinorFeedMain); trayContextMenu->addAction(sessionAutoUpdates); trayContextMenu->addSeparator(); trayContextMenu->addAction(sessionMarkAllAsRead); trayContextMenu->addAction(sessionPostNote); trayContextMenu->addSeparator(); trayContextMenu->addAction(settingsEditProfile); trayContextMenu->addAction(settingsConfigure); trayContextMenu->addSeparator(); trayContextMenu->addAction(helpBasicHelp); 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"; } void MainWindow::createToolbar() { this->mainToolBar = addToolBar(tr("Toolbar")); mainToolBar->setObjectName("mainToolBar"); // Not really needed in this case mainToolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); mainToolBar->addAction(this->sessionUpdateMainTimeline); mainToolBar->addAction(this->sessionUpdateMinorFeedMain); mainToolBar->addAction(this->sessionMarkAllAsRead); mainToolBar->addSeparator(); this->settingsFilters->setPriority(QAction::LowPriority); // Don't show text in besides-icon mode mainToolBar->addAction(this->settingsFilters); this->settingsConfigure->setPriority(QAction::LowPriority); mainToolBar->addAction(this->settingsConfigure); mainToolBar->hide(); } /* * Avoid auto-creation of popup menus in the menu bar, tool bar, etc. * */ QMenu *MainWindow::createPopupMenu() { return NULL; } void MainWindow::createStatusbarWidgets() { this->initializationProgressBar = new QProgressBar(this); this->initializationProgressBar->setRange(0, 12); this->initializationProgressBar->setMaximumWidth(100); this->initializationProgressBar->hide(); connect(pumpController, SIGNAL(initializationStepChanged(int)), initializationProgressBar, SLOT(setValue(int))); this->statusStateButton = new QToolButton(this); this->setStateIcon(MainWindow::Initializing); connect(statusStateButton, SIGNAL(clicked()), sessionAutoUpdates, SLOT(toggle())); this->statusLogButton = new QToolButton(this); statusLogButton->setIcon(QIcon::fromTheme("text-x-log", QIcon(":/images/log.png"))); statusLogButton->setToolTip(tr("Open the log viewer")); connect(statusLogButton, SIGNAL(clicked()), viewLogAction, SLOT(trigger())); this->statusBar()->addPermanentWidget(initializationProgressBar); this->statusBar()->addPermanentWidget(statusLogButton); this->statusBar()->addPermanentWidget(statusStateButton); this->statusBar()->setSizeGripEnabled(false); } void MainWindow::setStateIcon(MainWindow::StatusType statusType) { if (statusType == Initializing) { statusStateButton->setDisabled(true); // Until initialization is done statusStateButton->setToolTip(tr("Initializing...")); statusStateButton->setIcon(QIcon::fromTheme("user-offline", QIcon(":/images/button-offline.png"))); } else if (statusType == Autoupdating) { statusStateButton->setEnabled(true); statusStateButton->setToolTip("" + tr("Auto-updating enabled")); statusStateButton->setIcon(QIcon::fromTheme("user-online", QIcon(":/images/button-online.png"))); } else // MainWindow::Stopped { statusStateButton->setEnabled(true); statusStateButton->setToolTip("" + tr("Auto-updating disabled")); statusStateButton->setIcon(QIcon::fromTheme("user-busy", QIcon(":/images/button-busy.png"))); } } /* * 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())); // FIXME: this can mess up the first action in the context menu // 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, int highlightedCount) { if (!trayIconAvailable) { return; } if (count != -1 && highlightedCount != -1) // Valid counts were passed, update { this->trayCurrentNewCount = count; this->trayCurrentHLCount = highlightedCount; } QPixmap iconPixmap; switch (this->trayIconType) { case 0: // Default iconPixmap = QIcon(":/icon/32x32/dianara.png") .pixmap(32, 32) .scaled(32, 32); break; case 1: // system iconset, if available iconPixmap = QIcon::fromTheme("dianara", QIcon(":/icon/32x32/dianara.png")) .pixmap(32, 32) .scaled(32, 32); break; case 2: // Use your own avatar iconPixmap = this->avatarIconButton->icon() .pixmap(32, 32) .scaled(32, 32); break; case 3: // Custom icon iconPixmap = this->trayCustomPixmap; break; } // Paint the number of unread messages on top of the pixmap, if != 0 if (trayCurrentNewCount > 0) { // Draw a pseudo-shadow on the side first QPainter painter(&iconPixmap); if (trayCurrentHLCount > 0) { // Paint a shadow that covers the higher part too painter.drawPixmap(0, 0, 32, 32, QPixmap(":/images/tray-bg-high.png")); } else { // Shadow for the lower part only painter.drawPixmap(0, 0, 32, 32, QPixmap(":/images/tray-bg-low.png")); } QFont font; font.setPixelSize(16); font.setWeight(QFont::Black); // The number QString messagesCountString = QString("%1").arg(trayCurrentNewCount); QPen pen; pen.setBrush(Qt::white); painter.setFont(font); painter.setPen(pen); // Draw the number of new messages painter.drawText(0, 0, 32, 34, // End painting outside, at Y=34, to skip margins Qt::AlignRight | Qt::AlignBottom, messagesCountString); if (trayCurrentHLCount > 0) { // The other number messagesCountString = QString("%1").arg(trayCurrentHLCount); pen.setBrush(Qt::cyan); painter.setPen(pen); // Draw the number of highlighted messages painter.drawText(0, -2, // Start painting outside, at Y=-2, to avoid some margins 32, 32, Qt::AlignRight | Qt::AlignTop, messagesCountString); } } this->trayIcon->setIcon(iconPixmap); } /* * 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", QString()).toString(); this->setTitleAndTrayInfo(tabWidget->currentIndex()); // General program configuration this->updateConfigSettings(); // Load map of known post ID's from disk loadPostsEverSeen(); qDebug() << "Settings loaded"; } /* * Restore main window's state * */ void MainWindow::loadMainWindowConfig() { QSettings settings; settings.beginGroup("MainWindow"); this->resize(settings.value("windowSize", QSize(960, 680)).toSize()); if (!firstRun) // So we should have a proper position saved { this->move(settings.value("windowPosition").toPoint()); this->sideDockWidget->setMinimumWidth(0); // Remove temporary restriction this->restoreState(settings.value("windowState").toByteArray()); } settings.endGroup(); // Since this is called after creating menus and toolbars, we can lock them viewLockPanels->setChecked(true); // Lock to initialize; config loaded later } /* * Save general program settings and state: size, position... * */ void MainWindow::saveSettings() { QSettings settings; if (settings.isWritable()) { settings.setValue("firstRun", false); // General main window status settings.beginGroup("MainWindow"); settings.setValue("windowSize", this->size()); settings.setValue("windowPosition", this->pos()); settings.setValue("windowState", this->saveState()); settings.setValue("viewSidePanel", this->viewSidePanel->isChecked()); settings.setValue("viewToolbar", this->viewToolbar->isChecked()); settings.setValue("viewStatusBar", this->viewStatusBar->isChecked()); settings.setValue("viewLockPanels", this->viewLockPanels->isChecked()); settings.endGroup(); settings.sync(); qDebug() << "MainWindow settings saved:" << settings.fileName(); } else { qDebug() << "Error saving MainWindow settings to:" << settings.fileName() << "\n(disk full?)"; } this->savePostsEverSeen(); } /* * Set PumpController to ignore all SSL errors * */ void MainWindow::enableIgnoringSslErrors() { this->pumpController->setIgnoreSslErrors(true); } /* * Set PumpController's protocol schema to http:// * */ void MainWindow::enableNoHttpsMode() { this->pumpController->setNoHttpsMode(); } /* * Find a post in any of the main timelines; * * To be used to copy comments over to a cloned post * */ Post *MainWindow::findPostInTimelines(QString id, bool *ok) { QList postsInAllTimelines; postsInAllTimelines.append(mainTimeline->getPostsInTimeline()); postsInAllTimelines.append(directTimeline->getPostsInTimeline()); postsInAllTimelines.append(activityTimeline->getPostsInTimeline()); postsInAllTimelines.append(favoritesTimeline->getPostsInTimeline()); *ok = false; foreach (Post *singlePost, postsInAllTimelines) { if (singlePost->getObjectId() == id) { *ok = true; return singlePost; } } return NULL; } void MainWindow::loadPostsEverSeen() { QVariantMap postsEverSeen; QFile dataFile(this->dataDirectory + "/postsEverSeen.ids"); dataFile.open(QIODevice::ReadOnly); while (!dataFile.atEnd()) { QString line = dataFile.readLine(); QStringList splitLine = line.split(" | "); QString key = splitLine.first().trimmed(); QString value = splitLine.last().trimmed(); if (!key.isEmpty() && !value.isEmpty()) { postsEverSeen.insert(key, value); } } dataFile.close(); qDebug() << "Loaded" << postsEverSeen.keys().count() << "IDs from" << dataFile.fileName(); this->postIdsToStore = 0; pumpController->updatePostsEverSeen(postsEverSeen); } /* * Called when quitting, and also sometimes from notifyTimelineUpdate() * */ void MainWindow::savePostsEverSeen() { QVariantMap postsEverSeen = pumpController->getPostsEverSeen(); QFile dataFile(this->dataDirectory + "/postsEverSeen.ids"); dataFile.open(QIODevice::WriteOnly); foreach (QString key, postsEverSeen.keys()) { QByteArray line = key.toLocal8Bit(); line.append(" | "); line.append(postsEverSeen.value(key).toByteArray()); line.append("\n"); dataFile.write(line); } dataFile.close(); this->postIdsToStore = 0; } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////////// 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()); // 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))); // Hide the temporary account button from the status bar, if needed if (this->statusAccountButtonUsed) { this->statusAccountButton->hide(); } // Ensure progress bar is visible, for the first init after account setup this->initializationProgressBar->show(); //////////////////////////////////////////// Restart initialization process this->initializationComplete = false; this->mainTimeline->clearTimeLineContents(); this->directTimeline->clearTimeLineContents(); this->activityTimeline->clearTimeLineContents(); this->favoritesTimeline->clearTimeLineContents(); this->meanwhileFeed->clearContents(); this->mentionsFeed->clearContents(); this->actionsFeed->clearContents(); this->pumpController->setUserCredentials(userID); this->fdNotifier->setCurrentUserId(userID); qDebug() << "UserID updated from AccountDialog:" << userID; } /* * Update settings changed from ConfigDialog() * */ void MainWindow::updateConfigSettings() { this->publisher->syncFromConfig(); QSettings settings; settings.beginGroup("Configuration"); this->updateInterval = settings.value("updateInterval", 5).toInt(); if (updateInterval < 2 || updateInterval > 99) { updateInterval = 5; // TMP validation of stored setting - FIXME // Should be loaded and validated by GlobalObject } this->updateTimer->setInterval(updateInterval * 1000 * 60); // min > msec this->pumpController->setPostsPerPageMain(globalObject->getPostsPerPageMain()); this->pumpController->setPostsPerPageOther(globalObject->getPostsPerPageOther()); this->tabsPosition = settings.value("tabsPosition", QTabWidget::North).toInt(); tabWidget->setTabPosition((QTabWidget::TabPosition)tabsPosition); this->tabsMovable = settings.value("tabsMovable", true).toBool(); tabWidget->setMovable(tabsMovable); int selectedProxyType = settings.value("proxyType", 0).toInt(); // 0 means "no proxy" QNetworkProxy::ProxyType proxyType = QNetworkProxy::NoProxy; if (selectedProxyType == 1) { proxyType = QNetworkProxy::Socks5Proxy; } else if (selectedProxyType == 2) { proxyType = QNetworkProxy::HttpProxy; } QString proxyHostname = settings.value("proxyHostname").toString(); int proxyPort = settings.value("proxyPort", 0).toInt(); bool proxyUseAuth = settings.value("proxyUseAuth", false).toBool(); QString proxyUser = settings.value("proxyUser").toString(); QByteArray proxyPassword = QByteArray::fromBase64(settings.value("proxyPassword").toByteArray()); this->pumpController->setProxyConfig(proxyType, proxyHostname, proxyPort, proxyUseAuth, proxyUser, QString::fromLocal8Bit(proxyPassword)); // Sync PumpController's ignore-SSL-for-images option this->pumpController->setIgnoreSslInImages(globalObject->getPostIgnoreSslInImages()); // Sync privacy options in PumpController this->pumpController->setSilentFollows(globalObject->getSilentFollows()); this->pumpController->setSilentLists(globalObject->getSilentLists()); this->pumpController->setSilentLikes(globalObject->getSilentLikes()); // System tray icon type and custom icon filename, if any this->trayIconType = settings.value("systrayIconType", 0).toInt(); QString trayIconFilename = settings.value("systrayCustomIconFN").toString(); this->trayCustomPixmap = QPixmap(trayIconFilename).scaled(32, 32); if (trayCustomPixmap.isNull()) // Custom icon image is gone or something { trayCustomPixmap = QPixmap(":/icon/32x32/dianara.png"); } this->setTrayIconPixmap(trayCurrentNewCount, trayCurrentHLCount); // Sync icon settings.endGroup(); qDebug() << "updateInterval updated:" << updateInterval << updateInterval*60000; qDebug() << "tabsPosition updated:" << tabsPosition << tabWidget->tabPosition(); qDebug() << "tabsMovable updated:" << tabsMovable; qDebug() << "tray icon type updated:" << trayIconType; } /* * Enable or disable some widgets, depending on whether the applicacion * is authorized or not * */ void MainWindow::toggleWidgetsByAuthorization(bool authorized) { this->sessionUpdateMainTimeline->setEnabled(authorized); this->sessionUpdateDirectTimeline->setEnabled(authorized); this->sessionUpdateActivityTimeline->setEnabled(authorized); this->sessionUpdateFavoritesTimeline->setEnabled(authorized); this->sessionUpdateMinorFeedMain->setEnabled(authorized); this->sessionUpdateMinorFeedDirect->setEnabled(authorized); this->sessionUpdateMinorFeedActivity->setEnabled(authorized); if (authorized) { // TODO FIXME } else { // TODO } } void MainWindow::onInitializationComplete() { this->initializationComplete = true; this->sessionAutoUpdates->setEnabled(true); if (this->sessionAutoUpdates->isChecked()) { this->setStateIcon(MainWindow::Autoupdating); } else { this->setStateIcon(MainWindow::Stopped); } this->initializationProgressBar->hide(); this->adjustTimelineSizes(); // One final adjustment } /* * Stuff executed 1 second after MainWindow is created * */ void MainWindow::postInit() { qDebug() << "postInit();"; this->adjustTimelineSizes(); // Ask for proxy password if needed if (this->pumpController->needsProxyPassword()) { QString proxyPassword = QInputDialog::getText(this, tr("Proxy password required"), tr("You have configured a " "proxy server with " "authentication, but the " "password is not set.") + "\n\n" + tr("Enter the password " "for your proxy server:"), QLineEdit::Password); // FIXME: if this is cancelled, nothing will be loaded until program restart this->pumpController->setProxyPassword(proxyPassword); } QSettings settings; if (settings.value("FirstRunWizard/showWizard", true).toBool()) { this->showFirstRunWizard(); } } void MainWindow::showErrorPopup(QString message) { if (this->fdNotifier->getNotifyErrors()) { this->fdNotifier->showMessage(message); } } /* * 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 from minimized state"; } else { this->toggleMainWindow(); } } } /* * If FreeDesktop.org notifications are not available, * fall back to Qt's balloon ones * */ void MainWindow::showTrayFallbackMessage(QString title, QString message, int duration) { this->trayIcon->showMessage(title, message, QSystemTrayIcon::Information, duration); } void MainWindow::updateProfileData(QString avatarUrl, QString fullName, QString hometown, QString bio, QString eMail) { QString bioTooltip = bio; if (!bio.isEmpty()) { bioTooltip.prepend(""); // make it rich text, so it gets wordwrap bioTooltip.replace("\n", "
              "); // HTML newlines } else { bioTooltip = "" + tr("Your biography is empty") + ""; } this->fullNameLabel->setText(fullName); this->fullNameLabel->setToolTip(bioTooltip); this->userIdLabel->setText(this->userID); this->userIdLabel->setToolTip(bioTooltip); this->userHometownLabel->setText(hometown); this->userHometownLabel->setToolTip(bioTooltip); 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(bioTooltip + "


              " "" + tr("Click to edit your profile") + ""); // Fill/update this info in the profile editor too this->profileEditor->setProfileData(avatarURL, fullName, hometown, bio, eMail); // Refresh tray icon without changing unread/highlighted counts this->setTrayIconPixmap(-1, -1); } /* * Enable or disable the timer that auto-updates the timelines * */ void MainWindow::toggleAutoUpdates(bool checked) { QString message; if (checked) { this->setStateIcon(MainWindow::Autoupdating); this->updateTimer->start(); message = tr("Starting automatic update of timelines, " "once every %1 minutes.").arg(this->updateInterval); } else { this->setStateIcon(MainWindow::Stopped); this->updateTimer->stop(); message = tr("Stopping automatic update of timelines."); } this->setStatusBarMessage(message); this->logViewer->addToLog(message); } /* * Update Main Timeline and Meanwhile feed, if allowed. * Called automatically from a timer. * */ void MainWindow::onTimelineAutoupdate() { bool updatingAllowed = true; // Don't allow updates if the scrollbar is not near the top if (this->mainTimelineScrollArea->verticalScrollBar()->value() > 10) // Kinda TMP... { // FIXME: compare with verticalScrollBart()->minimum() updatingAllowed = false; } // Don't allow updates for any scroll position if page > 1 if (this->mainTimeline->getCurrentPage() > 1) { updatingAllowed = false; } // Regardless of that, if Dianara is hidden in the system tray, updates are fine if (this->isHidden()) { updatingAllowed = true; } if (updatingAllowed) { this->updateMainAndMinorTimelines(); qDebug() << "Updated some timelines automatically after:" << this->updateInterval << "min"; } else { this->bannerNotification->show(); qDebug() << "Not autoupdating timelines because main is not at the top " "of the first page; Showing banner notification instead..."; } } /* * Update all timelines * */ void MainWindow::updateAllTimelines() { mainTimeline->goToFirstPage(); // received timeline will come in a SIGNAL() directTimeline->goToFirstPage(); activityTimeline->goToFirstPage(); favoritesTimeline->goToFirstPage(); meanwhileFeed->updateFeed(); mentionsFeed->updateFeed(); actionsFeed->updateFeed(); qDebug() << "Updated all timelines by menu"; } void MainWindow::onUpdateRequestViaBanner() { // Move to top first this->mainTimelineScrollArea->verticalScrollBar()->setValue(0); this->updateMainAndMinorTimelines(); } void MainWindow::onUpdateDelayedViaBanner() { if (this->sessionAutoUpdates->isChecked()) { this->updateTimer->stop(); this->updateTimer->start(); } } /* * Update Main timeline and Meanwhile feed * * Those, in turn, might update the Messages timeline and the Mentions feed * */ void MainWindow::updateMainAndMinorTimelines() { this->updateTimer->stop(); // Stop early, to avoid double updates meanwhileFeed->updateFeed(); mainTimeline->goToFirstPage(); } /* * Update relevant feeds that need to reflect user's own activity. * * Called after posting. * */ void MainWindow::updateMainActivityMinorTimelines() { this->updateTimer->stop(); // Stop early, avoiding double updates mainTimeline->goToFirstPage(); activityTimeline->goToFirstPage(); meanwhileFeed->updateFeed(); userDidSomethingTimer->start(); // Call updateActionsFeed() a little later qDebug() << "Updated some timelines after posting"; } /* * Update the Favorites timeline. Called by timer some time after the user * liked or unliked a post, unless they keep liking more stuff. * */ void MainWindow::updateFavoritesTimeline() { favoritesTimeline->goToFirstPage(); } /* * Update the Actions feed. Called by timer some time after the user * does something minor, unless they keep doing more things. * */ void MainWindow::updateActionsFeed() { actionsFeed->updateFeed(); // Stop it specifically, for cases when this is called from the menu this->userDidSomethingTimer->stop(); } void MainWindow::scrollMainTimelineTo(QAbstractSlider::SliderAction sliderAction) { this->mainTimelineScrollArea->verticalScrollBar()->triggerAction(sliderAction); // this->adjustTimelineSizes(); } void MainWindow::scrollDirectTimelineTo(QAbstractSlider::SliderAction sliderAction) { this->directTimelineScrollArea->verticalScrollBar()->triggerAction(sliderAction); } void MainWindow::scrollActivityTimelineTo(QAbstractSlider::SliderAction sliderAction) { this->activityTimelineScrollArea->verticalScrollBar()->triggerAction(sliderAction); } void MainWindow::scrollFavoritesTimelineTo(QAbstractSlider::SliderAction sliderAction) { this->favoritesTimelineScrollArea->verticalScrollBar()->triggerAction(sliderAction); } /* * 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, 100); } void MainWindow::scrollDirectTimelineToWidget(QWidget *widget) { this->directTimelineScrollArea->ensureWidgetVisible(widget, 1, 100); } void MainWindow::scrollActivityTimelineToWidget(QWidget *widget) { this->activityTimelineScrollArea->ensureWidgetVisible(widget, 1, 100); } void MainWindow::scrollFavoritesTimelineToWidget(QWidget *widget) { this->favoritesTimelineScrollArea->ensureWidgetVisible(widget, 1, 100); } /* * Scroll to new stuff separator line widget (delayed, called from a timer) * */ void MainWindow::scrollToNewPosts() { this->scrollMainTimelineToWidget(mainTimeline->getSeparatorWidget()); } void MainWindow::notifyTimelineUpdate(PumpController::requestTypes timelineType, int newPostCount, int highlightCount, int directPostCount, int hlByFilterCount, int deletedPostCount, int filteredPostCount, int pendingPostCount, QString currentPage) { /* Stop auto-updates timer to postpone the auto-updates each time a manual * update is triggered (including moving to a different page), but only * for the main timeline. Maybe also for the DirectMessages TL? * */ if (timelineType == PumpController::MainTimelineRequest) { this->updateTimer->stop(); } bool loadedOlderPage = false; // Both of these set to -1 means it's an older page if (highlightCount == -1 && pendingPostCount == -1) { loadedOlderPage = true; } // Update postsEverSeen on disk, when there are enough useful IDs pending if (newPostCount > 0 && !loadedOlderPage) { this->postIdsToStore += newPostCount; // Some might already be in the file, but still... if (postIdsToStore > 10) { this->savePostsEverSeen(); // This sets postIdsToStore back to 0 } } // Restart auto-updates timer ONLY when autoupdates are enabled if (timelineType == PumpController::MainTimelineRequest && this->sessionAutoUpdates->isChecked()) { this->updateTimer->start(); } // Just in case the banner was visible, depending on how update was requested... this->bannerNotification->hide(); QString timelineName = PumpController::getFeedNameAndPath(timelineType).first(); // First handle the simpler case, where the loaded posts are from an older page if (loadedOlderPage) { this->setStatusBarMessage(tr("Received %1 older posts in '%2'.", "%1 is a number, %2 = name of a timeline") .arg(newPostCount) .arg(timelineName) + " " + currentPage); return; } QString statusBarMessage = tr("'%1' updated.", "%1 is the name of a feed") .arg(timelineName); if (newPostCount > 0) { QString timelineUpdatedAt = tr("Timeline updated at %1.") .arg(QTime::currentTime().toString()); // How many NEW QString newPostsString; if (newPostCount == 1) { newPostsString = tr("There is 1 new post."); } else { newPostsString = tr("There are %1 new posts.").arg(newPostCount); } statusBarMessage.append(" " + newPostsString); // How many of those NEW are HIGHLIGHTED QString hlPostsString; if (highlightCount > 0 && timelineType == PumpController::MainTimelineRequest) { QString directMessagesString = tr("Direct messages"); QString byFiltersString = tr("By filters"); if (highlightCount == 1) { hlPostsString = tr("1 highlighted", "singular, refers to a post"); } else { hlPostsString = tr("%1 highlighted", "plural, refers to posts").arg(highlightCount); } hlPostsString.append(" ("); if (directPostCount > 0 && hlByFilterCount > 0) { hlPostsString.append(QString("%1: %2").arg(directMessagesString) .arg(directPostCount) + ", " + QString("%1: %2").arg(byFiltersString) .arg(hlByFilterCount)); } else if (directPostCount > 0) { hlPostsString.append(directMessagesString); } else if (hlByFilterCount > 0) { hlPostsString.append(byFiltersString); } hlPostsString.append(")."); statusBarMessage.append(" " + hlPostsString); } // How many more PENDING to receive next time QString pendingPostsString; if (pendingPostCount > 0) { if (pendingPostCount == 1) { pendingPostsString = tr("1 more pending to receive.", "singular, one post"); } else { pendingPostsString = tr("%1 more pending to receive.", "plural, several posts") .arg(pendingPostCount); } statusBarMessage.append(" " + pendingPostsString); } // Also: how many are deleted or filtered out if (filteredPostCount > 0 || deletedPostCount > 0) { statusBarMessage.append(" " + tr("Also:")); QString filteredPostsString; if (filteredPostCount > 0) { if (filteredPostCount == 1) { filteredPostsString = tr("1 filtered out.", "singular, refers to a post"); } else { filteredPostsString = tr("%1 filtered out.", "plural, refers to posts") .arg(filteredPostCount); } statusBarMessage.append(" " + filteredPostsString); } QString deletedPostsString; if (deletedPostCount > 0) { if (deletedPostCount == 1) { deletedPostsString = tr("1 deleted.", "singular, refers to a post"); } else { deletedPostsString = tr("%1 deleted.", "plural, refers to posts") .arg(deletedPostCount); } statusBarMessage.append(" " + deletedPostsString); } } this->setStatusBarMessage(statusBarMessage); // Only for the main timeline if (timelineType == PumpController::MainTimelineRequest) { if (fdNotifier->getNotifyHLTimeline() && highlightCount > 0) { fdNotifier->showMessage(timelineUpdatedAt + "\n\n" + newPostsString + "\n" + hlPostsString + "\n" + pendingPostsString); // FIXME: needs to depend on notification style if (globalObject->getNotifyInTaskbar()) { qApp->alert(this); } } else if (fdNotifier->getNotifyNewTimeline()) { fdNotifier->showMessage(timelineUpdatedAt + "\n\n" + newPostsString + "\n" + pendingPostsString); // FIXME: as the one above if (globalObject->getNotifyInTaskbar()) { qApp->alert(this); } } this->logViewer->addToLog(statusBarMessage); // Jump to new stuff separator, if enabled if (this->globalObject->getJumpToNewOnUpdate() && this->initializationComplete) // Not for the first load! { /* Timer will jump in one second, after events have been * processed, and widgets have been adjusted */ this->delayedScrollTimer->start(1000); } } } else { if (timelineType == PumpController::MainTimelineRequest || timelineType == PumpController::DirectTimelineRequest) { // Only add 'no new posts' for timelines that can have unread posts statusBarMessage.append(QString::fromUtf8(" \342\210\205 ") // Empty set + tr("No new posts.")); } this->setStatusBarMessage(statusBarMessage); } // And adjust timelines sizes // NOW DISABLING: makes reloads a lot slower for no good reason ///// this->adjustTimelineSizes(); // If conditions are right, update the Messages tab, too if (timelineType == PumpController::MainTimelineRequest) { // Store statusBarMessage, to restore after the Messages timeline update this->previousStatusFeedInfo = statusBarMessage; // Some new direct messages (though maybe via Cc), so update that timeline if (directPostCount > 0 || pendingPostCount > 0) // Also if more than max new posts { if (initializationComplete) // But not the very first time { this->directTimeline->goToFirstPage(); } } // Also, update menu info label with current time this->menuInfoWidget->hide(); // Hide info widget before updating label // to ensure it gets the proper size this->menuInfoLabel->setText(tr("Last update: %1") .arg(QTime::currentTime().toString())); this->menuInfoLabel->setToolTip(QDate::currentDate().toString()); // Show again, but only if the menu bar is not native (not handled by environment) this->menuInfoWidget->setVisible(!this->menuBar()->isNativeMenuBar()); } else { /* * Updated other timeline, and we have a previous statusbar message, * which means this is an auto-update. * If this update didn't bring anything new, we'll restore the previous * status message, which might have been more interesting. * */ if (newPostCount == 0 && !this->previousStatusFeedInfo.isEmpty()) { this->setStatusBarMessage(this->previousStatusFeedInfo); } this->previousStatusFeedInfo.clear(); // Empty for next time } } /* * Update timelines titles with number of new posts * */ void MainWindow::setTimelineTabTitle(PumpController::requestTypes timelineType, int newPostCount, int highlightCount, int totalFeedPostCount) { int updatedTab = 0; QString unreadPrefix; // Will containg an * if there are new posts QString messageCountString; if (newPostCount > 0) { messageCountString = QString(" (%1)").arg(newPostCount); unreadPrefix = "* "; if (highlightCount > 0 && timelineType == PumpController::MainTimelineRequest) { // Showing count of highlighted posts makes sense in the main TL only messageCountString.append(QString(" [%1]").arg(highlightCount)); unreadPrefix.prepend("*"); } } QString totalItemsString = "\n\n" + tr("Total posts: %1") .arg(QLocale::system() .toString(totalFeedPostCount)); switch (timelineType) { case PumpController::MainTimelineRequest: this->tabWidget->setTabText(0, unreadPrefix + tr("&Timeline") + messageCountString); this->tabWidget->setTabToolTip(0, tr("The main timeline") + totalItemsString); updatedTab = 0; break; case PumpController::DirectTimelineRequest: this->tabWidget->setTabText(1, unreadPrefix + tr("&Messages") + messageCountString); this->tabWidget->setTabToolTip(1, tr("Messages sent explicitly to you") + totalItemsString); updatedTab = 1; break; case PumpController::ActivityTimelineRequest: this->tabWidget->setTabText(2, unreadPrefix + tr("&Activity") + messageCountString); this->tabWidget->setTabToolTip(2, tr("Your own posts") + totalItemsString); updatedTab = 2; break; case PumpController::FavoritesTimelineRequest: this->tabWidget->setTabText(3, unreadPrefix + tr("Favor&ites") + messageCountString); this->tabWidget->setTabToolTip(3, tr("Your favorited posts") + totalItemsString); updatedTab = 3; break; default: updatedTab = -1; qDebug() << "setTimelineTabTitle() called with wrong feed type!"; } // 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) { this->setTrayIconPixmap(newPostCount, highlightCount); } } /* * 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; if (!this->userID.isEmpty()) { title.append(" - " + userID); } title.append(" - Dianara"); /* Not sure if I like it if (currentTabTitle.endsWith(")")) // kinda TMP { title.prepend("* "); } */ this->setWindowTitle(title); if (trayIconAvailable) { QString idToShow = this->userID.isEmpty() ? tr("Your Pump.io account is not configured") : userID; #ifdef Q_OS_UNIX title = "Dianara: " + currentTabTitle + "

              [" + idToShow + "]"; #else // Some OSes don't render HTML in the tray tooltip title = "Dianara: " + currentTabTitle + "\n\n[" + idToShow + "]"; #endif this->trayIcon->setToolTip(title); this->trayTitleSeparatorAction->setText("Dianara - " + this->userID); } } /* * Set the title for the minor feed (Meanwhile), with new item count * */ void MainWindow::setMinorFeedTitle(int newItemsCount, int newHighlightedItemsCount) { QString title = PumpController::getFeedNameAndPath(PumpController::MinorFeedMainRequest) .first() + "..."; if (newItemsCount > 0) { title.append(QString(" (%1)").arg(newItemsCount)); if (newHighlightedItemsCount > 0) { title.append(QString(" [%1]").arg(newHighlightedItemsCount)); } } this->leftPanel->setItemText(0, title); } void MainWindow::setMentionsFeedTitle(int newItemsCount, int newHighlightedItemsCount) { Q_UNUSED(newHighlightedItemsCount) QString title = PumpController::getFeedNameAndPath(PumpController::MinorFeedDirectRequest) .first(); if (newItemsCount > 0) { title.append(QString(" (%1)").arg(newItemsCount)); } this->leftPanel->setItemText(1, title); } void MainWindow::notifyMinorFeedUpdate(PumpController::requestTypes feedType, int newItemsCount, int highlightedCount, int filteredCount, int pendingItemsCount) { QString feedName = PumpController::getFeedNameAndPath(feedType).first(); // First, handle the simpler case, where items are older if (highlightedCount == -1 && pendingItemsCount == -1) { // When both of these are -1 this->setStatusBarMessage(tr("Received %1 older activities in '%2'.", "%1 is a number, %2 = name of feed") .arg(newItemsCount) .arg(feedName)); return; } QString statusBarMessage = tr("'%1' updated.", "%1 is the name of a feed").arg(feedName); if (newItemsCount > 0) { QString mwUpdatedAt = tr("Minor feed updated at %1.") .arg(QTime::currentTime().toString()); // How many NEW items QString newItemsString; if (newItemsCount == 1) { newItemsString = tr("There is 1 new activity."); } else { newItemsString = tr("There are %1 new activities.") .arg(newItemsCount); } statusBarMessage.append(" " + newItemsString); // How many of those NEW items are highlighted QString hlItemsString; if (highlightedCount > 0 && feedType == PumpController::MinorFeedMainRequest) // HL only matter in the Meanwhile feed { if (highlightedCount == 1) { hlItemsString = tr("1 highlighted.", "singular, refers to an activity"); } else { hlItemsString = tr("%1 highlighted.", "plural, refers to activities") .arg(highlightedCount); } statusBarMessage.append(" " + hlItemsString); } // How many more items are PENDING to receive on next update QString pendingItemsString; if (pendingItemsCount > 0) { if (pendingItemsCount == 1) { pendingItemsString = tr("1 more pending to receive.", "singular, 1 activity"); } else { pendingItemsString = tr("%1 more pending to receive.", "plural, several activities") .arg(pendingItemsCount); } statusBarMessage.append(" " + pendingItemsString); } // Also: how many are filtered out QString filteredItemsString; if (filteredCount > 0) { statusBarMessage.append(" " + tr("Also:")); if (filteredCount == 1) { filteredItemsString = tr("1 filtered out.", "singular, refers to one activity"); } else { filteredItemsString = tr("%1 filtered out.", "plural, several activities") .arg(filteredCount); } statusBarMessage.append(" " + filteredItemsString); } // Some stuff is only done for the Meanwhile feed: HL counts, notifications... if (feedType == PumpController::MinorFeedMainRequest) { if (highlightedCount > 0) { if (fdNotifier->getNotifyHLMeanwhile()) { fdNotifier->showMessage(mwUpdatedAt + "\n\n" + newItemsString + "\n" + hlItemsString + "\n" + pendingItemsString); // FIXME: needs to depend on notification style if (globalObject->getNotifyInTaskbar()) { qApp->alert(this); } } } else { if (fdNotifier->getNotifyNewMeanwhile()) { fdNotifier->showMessage(mwUpdatedAt + "\n\n" + newItemsString + "\n" + pendingItemsString); if (globalObject->getNotifyInTaskbar()) { qApp->alert(this); } } } // Store statusBarMessage, to restore after the Mentions and // Actions feeds are updated, in case they have nothing new if (feedType == PumpController::MinorFeedMainRequest) { this->previousStatusFeedInfo = statusBarMessage; // Some highlighted in the Meanwhile could mean some new mentions, so update that feed if (highlightedCount > 0 || pendingItemsCount > 0) // Also if more than max new activities { if (initializationComplete) // But not the first time { this->mentionsFeed->updateFeed(); } } } } this->setStatusBarMessage(statusBarMessage); this->logViewer->addToLog(statusBarMessage); } else { if (feedType != PumpController::MinorFeedActivityRequest) { // Don't add redundant 'no activities' for Actions feed statusBarMessage.append(QString::fromUtf8(" \342\210\205 ") // Empty set + tr("No new activities.")); } // For Mentions/Actions, use previously stored useful message, if any if (feedType != PumpController::MinorFeedMainRequest && !this->previousStatusFeedInfo.isEmpty()) { statusBarMessage = previousStatusFeedInfo; } previousStatusFeedInfo.clear(); this->setStatusBarMessage(statusBarMessage); } } /* * Store avatars on disk * */ void MainWindow::storeAvatar(QByteArray avatarData, QString avatarUrl) { QString fileName = MiscHelpers::getCachedAvatarFilename(avatarUrl); qDebug() << "Saving avatar to disk: " << fileName; QFile avatarFile(fileName); avatarFile.open(QFile::WriteOnly); avatarFile.write(avatarData); avatarFile.close(); this->pumpController->notifyAvatarStored(avatarUrl, 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, QString imageUrl) { QString fileName = MiscHelpers::getCachedImageFilename(imageUrl); QFile imageFile(fileName); bool fileOpenedOk = imageFile.open(QFile::WriteOnly); imageFile.write(imageData); imageFile.close(); qDebug() << "Saving image to disk: " << fileName << "; File opened OK? " << fileOpenedOk; if (fileOpenedOk && imageFile.size() > 0) { qDebug() << "Saved " << imageFile.size() << "bytes"; this->pumpController->notifyImageStored(imageUrl); } else { qDebug() << "Couldn't save" << fileName << "to disk!"; QString message = tr("Error storing image!") + " (" + tr("%1 bytes").arg(imageData.size()) + ", " + imageUrl + ")"; this->setStatusBarMessage(message); logViewer->addToLog(message); if (fileOpenedOk) // Opened OK but no disk space, or something { imageFile.remove(); } this->pumpController->notifyImageFailed(imageUrl); } qDebug() << "imageData size:" << imageData.size(); } /* * Update status bar message, from PumpController's signals * */ void MainWindow::setStatusBarMessage(QString message) { this->oldStatusBarMessage = "[" + QTime::currentTime().toString() + "] " + message; this->statusBar()->showMessage(oldStatusBarMessage, 0); } /* * Show a temporary message in the statusBar * * If message is empty, restore the old one * */ void MainWindow::setTransientStatusMessage(QString message) { if (!message.isEmpty()) { if (message.startsWith("http")) { message = tr("Link to: %1").arg(message); } this->statusBar()->showMessage(message, 0); } else { // Old status message was saved in setStatusBarMessage() this->statusBar()->showMessage(oldStatusBarMessage, 0); } } /* * Mark all posts in all timelines as read * and clear their counters * */ void MainWindow::markAllAsRead() { this->setTransientStatusMessage(tr("Marking everything as read...")); qApp->processEvents(); mainTimeline->markPostsAsRead(); directTimeline->markPostsAsRead(); // activityTimeline and favoritesTimeline don't need this; they're never "unread" meanwhileFeed->markAllAsRead(); this->setMinorFeedTitle(0, 0); mentionsFeed->markAllAsRead(); this->setMentionsFeedTitle(0, 0); // activitiesFeed doesn't need this, either this->setTransientStatusMessage(QString()); // Restore } void MainWindow::startPost(QString title, QString content) { if (this->isHidden()) { this->toggleMainWindow(); } this->publisher->setFullMode(); // Don't try setting title and content if called via menu, only via D-Bus if (!title.isEmpty() || !content.isEmpty()) { this->publisher->setTitleAndContent(title, content); } } /* * Refresh all timestamps in the minor feed, in timelines posts, * and in comments * */ void MainWindow::refreshAllTimestamps() { this->mainTimeline->updateFuzzyTimestamps(); this->directTimeline->updateFuzzyTimestamps(); this->activityTimeline->updateFuzzyTimestamps(); this->favoritesTimeline->updateFuzzyTimestamps(); this->meanwhileFeed->updateFuzzyTimestamps(); this->mentionsFeed->updateFuzzyTimestamps(); this->actionsFeed->updateFuzzyTimestamps(); } /* * 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 timelineWidth; int timelineHeight; // Get the right timelinewidth based on currently active tab's timeline width switch (this->tabWidget->currentIndex()) { case 0: // main timelineWidth = mainTimelineScrollArea->viewport()->width(); timelineHeight = mainTimelineScrollArea->viewport()->height(); break; case 1: // direct timelineWidth = directTimelineScrollArea->viewport()->width(); timelineHeight = directTimelineScrollArea->viewport()->height(); break; case 2: // activity timelineWidth = activityTimelineScrollArea->viewport()->width(); timelineHeight = activityTimelineScrollArea->viewport()->height(); break; case 3: // favorites timelineWidth = favoritesTimelineScrollArea->viewport()->width(); timelineHeight = favoritesTimelineScrollArea->viewport()->height(); break; default: // Contacts tab, ATM timelineWidth = contactManager->width(); //->visibleRegion().boundingRect().width(); //->width() - scrollbarWidth; timelineHeight = contactManager->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; } this->globalObject->storeTimelineHeight(timelineHeight); // Resize switch (this->tabWidget->currentIndex()) { case 0: // main mainTimeline->resizePosts(QList(), true); // resizeAll=true break; case 1: // direct directTimeline->resizePosts(QList(), true); break; case 2: // activity activityTimeline->resizePosts(QList(), true); break; case 3: // favorites favoritesTimeline->resizePosts(QList(), true); break; default: // Contacts tab, ATM break; } } void MainWindow::showUserTimeline(QString userId, QString userName, QIcon userAvatar, QString userOutbox) { UserPosts *userTimeline = new UserPosts(userId, userName, userAvatar, userOutbox, this->pumpController, this->globalObject, this->filterChecker, nullptr); // No parent, independent window userTimeline->show(); } /* * Lock or unlock toolbars and docks * */ void MainWindow::toggleLockedPanels(bool locked) { if (locked) { sideDockWidget->setTitleBarWidget(sideDockTitleWidget); sideDockWidget->setFeatures(QDockWidget::NoDockWidgetFeatures); } else { sideDockWidget->setTitleBarWidget(nullptr); sideDockWidget->setFeatures(QDockWidget::DockWidgetMovable); } mainToolBar->setMovable(!locked); } /* * 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->sideDockWidget->setVisible(shown); if (!shown) { qApp->processEvents(); this->adjustTimelineSizes(); } } void MainWindow::toggleToolbar(bool shown) { qDebug() << "Showing toolbar:" << shown; this->mainToolBar->setVisible(shown); } /* * Hide or show the status bar * */ void MainWindow::toggleStatusBar(bool shown) { qDebug() << "Showing status bar:" << 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(); } } void MainWindow::toggleMeanwhileFeed() { this->leftPanel->setCurrentIndex(0); this->meanwhileFeed->setFocus(); } void MainWindow::toggleMentionsFeed() { this->leftPanel->setCurrentIndex(1); this->mentionsFeed->setFocus(); } void MainWindow::toggleActionsFeed() { this->leftPanel->setCurrentIndex(2); this->actionsFeed->setFocus(); } void MainWindow::showFirstRunWizard() { FirstRunWizard *frWizard = new FirstRunWizard(this->accountDialog, this->profileEditor, this->configDialog, this->helpWidget, this->globalObject, this); frWizard->show(); } /* * Open website in browser * */ void MainWindow::visitWebSite() { qDebug() << "Opening website in browser"; MiscHelpers::openUrl(QUrl("https://jancoding.wordpress.com/dianara"), this); } void MainWindow::visitBugTracker() { qDebug() << "Opening bugtracker in browser"; MiscHelpers::openUrl(QUrl("https://gitlab.com/dianara/dianara-dev/issues"), this); } /* * Open Pump.io's User Guide in web browser * (currently at readthedocs.org/.io) * */ void MainWindow::visitPumpGuide() { qDebug() << "Opening Pump.io User Guide in browser"; MiscHelpers::openUrl(QUrl("https://pumpio.readthedocs.io/" "en/latest/userguide.html"), this); } void MainWindow::visitTips() { qDebug() << "Opening Pump.io tips in browser"; MiscHelpers::openUrl(QUrl("https://communicationfreedom.wordpress.com/" "2014/03/17/pump-io-tips/"), this); } void MainWindow::visitUserList() { qDebug() << "Opening Pump.io users-by-language wiki page in browser"; MiscHelpers::openUrl(QUrl("https://github.com/pump-io/" "pump.io/wiki/Users-by-language"), this); } void MainWindow::visitPumpStatus() { qDebug() << "Opening Pump.io Network Status website in browser"; MiscHelpers::openUrl(QUrl("https://sjoberg.fi/pumpcheck.txt"), // TEMPORARY URL -- FIXME this); // http://pumpstatus.strugee.net should be used in the near future // and something more official further down the road } /* * About... message * */ void MainWindow::aboutDianara() { QMessageBox::about(this, tr("About Dianara"), QString("Dianara v%1") .arg(qApp->applicationVersion()) + "
              " "Copyright 2012-2017 JanKusanagi
              " "" "https://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 and " "other media, interact with posts, manage " "your contacts and follow new people.") + "
              " // --- + tr("English translation by JanKusanagi.", "TRANSLATORS: Change this with your language and " "name. If there was another translator before you, " "add your name after theirs ;)") + "
              " "
              " + tr("Thanks to all the testers, translators and " "packagers, who help make Dianara better!") + "
              " // --- + tr("Dianara is Free Software, licensed under the " "GNU GPL license, and uses some Oxygen icons " "under LGPL license.") + "
              " "
              " "" "GNU GPL v2 - " "" "GNU LGPL v2.1" "
              " "" "techbase.kde.org/Projects/Oxygen" "
              " "
              " + QString("" "Qt v%1").arg(qVersion()) + QString(" — %1").arg(qApp->platformName())); } void MainWindow::toggleMainWindow(bool firstTime) { bool shouldHide = false; if (firstTime) { shouldHide = this->globalObject->getHideInTray(); if (shouldHide) { qDebug() << "Hiding window on startup due to configuration"; } } if ((this->isHidden() && !shouldHide) || !this->trayIconAvailable) { this->trayShowWindowAction->setText(tr("&Hide Window")); this->show(); this->activateWindow(); // Try to avoid WM putting the window behind others qDebug() << "SHOWING main window"; // Call adjustTimelineSizes() half a second later delayedResizeTimer->stop(); delayedResizeTimer->start(500); } else { this->trayShowWindowAction->setText(tr("&Show Window")); this->hide(); qDebug() << "HIDING main window"; } } /* * React to buttons being pressed on a system notification, * previously sent by Dianara. * * Slot connected to a DBus signal. * */ void MainWindow::onNotificationAction(uint id, QString action) { qDebug() << "Received from org.freedesktop.Notifications:" << id << action; if (action == QString("dianara_%1_show").arg(qApp->applicationPid())) { qDebug() << "It's for us!"; if (this->isHidden()) { this->toggleMainWindow(); } else { this->activateWindow(); } } } void MainWindow::showAuthError(QString title, QString message) { QMessageBox::warning(this, title, message); } void MainWindow::onSessionManagerQuitRequest(QSessionManager &manager) { Q_UNUSED(manager) std::cout << "Session manager requested shutdown\n"; std::cout.flush(); this->reallyQuitProgram = true; this->quitProgram(tr("Closing due to environment shutting down...")); } /* * Close the program. Needed to quit correctly from context menu * */ void MainWindow::quitProgram(QString reason) { if (this->isHidden()) { this->show(); } saveSettings(); if (!reallyQuitProgram && this->publisher->isFullMode()) // If composer is active, ask for confirmation { // FIXME: composing comments should also block here int confirmation = QMessageBox::question(this, tr("Quit?"), tr("You are composing a note or a comment.") + "\n\n" + tr("Do you really want to close Dianara?"), tr("&Yes, close the program"), tr("&No"), QString(), 1, 1); if (confirmation == 0) { qDebug() << "Exit confirmed; quitting, bye!"; reallyQuitProgram = true; } else { qDebug() << "Confirmation cancelled, not exiting"; reallyQuitProgram = false; } } else // FIXME: some redundancies here... { reallyQuitProgram = true; } if (reallyQuitProgram) { if (reason.isEmpty()) { reason = tr("Shutting down Dianara..."); } this->setStatusBarMessage(reason); this->globalObject->logMessage(reason); std::cout << "\nShutting down Dianara (" + this->userID.toStdString() + ")...\n"; std::cout.flush(); this->globalObject->notifyProgramShutdown(); // Manually stop and remove some stuff; trying to minimize shutdown time this->updateTimer->stop(); this->timestampsTimer->stop(); this->favoritesReloadTimer->stop(); this->userDidSomethingTimer->stop(); this->mainTimeline->clearTimeLineContents(false); // Don't show 'loading' this->directTimeline->clearTimeLineContents(false); this->activityTimeline->clearTimeLineContents(false); this->favoritesTimeline->clearTimeLineContents(false); this->meanwhileFeed->clearContents(); this->mentionsFeed->clearContents(); this->actionsFeed->clearContents(); this->publisher->deleteLater(); this->contactManager->deleteLater(); this->logViewer->close(); this->logViewer->deleteLater(); this->helpWidget->close(); this->helpWidget->deleteLater(); this->filterEditor->close(); this->filterEditor->deleteLater(); qApp->setQuitOnLastWindowClosed(true); // Add more needed shutdown stuff here qApp->processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers); qApp->closeAllWindows(); //qApp->processEvents(); std::cout << "All windows closed, bye! o/\n\n"; std::cout.flush(); /* This is needed to avoid problems when building with Qt 5, and * running under Plasma 5 * */ qApp->quit(); } } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// PROTECTED ////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void MainWindow::closeEvent(QCloseEvent *event) { qDebug() << "MainWindow::closeEvent()"; // FIXME: handle cases where tray icon isn't available // Handle also really closing via environment shutdown if (!trayIconAvailable && !reallyQuitProgram) { int confirmation = QMessageBox::question(this, tr("Quit?"), tr("System tray icon is not available.") + " " + tr("Dianara cannot be hidden in the " "system tray.") + "\n\n" + tr("Do you want to close the program " "completely?"), tr("&Yes, close the program"), tr("&No"), QString(), 1, 1); if (confirmation == 0) { qDebug() << "Main window closed without a tray icon, shutting down"; reallyQuitProgram = true; this->quitProgram(); } else { qDebug() << "Confirmation cancelled, not closing main window"; event->ignore(); return; } } if (reallyQuitProgram) { event->accept(); // really close, if called from Quit menu qDebug() << "Quit called from menu, shutting down program"; } else { this->toggleMainWindow(); // 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(); delayedResizeTimer->stop(); delayedResizeTimer->start(500); // call adjustTimelineSizes() a little later event->accept(); } dianara-v1.4.1/src/timeline.cpp0000644000175000017500000014477113221567115014526 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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(PumpController::requestTypes timelineType, PumpController *pumpController, GlobalObject *globalObject, FilterChecker *filterChecker, QWidget *parent) : QWidget(parent) { m_pumpController = pumpController; connect(m_pumpController, &PumpController::timelineFailed, this, &TimeLine::onUpdateFailed); m_globalObject = globalObject; m_filterChecker = filterChecker; m_timelineType = timelineType; m_isFavoritesTimeline = false; // Initialize this->setMinimumSize(180, 180); // Ensure something's always visible // 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("url", "https://jancoding.wordpress.com/dianara"); demoAuthorData.insert("location", demoLocationData); demoAuthorData.insert("summary", "I am not a real user"); QVariantMap demoGeneratorData; demoGeneratorData.insert("displayName", "Dianara"); QVariantMap demoObjectData; demoObjectData.insert("objectType", "note"); demoObjectData.insert("id", "demo-post-id"); #if 0 // =1 for quick ImageViewer tests demoObjectData.insert("objectType", "image"); QVariantMap demoImageData; demoImageData.insert("url", "http://dianara.nongnu.org/images/mageia.jpg"); demoObjectData.insert("fullImage", demoImageData); #endif // Show date/time when latest stable version was released demoObjectData.insert("published", "2017-12-30T15:00:00Z"); QSettings settings; // FIXME: kinda tmp, until posts have "unread" status, etc. settings.beginGroup("TimelineStates"); // Demo post content depends on timeline type; also, restore some feed values switch (m_timelineType) { case PumpController::MainTimelineRequest: 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, for instance:") + "
              " "" "http://pump.io/tryit.html" "

              " + tr("Press F1 if you want to open the Help window.") + "

              " + 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 over a button or a text field with " "your mouse, you'll probably see some " "extra information.") + "

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

              " "" + tr("Pump.io User Guide") + "" + "

              "); m_previousNewestPostId = settings.value("previousNewestPostIdMain") .toString(); m_fullTimelinePostCount = settings.value("totalPostsMain").toInt(); break; case PumpController::DirectTimelineRequest: demoObjectData.insert("displayName", tr("Direct Messages Timeline")); demoObjectData.insert("content", tr("Here, you'll see posts " "specifically directed to you.") + "


              "); m_previousNewestPostId = settings.value("previousNewestPostIdDirect") .toString(); m_fullTimelinePostCount = settings.value("totalPostsDirect").toInt(); break; case PumpController::ActivityTimelineRequest: demoObjectData.insert("displayName", tr("Activity Timeline")); demoObjectData.insert("content", tr("You'll see your own posts here.") + "


              "); m_previousNewestPostId = settings.value("previousNewestPostIdActivity") .toString(); m_fullTimelinePostCount = settings.value("totalPostsActivity").toInt(); break; case PumpController::FavoritesTimelineRequest: demoObjectData.insert("displayName", tr("Favorites Timeline")); demoObjectData.insert("content", tr("Posts and comments you've liked.") + "


              "); m_previousNewestPostId = settings.value("previousNewestPostIdFavorites") .toString(); m_fullTimelinePostCount = settings.value("totalPostsFavorites").toInt(); m_isFavoritesTimeline = true; break; default: demoObjectData.insert("content", "

              Empty timeline

              "); } settings.endGroup(); QVariantMap demoPostData; demoPostData.insert("actor", demoAuthorData); demoPostData.insert("generator", demoGeneratorData); demoPostData.insert("object", demoObjectData); demoPostData.insert("id", "demo-activity-id"); m_firstLoad = true; m_gettingNew = true; // First time should be true m_unreadPostsCount = 0; m_timelineOffset = 0; m_oldTimelineOffset = 0; m_wasOnFirstPage = true; m_postsPendingForNextTime = 0; this->syncPostsPerPage(); // Separator frame, to mark where new posts from the last batch end m_separatorFrame = new QFrame(this); m_separatorFrame->setFrameStyle(QFrame::HLine); m_separatorFrame->setMinimumHeight(28); m_separatorFrame->setContentsMargins(0, 8, 0, 8); m_separatorFrame->hide(); // Info label, shown when there are no posts, or to indicate loading and such m_infoLabel = new QLabel(this); m_infoLabel->setAlignment(Qt::AlignCenter); m_infoLabel->setWordWrap(true); m_infoLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding); m_infoLabel->hide(); m_getNewPendingButton = new QPushButton(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), "*get more pending messages*", this); m_getNewPendingButton->setFlat(true); connect(m_getNewPendingButton, &QAbstractButton::clicked, this, &TimeLine::getNewPending); m_getNewPendingButton->hide(); // This will hold the posts and be hidden or disabled when needed m_postsWidget = new QWidget(this); m_postsWidget->setContentsMargins(0, 0, 0, 0); /* * Allow focus in the post holder itself, to avoid focus going to * the pagination buttons when cancelling post creation in the publisher * */ m_postsWidget->setFocusPolicy(Qt::StrongFocus); if (m_timelineType == PumpController::UserTimelineRequest) { m_postsWidget->hide(); // Will be shown when ready // Hiding it now ensures the infoLabel messages appear well centered } m_firstPageButton = new QPushButton(QIcon::fromTheme("go-first", QIcon(":/images/button-previous.png")), tr("Newest"), this); m_firstPageButton->setFocusPolicy(Qt::ClickFocus); connect(m_firstPageButton, &QAbstractButton::clicked, this, &TimeLine::goToFirstPage); m_previousPageButton = new QPushButton(QIcon::fromTheme("go-previous", QIcon(":/images/button-previous.png")), tr("Newer"), this); m_previousPageButton->setFocusPolicy(Qt::ClickFocus); connect(m_previousPageButton, &QAbstractButton::clicked, this, &TimeLine::goToPreviousPage); m_pageSelector = new PageSelector(this); connect(m_pageSelector, &PageSelector::pageJumpRequested, this, &TimeLine::goToSpecificPage); m_currentPageButton = new QPushButton(QIcon::fromTheme("go-next-view-page"), "1 / 1", // Correct value will be set on real update this); m_currentPageButton->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); m_currentPageButton->setFocusPolicy(Qt::ClickFocus); connect(m_currentPageButton, &QAbstractButton::clicked, this, &TimeLine::showPageSelector); m_nextPageButton = new QPushButton(QIcon::fromTheme("go-next", QIcon(":/images/button-next.png")), tr("Older"), this); m_nextPageButton->setFocusPolicy(Qt::ClickFocus); connect(m_nextPageButton, &QAbstractButton::clicked, this, &TimeLine::goToNextPage); // Set reversed icons for bottom buttons if RTL language causes reversed layout if (qApp->layoutDirection() == Qt::RightToLeft) { m_firstPageButton->setIcon(QIcon::fromTheme("go-last", QIcon(":/images/button-next.png"))); m_previousPageButton->setIcon(QIcon::fromTheme("go-next", QIcon(":/images/button-next.png"))); m_nextPageButton->setIcon(QIcon::fromTheme("go-previous", QIcon(":/images/button-previous.png"))); } ///// Layout m_postsLayout = new QVBoxLayout(); m_postsLayout->setContentsMargins(0, 0, 0, 0); m_postsWidget->setLayout(m_postsLayout); // Setting alignment of this layout to AlignTop caused all posts to be // compressed at the top when there were a lot of them; removed m_bottomLayout = new QHBoxLayout(); m_bottomLayout->addSpacing(2); m_bottomLayout->addWidget(m_firstPageButton, 3); m_bottomLayout->addSpacing(2); m_bottomLayout->addStretch(1); m_bottomLayout->addSpacing(2); m_bottomLayout->addWidget(m_previousPageButton, 3); m_bottomLayout->addWidget(m_currentPageButton, 1); m_bottomLayout->addWidget(m_nextPageButton, 3); m_bottomLayout->addSpacing(2); m_mainLayout = new QVBoxLayout(); m_mainLayout->setContentsMargins(0, 0, 0, 0); m_mainLayout->addWidget(m_getNewPendingButton); m_mainLayout->addWidget(m_infoLabel, 2); m_mainLayout->addWidget(m_postsWidget, 1); m_mainLayout->addStretch(0); // Ensure buttons are always at the bottom m_mainLayout->addSpacing(2); // 2 pixel separation m_mainLayout->addLayout(m_bottomLayout, 0); this->setLayout(m_mainLayout); this->createKeyboardActions(); // Add the default "demo" post if (timelineType != PumpController::UserTimelineRequest) { ASActivity *demoActivity = new ASActivity(demoPostData, QString(), this); Post *demoPost = new Post(demoActivity, false, // Not highlighted false, // Not standalone m_pumpController, m_globalObject, this); m_postsInTimeline.append(demoPost); m_postsLayout->addWidget(demoPost); } else { this->showMessage(tr("Requesting...")); } // Sync avatar's follow state for every post when there are changes in the Following list connect(m_pumpController, &PumpController::followingListChanged, this, &TimeLine::updateAvatarFollowStates); // Also, sort the Following list completion model // Disable buttons initially, until something is received this->disablePaginationButtons(); qDebug() << "TimeLine created"; } /* * Destructor stores timeline states in the settings * */ TimeLine::~TimeLine() { QSettings settings; settings.beginGroup("TimelineStates"); switch (m_timelineType) { case PumpController::MainTimelineRequest: settings.setValue("previousNewestPostIdMain", m_previousNewestPostId); settings.setValue("totalPostsMain", m_fullTimelinePostCount); break; case PumpController::DirectTimelineRequest: settings.setValue("previousNewestPostIdDirect", m_previousNewestPostId); settings.setValue("totalPostsDirect", m_fullTimelinePostCount); break; case PumpController::ActivityTimelineRequest: settings.setValue("previousNewestPostIdActivity", m_previousNewestPostId); settings.setValue("totalPostsActivity", m_fullTimelinePostCount); break; case PumpController::FavoritesTimelineRequest: settings.setValue("previousNewestPostIdFavorites", m_previousNewestPostId); settings.setValue("totalPostsFavorites", m_fullTimelinePostCount); break; case PumpController::UserTimelineRequest: break; default: qDebug() << "Timeline destructor: timelineType is invalid!"; } settings.endGroup(); qDebug() << "TimeLine destroyed; Type:" << m_timelineType; } /* * QActions with shourtcuts, for better keyboard control * */ void TimeLine::createKeyboardActions() { // Single step m_scrollUpAction = new QAction(this); m_scrollUpAction->setShortcut(QKeySequence("Ctrl+Up")); connect(m_scrollUpAction, &QAction::triggered, this, &TimeLine::scrollUp); this->addAction(m_scrollUpAction); m_scrollDownAction = new QAction(this); m_scrollDownAction->setShortcut(QKeySequence("Ctrl+Down")); connect(m_scrollDownAction, &QAction::triggered, this, &TimeLine::scrollDown); this->addAction(m_scrollDownAction); // Pages m_scrollPageUpAction = new QAction(this); m_scrollPageUpAction->setShortcut(QKeySequence("Ctrl+PgUp")); connect(m_scrollPageUpAction, &QAction::triggered, this, &TimeLine::scrollPageUp); this->addAction(m_scrollPageUpAction); m_scrollPageDownAction = new QAction(this); m_scrollPageDownAction->setShortcut(QKeySequence("Ctrl+PgDown")); connect(m_scrollPageDownAction, &QAction::triggered, this, &TimeLine::scrollPageDown); this->addAction(m_scrollPageDownAction); // Top / Bottom m_scrollTopAction = new QAction(this); m_scrollTopAction->setShortcut(QKeySequence("Ctrl+Home")); connect(m_scrollTopAction, &QAction::triggered, this, &TimeLine::scrollToTop); this->addAction(m_scrollTopAction); m_scrollBottomAction = new QAction(this); m_scrollBottomAction->setShortcut(QKeySequence("Ctrl+End")); connect(m_scrollBottomAction, &QAction::triggered, this, &TimeLine::scrollToBottom); this->addAction(m_scrollBottomAction); // Previous/Next page in timeline m_previousPageAction = new QAction(this); m_previousPageAction->setShortcut(QKeySequence("Ctrl+Left")); connect(m_previousPageAction, &QAction::triggered, m_previousPageButton, &QAbstractButton::click); m_previousPageButton->setToolTip(m_previousPageAction->shortcut() .toString(QKeySequence::NativeText)); this->addAction(m_previousPageAction); m_nextPageAction = new QAction(this); m_nextPageAction->setShortcut(QKeySequence("Ctrl+Right")); connect(m_nextPageAction, &QAction::triggered, m_nextPageButton, &QAbstractButton::click); m_nextPageButton->setToolTip(m_nextPageAction->shortcut() // FIXME: maybe add a clearer message .toString(QKeySequence::NativeText)); this->addAction(m_nextPageAction); // It's safer to use these QActions than setting shortcuts to buttons // The latter gets messed up when built with Qt 5 and run under Plasma 5.x m_showPageSelectorAction = new QAction(this); m_showPageSelectorAction->setShortcut(QKeySequence("Ctrl+G")); connect(m_showPageSelectorAction, &QAction::triggered, m_currentPageButton, &QAbstractButton::click); this->addAction(m_showPageSelectorAction); } void TimeLine::setCustomUrl(QString url) { m_customUrl = url; } /* * Remove all widgets (Post *) from the timeline * */ void TimeLine::clearTimeLineContents(bool showMessage) { foreach (Post *oldPost, m_postsInTimeline) { m_postsLayout->removeWidget(oldPost); delete oldPost; } m_postsInTimeline.clear(); m_objectsIdList.clear(); m_postsPendingForNextTime = 0; m_postsLayout->removeWidget(m_separatorFrame); m_separatorFrame->hide(); if (showMessage) { this->showMessage(tr("Loading...")); } qApp->processEvents(); // So GUI gets updated } /* * Remove oldest posts from current page, to avoid ever-increasing memory usage. * Called after updating the timeline, only when getting newer posts on the * first page. * * At the very least, keep as many posts as were received in last update. * */ void TimeLine::removeOldPosts(int minimumToKeep) { int maxPosts = qMax(m_postsPerPage * 2, // TMP FIXME minimumToKeep); if (m_postsInTimeline.count() <= maxPosts) { // Not too many posts yet, so do nothing return; } int postCounter = 0; foreach (Post *post, m_postsInTimeline) { if (postCounter >= maxPosts) { if (!post->isNew() // Don't remove if it's unread && !post->isBeingCommented()) // or currently being commented on { m_postsLayout->removeWidget(post); m_postsInTimeline.removeOne(post); m_objectsIdList.removeAt(postCounter); delete post; } } ++postCounter; } // Update "next" link manually, based on the last post present in the page QByteArray lastPostId = m_postsInTimeline.last()->getActivityId().toLocal8Bit(); lastPostId = lastPostId.toPercentEncoding(); // Needs to be percent-encoded m_nextPageLink = m_pumpController->getFeedApiUrl(m_timelineType) + "?before=" + lastPostId; } void TimeLine::insertSeparator(int position) { m_postsLayout->insertWidget(position, m_separatorFrame); m_separatorFrame->show(); } int TimeLine::getCurrentPage() { if (m_postsPerPage == 0) { m_postsPerPage = 1; } return (m_timelineOffset / m_postsPerPage) + 1; } int TimeLine::getTotalPages() { if (m_postsPerPage == 0) { m_postsPerPage = 1; } int totalPages = qCeil(m_fullTimelinePostCount / (float)m_postsPerPage); return qMax(totalPages, 1); // 1 is the minimum } int TimeLine::getTotalPosts() { return m_fullTimelinePostCount; } /* * Update the button at the bottom of the page, indicating current "page" * */ void TimeLine::updateCurrentPageNumber() { const int currentPage = this->getCurrentPage(); const int totalPages = this->getTotalPages(); const QString currentPageString = QLocale::system().toString(currentPage); const QString totalPagesString = QLocale::system().toString(totalPages); const QString totalPostsString = QLocale::system() .toString(m_fullTimelinePostCount); m_currentPageButton->setText(QString("%1 / %2") .arg(currentPageString) .arg(totalPagesString)); m_currentPageTotalString = tr("Page %1 of %2.").arg(currentPageString) .arg(totalPagesString); m_currentPageButton->setToolTip(m_currentPageTotalString + "
              " + tr("Showing %1 posts per page.") .arg(m_postsPerPage) + "
              " + tr("%1 posts in total.") .arg(totalPostsString) + "
              " "" + tr("Click here or press Control+G to " "jump to a specific page") + ""); m_previousPageButton->setDisabled(currentPage == 1); // Disabled on 1st page m_nextPageButton->setDisabled(currentPage == totalPages); // Disabled on last page } void TimeLine::syncPostsPerPage() { if (m_timelineType == PumpController::MainTimelineRequest || m_timelineType == PumpController::UserTimelineRequest) { m_postsPerPage = m_globalObject->getPostsPerPageMain(); } else { m_postsPerPage = m_globalObject->getPostsPerPageOther(); } } void TimeLine::enablePaginationButtons() { m_firstPageButton->setEnabled(true); m_currentPageButton->setEnabled(true); this->updateCurrentPageNumber(); // Will re-enable prev/next buttons as needed } void TimeLine::disablePaginationButtons() { m_getNewPendingButton->setDisabled(true); m_firstPageButton->setDisabled(true); m_previousPageButton->setDisabled(true); m_currentPageButton->setDisabled(true); m_nextPageButton->setDisabled(true); } /* * Resize all posts in timeline * */ void TimeLine::resizePosts(QList postsToResize, bool resizeAll) { if (resizeAll) { postsToResize = m_postsInTimeline; } foreach (Post *post, postsToResize) { // Call setPostContents() and setPostHeight() // New method, disabled for 1.3.1; has some drawbacks //post->onResizeOrShow(); /* -- Old method, forcing a resize */ post->resize(post->width() - 1, post->height() - 1); /* re-enabled for 1.3.1 */ } } void TimeLine::markPostsAsRead() { foreach (Post *post, m_postsInTimeline) { // Mark post as read without informing the timeline post->setPostAsRead(false); } m_unreadPostsCount = 0; m_highlightedPostsCount = 0; emit unreadPostsCountChanged(m_timelineType, m_unreadPostsCount, m_highlightedPostsCount, m_fullTimelinePostCount); } void TimeLine::updateFuzzyTimestamps() { foreach (Post *post, m_postsInTimeline) { post->setFuzzyTimestamps(); } } bool TimeLine::commentingOnAnyPost() { foreach (Post *post, m_postsInTimeline) { if (post->isBeingCommented()) { return true; } } return false; } void TimeLine::notifyBlockedUpdates() { const QString tlName = PumpController::getFeedNameAndPath(m_timelineType).first(); m_globalObject->setStatusMessage(tr("'%1' cannot be updated " "because a comment is currently " "being composed.", "%1 = feed's name").arg(tlName)); } /* * Return list of pointers to Post() objects currently in the timeline * */ QList TimeLine::getPostsInTimeline() { return m_postsInTimeline; } QFrame *TimeLine::getSeparatorWidget() { return m_separatorFrame; } void TimeLine::showMessage(QString message) { m_infoLabel->setText("" + message + ""); m_infoLabel->show(); } /*****************************************************************************/ /*****************************************************************************/ /********************************** SLOTS ************************************/ /*****************************************************************************/ /*****************************************************************************/ void TimeLine::setTimeLineContents(QVariantList postList, QString previousLink, QString nextLink, int totalItems) { qDebug() << "TimeLine::setTimeLineContents()"; if (this->commentingOnAnyPost()) { // Extra protection, see https://gitlab.com/dianara/dianara-dev/issues/35 qDebug() << "Aborting timeline update due to comment in progress"; this->notifyBlockedUpdates(); return; } int postListSize = postList.size(); // Disable to avoid clicks to posts (which would mark them as read) if (postListSize > 0) // until fully updated { this->setDisabled(true); } // Remove all previous posts in timeline, when switching pages if (m_firstLoad || !m_wasOnFirstPage || m_isFavoritesTimeline || m_timelineOffset > 0) // FIXME: this fails in some situations { // Hide the posts while TL reloads; helps performance a lot m_postsWidget->hide(); qDebug() << "Removing previous posts from timeline"; this->clearTimeLineContents(); m_unreadPostsCount = 0; m_highlightedPostsCount = 0; // Ask mainWindow to scroll the QScrollArea containing the timeline to the top // emit scrollTo(QAbstractSlider::SliderToMinimum); ////////// TMP FIXME: don't scroll to top; make it optional m_previousPageLink = previousLink; m_nextPageLink = nextLink; qDebug() << "Prev/Next links:" << m_previousPageLink << m_nextPageLink; } else { if (!previousLink.isEmpty()) { m_previousPageLink = previousLink; // Just the previousLink; don't store nextLink, keep the old one } } int totalPostDifference = totalItems - m_fullTimelinePostCount; m_fullTimelinePostCount = totalItems; // Check how many more posts need to be received, if more than max are pending m_postsPendingForNextTime += totalPostDifference; m_postsPendingForNextTime -= postListSize; if (m_postsPendingForNextTime > 0) { if (m_firstLoad) { // The difference in pending posts is in the older pages, so doesn't count m_postsPendingForNextTime = 0; // FIXME 1.4.x: On first load, should display the "pending" number // at the bottom or at the "older" button } else { // Button at the top to fetch the pending messages, even more new stuff m_getNewPendingButton->setText(tr("%1 more posts pending for " "next update.") .arg(m_postsPendingForNextTime) + " " // 3 spaces, then an alarm clock + QString::fromUtf8("\342\217\260") + "\n" + tr("Click here to receive " "them now.")); m_getNewPendingButton->setEnabled(true); m_getNewPendingButton->show(); } } else { m_postsPendingForNextTime = 0; // In case it was less than 0 m_getNewPendingButton->hide(); } // Remove the current separator line m_postsLayout->removeWidget(m_separatorFrame); m_separatorFrame->hide(); ////////////////////////////////////// Start adding content to the timeline int newPostCount = 0; int newHighlightedPostsCount = 0; int newDirectPostsCount = 0; int newHLByFilterPostsCount = 0; int newDeletedPostsCount = 0; int newFilteredPostsCount = 0; bool allNewPostsCounted = false; int insertedPosts = 0; bool needToInsertSeparator = false; // Here we'll store the post ID for the first (newest) post in the timeline QString newestPostId; // (actually activity ID) // With it, we can know how many new posts (if any) we receive next time QList postsInsertedThisTime; // Fill timeline with new contents foreach (QVariant singlePost, postList) { if (singlePost.type() == QVariant::Map) { bool postIsNew = false; QVariantMap activityMap; // Since "Favorites" is a collection of objects, not activities, // we need to put "Favorites" posts into fake activities if (!m_isFavoritesTimeline) { // 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, m_pumpController->currentUserId(), this); // See if it's deleted QString postDeletedTime = activity->object()->getDeletedTime(); // See if we have to filter it out (or highlight it) int filtered = m_filterChecker->validateActivity(activity); // See if we hide the post if a copy is already visible in the timeline bool postIsDuplicated = false; if (m_globalObject->getHideDuplicates()) // Depending on the setting { if (m_objectsIdList.contains(activity->object()->getId())) { postIsDuplicated = true; } } if (newestPostId.isEmpty()) // only first time, for newest post { if (m_gettingNew) { newestPostId = activity->getId(); } else { newestPostId = m_previousNewestPostId; allNewPostsCounted = true; } } if (!allNewPostsCounted) { if (activity->getId() == m_previousNewestPostId) { allNewPostsCounted = true; if (newPostCount > 0) { needToInsertSeparator = true; } } else { // If post is NOT deleted or filtered, not ours, and // this is not the Favorites timeline, add it to the count if (postDeletedTime.isEmpty() && filtered != FilterChecker::FilterOut && !postIsDuplicated && activity->author()->getId() != m_pumpController->currentUserId() && activity->object()->author()->getId() != m_pumpController->currentUserId() && !m_isFavoritesTimeline && m_timelineType != PumpController::UserTimelineRequest) { ++newPostCount; // Mark current post as new postIsNew = true; } else { if (!postDeletedTime.isEmpty()) { ++newDeletedPostsCount; } else if (filtered == FilterChecker::FilterOut || postIsDuplicated) { ++newFilteredPostsCount; } } } } bool highlightedByFilter = false; if (filtered == FilterChecker::Highlight) { highlightedByFilter = true; } Post *newPost = new Post(activity, highlightedByFilter, false, // NOT standalone m_pumpController, m_globalObject, this); if (postIsNew) { newPost->setPostAsNew(); connect(newPost, &Post::postRead, this, &TimeLine::decreaseUnreadPostsCount); int highlightType = newPost->getHighlightType(); if (highlightType != Post::NoHighlight) { ++newHighlightedPostsCount; if (highlightType == Post::MessageForUserHighlight) { ++newDirectPostsCount; } if (highlightType == Post::FilterRulesHighlight) { ++newHLByFilterPostsCount; } } } if (needToInsertSeparator) // ------- { this->insertSeparator(insertedPosts); ++insertedPosts; needToInsertSeparator = false; } m_objectsIdList.insert(insertedPosts, newPost->getObjectId()); postsInsertedThisTime.append(newPost); m_postsLayout->insertWidget(insertedPosts, newPost); m_postsInTimeline.insert(insertedPosts, newPost); ++insertedPosts; // Bounce Post() signal to MainWindow connect(newPost, &Post::commentingOnPost, this, &TimeLine::commentingOnPost); // If post has been filtered out or hidden because it's a duplicate if (filtered == FilterChecker::FilterOut || postIsDuplicated) { newPost->hide(); // For now; maybe make it so that it can be clicked to show - FIXME qDebug() << "Post filtered out or hidden because it's a duplicate\n" << "Filter action:" << filtered << "(0=filter out; 1=highlight, 999=no filtering)\n" << "Duplicated:" << postIsDuplicated; } } else // singlePost.type() is not a QVariant::Map { qDebug() << "Expected a Map, got something else"; qDebug() << postList; } } // end foreach qApp->processEvents(); // pre-resize posts m_firstLoad = false; // If there were new posts, and separator not already added, add it: ----- if (newPostCount > 0 && m_separatorFrame->isHidden()) { this->insertSeparator(insertedPosts); } if (!newestPostId.isEmpty()) { m_previousNewestPostId = newestPostId; } m_unreadPostsCount += newPostCount; m_highlightedPostsCount += newHighlightedPostsCount; qDebug() << "-----------\nNew posts:" << newPostCount << "\nActual total new from previous update:" << totalPostDifference << "\nNewest post ID:" << m_previousNewestPostId << "\nNew highlighted:" << newHighlightedPostsCount << "\n------ New direct:" << newDirectPostsCount << "\n------ New HL by filter:" << newHLByFilterPostsCount << "\nNew deleted: " << newDeletedPostsCount << "\nNew filtered out: " << newFilteredPostsCount << "\nTotal posts:" << m_fullTimelinePostCount << "\nTotal currently loaded posts:" << m_postsInTimeline.size(); if (postListSize > 0) { // Resize the posts, but only the ones added in this update this->resizePosts(postsInsertedThisTime); } if (m_gettingNew) { emit timelineRendered(m_timelineType, newPostCount, newHighlightedPostsCount, newDirectPostsCount, newHLByFilterPostsCount, newDeletedPostsCount, newFilteredPostsCount, m_postsPendingForNextTime, QString()); // Current page doesn't apply emit unreadPostsCountChanged(m_timelineType, m_unreadPostsCount, m_highlightedPostsCount, m_fullTimelinePostCount); // Clean up, keeping at least the posts that were just received if (postListSize > 0) // but only if there was _something_ { this->removeOldPosts(postListSize); } } else { this->updateCurrentPageNumber(); emit timelineRendered(m_timelineType, postListSize, -1, // Highlighted counts (direct and by filter) -1, -1, // are irrelevant in this case newDeletedPostsCount, newFilteredPostsCount, -1, // And so is the "pending for next time" m_currentPageTotalString); emit unreadPostsCountChanged(m_timelineType, 0, 0, m_fullTimelinePostCount); } if (m_postsInTimeline.length() == 0) { this->showMessage(tr("There are no posts")); m_postsWidget->hide(); } else { m_infoLabel->hide(); m_postsWidget->show(); // Show posts again } // Enable timeline again, since everything is added and drawn this->setEnabled(true); this->enablePaginationButtons(); qDebug() << "setTimeLineContents() /END"; } void TimeLine::onUpdateFailed(int requestType) { if (requestType == m_timelineType) { this->setEnabled(true); // Just in case m_timelineOffset = m_oldTimelineOffset; this->enablePaginationButtons(); } } /* * Update data in all currently-visible posts matching the object ID sent * by the minor feed * */ void TimeLine::updatePostsFromMinorFeed(ASObject *object) { /* FIXME: This should handle cases where a comment might be visible as a * post in the timeline (shared) _and_ be a comment in a visible post * * Also, something could be a note, therefore visible in the timeline, * but also be in reply to something else. * */ if (object->getInReplyToId().isEmpty()) // Parent object { foreach (Post *post, m_postsInTimeline) { if (post->getObjectId() == object->getId()) { post->updateDataFromObject(object); } } } else // Reply to something { foreach (Post *post, m_postsInTimeline) { if (post->getObjectId() == object->getInReplyToId()) { post->updateCommentFromObject(object); } } } } void TimeLine::addLikesFromMinorFeed(QString objectId, QString objectType, QString actorId, QString actorName, QString actorUrl) { // FIXME 1.4.x: handle updating likes in comments foreach (Post *post, m_postsInTimeline) { if (post->getObjectId() == objectId) { post->appendLike(actorId, actorName, actorUrl); } } } void TimeLine::removeLikesFromMinorFeed(QString objectId, QString objectType, QString actorId) { // FIXME 1.4.x: handle updating likes in comments foreach (Post *post, m_postsInTimeline) { if (post->getObjectId() == objectId) { post->removeLike(actorId); } } } /* * Add one single comment read from a minor feed, to the * corresponding parent post in this timeline * */ void TimeLine::addReplyFromMinorFeed(ASObject *object) { QString parentPostId = object->getInReplyToId(); foreach (Post *post, m_postsInTimeline) { if (post->getObjectId() == parentPostId) { post->appendComment(object); } } } void TimeLine::setPostsDeletedFromMinorFeed(ASObject *object) { foreach (Post *post, m_postsInTimeline) { if (post->getObjectId() == object->getId()) { post->setPostAsRead(true); // Just in case, and notifying timeline post->setPostDeleted(object->getDeletedOnString()); } } // If the object has a parent, find it in the comments if (!object->getInReplyToId().isEmpty()) { foreach (Post *post, m_postsInTimeline) { if (post->getObjectId() == object->getInReplyToId()) { post->setCommentDeletedFromObject(object); } } } } /* * Add the full list of likes to a post * */ void TimeLine::setLikesInPost(QVariantList likesList, QString originatingPostUrl) { // FIXME: should get proper post ID from the JSON, // instead of using and splitting the replyUrl const QString cleanUrl = originatingPostUrl.split("?").first(); //qDebug() << "Originating post URL:" << cleanUrl; // Look for the originating Post() object foreach (Post *post, m_postsInTimeline) { if (post->likesUrl() == cleanUrl) { qDebug() << "Found originating Post; setting likes on it..."; post->setLikes(likesList); /* * Don't break, so likes get set in other * visible copies of the post too */ } } } /* * Add the full list of comments to a post * */ void TimeLine::setCommentsInPost(QVariantList commentsList, QString originatingPostUrl) { // FIXME: should get proper post ID from the JSON, // instead of using and splitting the replyUrl const QString cleanUrl = originatingPostUrl.split("?").first(); //qDebug() << "Originating post URL:" << cleanUrl; // Look for the originating Post() object foreach (Post *post, m_postsInTimeline) { if (post->commentsUrl() == cleanUrl) { qDebug() << "Found originating Post; setting comments on it..."; post->setComments(commentsList); /* 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()"; if (this->commentingOnAnyPost()) { // Update is blocked because a post is being commented on this->notifyBlockedUpdates(); return; } this->syncPostsPerPage(); m_gettingNew = true; if (m_timelineOffset == 0) // On page 1 { m_wasOnFirstPage = true; } else { m_wasOnFirstPage = false; m_previousPageLink.clear(); // Full reload of newest stuff m_oldTimelineOffset = m_timelineOffset; m_timelineOffset = 0; this->setDisabled(true); // Disable soon, to avoid wrong clicks } // Disable pagination buttons to avoid double-clicking problems, in case // the whole timeline isn't disabled (whenever we're in the first page) this->disablePaginationButtons(); // The ones that make sense will be re-enabled later /* * If this is the favorites timeline, previousPageLink will be empty, * which means the first posts at offset 0 will be loaded anyway * */ m_pumpController->getFeed(m_timelineType, m_postsPerPage, m_previousPageLink); } void TimeLine::goToPreviousPage() { qDebug() << "TimeLine::goToPreviousPage()"; if (this->commentingOnAnyPost()) { this->notifyBlockedUpdates(); return; } this->syncPostsPerPage(); m_gettingNew = false; this->setDisabled(true); // Disable soon, to avoid wrong clicks m_oldTimelineOffset = m_timelineOffset; m_timelineOffset -= m_postsPerPage; if (m_timelineOffset < 0) { m_timelineOffset = 0; } if (!m_isFavoritesTimeline) // Not favorites, use PreviousLink { m_pumpController->getFeed(m_timelineType, m_postsPerPage, m_previousPageLink); } else { m_pumpController->getFeed(m_timelineType, m_postsPerPage, QString(), m_timelineOffset); } } void TimeLine::goToNextPage() { qDebug() << "TimeLine::goToNextPage()"; if (this->commentingOnAnyPost()) { this->notifyBlockedUpdates(); return; } this->syncPostsPerPage(); m_gettingNew = false; this->setDisabled(true); // Disable soon, to avoid wrong clicks m_oldTimelineOffset = m_timelineOffset; m_timelineOffset += m_postsPerPage; if (!m_isFavoritesTimeline) // Not favorites, use NextLink { m_pumpController->getFeed(m_timelineType, m_postsPerPage, m_nextPageLink); } else // Use offset { m_pumpController->getFeed(m_timelineType, m_postsPerPage, QString(), m_timelineOffset); } } void TimeLine::goToSpecificPage(int pageNumber) { qDebug() << "TimeLine::goToSpecificPage(): " << pageNumber; if (this->commentingOnAnyPost()) { this->notifyBlockedUpdates(); return; } this->syncPostsPerPage(); m_gettingNew = false; // FIXME: should be true if page=1 this->setDisabled(true); // Disable soon, to avoid wrong clicks m_oldTimelineOffset = m_timelineOffset; m_timelineOffset = (pageNumber - 1) * m_postsPerPage; /*************************************************************************** * Workaround for Pump.io core bug: * * Ensure that timelineOffset is NOT greater * than (fullTimelinePostCount - postsPerPage) * * Otherwise, when requesting the last page, the server might return * ALL posts, even beyond API limits, instead of the last (oldest) few * posts. * * https://github.com/pump-io/pump.io/issues/1087 * */ if (m_timelineType == PumpController::UserTimelineRequest) { int maxTimelineOffset = m_fullTimelinePostCount - m_postsPerPage; m_timelineOffset = qMin(m_timelineOffset, maxTimelineOffset); } m_pumpController->getFeed(m_timelineType, m_postsPerPage, m_customUrl, // No prev/next links, so use possible customUrl, or empty m_timelineOffset); // And use offset instead } /* * Get newest posts that were pending, explicitly from the "get pending" button. * * Needed to set focus on another widget first. * */ void TimeLine::getNewPending() { // Avoid annoying focus changes when 'get pending' button is disabled m_postsWidget->setFocus(); this->goToFirstPage(); } void TimeLine::showPageSelector() { m_pageSelector->showForPage(this->getCurrentPage(), this->getTotalPages()); } void TimeLine::scrollUp() { emit scrollTo(QAbstractSlider::SliderSingleStepSub); } void TimeLine::scrollDown() { emit scrollTo(QAbstractSlider::SliderSingleStepAdd); } void TimeLine::scrollPageUp() { emit scrollTo(QAbstractSlider::SliderPageStepSub); } void TimeLine::scrollPageDown() { emit scrollTo(QAbstractSlider::SliderPageStepAdd); } void TimeLine::scrollToTop() { emit scrollTo(QAbstractSlider::SliderToMinimum); } void TimeLine::scrollToBottom() { emit scrollTo(QAbstractSlider::SliderToMaximum); } /* * Decrease internal counter of unread posts (by 1), and inform * the parent window, so it can update its tab titles * */ void TimeLine::decreaseUnreadPostsCount(bool wasHighlighted) { --m_unreadPostsCount; if (wasHighlighted) { --m_highlightedPostsCount; } emit unreadPostsCountChanged(m_timelineType, m_unreadPostsCount, m_highlightedPostsCount, m_fullTimelinePostCount); } void TimeLine::updateAvatarFollowStates() { foreach (Post *post, m_postsInTimeline) { post->syncAvatarFollowState(); } // Since this happens when the Following list changes, sort its model m_globalObject->sortNickCompletionModel(); } dianara-v1.4.1/src/siteuserslist.h0000644000175000017500000000344213206634250015272 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 SITEUSERSLIST_H #define SITEUSERSLIST_H #include #include #include #include #include #include #include "pumpcontroller.h" #include "globalobject.h" #include "contactlist.h" class SiteUsersList : public QWidget { Q_OBJECT public: explicit SiteUsersList(PumpController *pumpController, GlobalObject *globalObject, QWidget *parent = 0); ~SiteUsersList(); signals: public slots: void getList(); void setListContents(QVariantList userList, int totalUsers); void closeList(); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_middleLayout; QLabel *m_explanationLabel; QPushButton *m_getListButton; QLabel *m_infoLinksLabel; QLabel *m_serverInfoLabel; QPushButton *m_closeListButton; ContactList *m_userList; PumpController *m_pumpController; }; #endif // SITEUSERSLIST_H dianara-v1.4.1/src/commenterblock.cpp0000644000175000017500000005315313212320503015701 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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, GlobalObject *globalObject, QString parentId, QString parentAuthorId, bool parentStandalone, QWidget *parent) : QWidget(parent) { m_pumpController = pumpController; m_globalObject = globalObject; m_parentPostId = parentId; m_parentPostAuthorId = parentAuthorId; m_parentPostStandalone = parentStandalone; m_editingMode = false; m_currentCommentCount = 0; m_allCommentsShown = false; m_reloadCommentsString = QString::fromUtf8("\342\206\273") // Reload symbol + "   " + tr("Reload comments"); m_showAllCommentsLinkLabel = new QLabel("" + m_reloadCommentsString + "", this); m_showAllCommentsLinkLabel->setContextMenuPolicy(Qt::NoContextMenu); QFont showAllFont; showAllFont.setPointSize(showAllFont.pointSize() - 3); m_showAllCommentsLinkLabel->setFont(showAllFont); connect(m_showAllCommentsLinkLabel, &QLabel::linkActivated, this, &CommenterBlock::requestAllComments); this->setContentsMargins(0, 0, 0, 0); m_commentsLayout = new QVBoxLayout(); m_commentsLayout->setContentsMargins(0, 0, 0, 0); m_commentsLayout->setSpacing(1); m_commentsWidget = new QWidget(this); m_commentsWidget->setContentsMargins(0, 0, 0, 0); QSizePolicy sizePolicy; sizePolicy.setHeightForWidth(false); sizePolicy.setWidthForHeight(false); sizePolicy.setHorizontalPolicy(QSizePolicy::Minimum); sizePolicy.setVerticalPolicy(QSizePolicy::Maximum); m_commentsWidget->setSizePolicy(sizePolicy); m_commentsWidget->setLayout(m_commentsLayout); m_commentsScrollArea = new QScrollArea(this); m_commentsScrollArea->setContentsMargins(0, 0, 0, 0); m_commentsScrollArea->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); m_commentsScrollArea->setWidget(m_commentsWidget); m_commentsScrollArea->setWidgetResizable(true); m_getAllCommentsTimer = new QTimer(this); m_getAllCommentsTimer->setSingleShot(true); connect(m_getAllCommentsTimer, &QTimer::timeout, this, &CommenterBlock::requestAllComments); // Hide these until setComments() is called, if there are any comments m_showAllCommentsLinkLabel->hide(); m_commentsScrollArea->hide(); m_scrollToBottomTimer = new QTimer(this); m_scrollToBottomTimer->setSingleShot(true); connect(m_scrollToBottomTimer, &QTimer::timeout, this, &CommenterBlock::scrollCommentsToBottom); m_commentComposer = new Composer(m_globalObject, false, // forPublisher = false this); m_commentComposer->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_commentComposer->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_commentComposer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); connect(m_commentComposer, &Composer::editingFinished, this, &CommenterBlock::sendComment); connect(m_commentComposer, &Composer::editingCancelled, this, &CommenterBlock::setMinimumMode); // Formatting/Tools button exported from Composer m_toolsButton = m_commentComposer->getToolsButton(); // Info label about sending status m_statusInfoLabel = new QLabel(this); m_statusInfoLabel->setAlignment(Qt::AlignCenter); m_statusInfoLabel->setWordWrap(true); showAllFont.setPointSize(showAllFont.pointSize() + 1); m_statusInfoLabel->setFont(showAllFont); m_commentButton = new QPushButton(QIcon::fromTheme("mail-send", QIcon(":/images/button-post.png")), tr("Comment", "Infinitive verb"), this); m_commentButton->setToolTip(QStringLiteral("") + tr("You can press Control+Enter to send " "the comment with the keyboard")); connect(m_commentButton, &QAbstractButton::clicked, this, &CommenterBlock::sendComment); m_cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("Cancel"), this); m_cancelButton->setToolTip(QStringLiteral("") + tr("Press ESC to cancel the comment " "if there is no text")); connect(m_cancelButton, &QAbstractButton::clicked, m_commentComposer, &Composer::cancelPost); m_bottomLayout = new QGridLayout(); m_bottomLayout->setContentsMargins(0, 0, 0, 0); m_bottomLayout->setSpacing(1); m_bottomLayout->addWidget(m_commentComposer, 0, 0, 8, 3); m_bottomLayout->addWidget(m_toolsButton, 0, 3, 1, 1); m_bottomLayout->addWidget(m_statusInfoLabel, 1, 3, 5, 1, Qt::AlignCenter); m_bottomLayout->addWidget(m_commentButton, 6, 3, 1, 1); m_bottomLayout->addWidget(m_cancelButton, 7, 3, 1, 1); m_mainLayout = new QVBoxLayout(); m_mainLayout->setContentsMargins(0, 0, 0, 0); m_mainLayout->setSpacing(1); m_mainLayout->addWidget(m_showAllCommentsLinkLabel, 0, Qt::AlignRight | Qt::AlignTop); m_mainLayout->addWidget(m_commentsScrollArea, 0); m_mainLayout->addLayout(m_bottomLayout, 0); this->setLayout(m_mainLayout); this->setMinimumMode(); qDebug() << "Commenter created"; } CommenterBlock::~CommenterBlock() { qDebug() << "Commenter destroyed"; } void CommenterBlock::clearComments() { foreach (Comment *comment, m_commentsInBlock) { comment->deleteLater(); } m_commentsInBlock.clear(); m_currentCommentCount = 0; } void CommenterBlock::setComments(QVariantList commentsList, int commentCount) { m_reloadErrorString.clear(); // No error to show! disconnect(m_pumpController, &PumpController::commentsNotReceived, this, &CommenterBlock::onCommentsNotReceived); if (commentCount > 0) { // If some comments are actually included, clear first if (commentsList.length() > 0) { this->clearComments(); } m_currentCommentCount = commentCount; if (m_currentCommentCount > commentsList.size()) { m_allCommentsShown = false; } else { m_allCommentsShown = true; } this->updateShowAllLink(); foreach (QVariant commentVariant, commentsList) { ASObject *commentObject = new ASObject(commentVariant.toMap(), this); this->appendComment(commentObject); } m_commentsScrollArea->show(); // Move scrollbar to the bottom this->scrollCommentsToBottom(); // First, try // Then, some msecs 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 m_scrollToBottomTimer->start(500); } else { // For cases where 0 comments were received, after specifically reloading this->updateShowAllLink(); } } /* * Add a single comment to the layout * */ void CommenterBlock::appendComment(ASObject *object, bool justOne) { foreach (Comment *previousComment, m_commentsInBlock) { if (previousComment->getObjectId() == object->getId()) { return; // Comment is already present, so abort } } Comment *comment = new Comment(m_pumpController, m_globalObject, object, this); connect(comment, &Comment::commentQuoteRequested, this, &CommenterBlock::quoteComment); connect(comment, &Comment::commentEditRequested, this, &CommenterBlock::editComment); // Highlight if the comment was made by the author of the parent post if (m_globalObject->getPostHLAuthorComments()) // if configured to do so { if (object->author()->getId() == m_parentPostAuthorId) { comment->setHint(m_globalObject->getColor(4)); // HL filtering color } } // Or highlight if this is our own comment, also optional if (m_globalObject->getPostHLOwnComments()) { if (object->author()->getId() == m_pumpController->currentUserId()) { comment->setHint(m_globalObject->getColor(3)); // Your own posts color } } // Add the comment at the end if it's just one (via MinorFeed), or at // the top when it's part of a full list, since those lists come in reverse m_commentsInBlock.append(comment); // First, keep track of it if (justOne) { m_commentsLayout->addWidget(comment); ++m_currentCommentCount; // Check if we need to show "reload comments" or "show all X comments" if (m_currentCommentCount == m_commentsInBlock.size()) { m_allCommentsShown = true; } this->updateShowAllLink(); // Showing this is needed if there were 0 comments before m_commentsScrollArea->show(); m_scrollToBottomTimer->start(500); } else // Called from setComments() { m_commentsLayout->insertWidget(0, comment); } } void CommenterBlock::updateCommentFromObject(ASObject *object) { foreach (Comment *comment, m_commentsInBlock) { if (comment->getObjectId() == object->getId()) { comment->updateDataFromObject(object); } } // FIXME: this needs some checking... m_commentsWidget->setMinimumHeight(10); m_scrollToBottomTimer->start(500); this->adjustCommentsHeight(); this->adjustCommentArea(); } void CommenterBlock::setCommentDeletedFromObject(ASObject *object) { foreach (Comment *comment, m_commentsInBlock) { if (comment->getObjectId() == object->getId()) { comment->setCommentDeleted(object->getDeletedOnString()); } } // FIXME: this needs some checking... and is copied from previous function m_commentsWidget->setMinimumHeight(10); m_scrollToBottomTimer->start(500); this->adjustCommentsHeight(); this->adjustCommentArea(); } void CommenterBlock::updateShowAllLink() { QString showAllString; if (m_allCommentsShown) { showAllString = m_reloadCommentsString; } else { if (m_currentCommentCount == 0) { showAllString = QString::fromUtf8("\342\206\273") // Reload symbol + "   " // Another option, magnifying glass: 🔎 + tr("Check for comments"); } else { showAllString = tr("Show all %1 comments") .arg(m_currentCommentCount); } } // Small trick to avoid a bug where the link stops having appropriate mouse pointer m_showAllCommentsLinkLabel->clear(); m_showAllCommentsLinkLabel->hide(); m_showAllCommentsLinkLabel->setText(m_reloadErrorString + "" + showAllString + ""); m_showAllCommentsLinkLabel->show(); } /* * Disable the "check for comments" link when there's no valid comment URL, * by setting a clear message instead. Simply disabling the widget wouldn't * make the link look disabled. * */ void CommenterBlock::disableShowAllLink() { m_showAllCommentsLinkLabel->setText(tr("Comments are not available")); } void CommenterBlock::updateFuzzyTimestamps() { foreach (Comment *comment, m_commentsInBlock) { comment->setFuzzyTimestamps(); } } void CommenterBlock::updateAvatarFollowStates() { foreach (Comment *comment, m_commentsInBlock) { comment->syncAvatarFollowState(); } } void CommenterBlock::adjustCommentsWidth() { //m_commentsWidget->setMaximumWidth(m_commentsScrollArea->viewport()->width() - 2); } void CommenterBlock::adjustCommentsHeight() { if (m_commentsScrollArea->isVisible()) { m_commentsScrollArea->hide(); // m_commentsWidget->ensurePolished(); // Disabled for 1.3.1; using classic resize stuff m_commentsScrollArea->show(); } } void CommenterBlock::adjustCommentArea() { m_commentsWidget->ensurePolished(); // So .height() is valid int commentsWidgetHeight = qMax(m_commentsWidget->height(), 32); // 32 px min! (size of avatar) int goodScrollAreaHeight = qMin(commentsWidgetHeight, m_globalObject->getTimelineHeight()); // Not bigger than the window goodScrollAreaHeight += m_commentsScrollArea->frameWidth() * 2; // Account for the frame if (!m_parentPostStandalone) { // Minimum is set only when the post is not open as standalone m_commentsScrollArea->setMinimumHeight(goodScrollAreaHeight); } m_commentsScrollArea->setMaximumHeight(goodScrollAreaHeight); } void CommenterBlock::redrawComments() { foreach (Comment *comment, m_commentsInBlock) { comment->setCommentContents(); } } bool CommenterBlock::isFullMode() { return m_fullMode; } int CommenterBlock::getCommentCount() { return m_currentCommentCount; } Composer *CommenterBlock::getComposer() { return m_commentComposer; } /*****************************************************************************/ /*********************************** SLOTS ***********************************/ /*****************************************************************************/ void CommenterBlock::setMinimumMode() { m_commentComposer->hide(); m_toolsButton->hide(); m_statusInfoLabel->clear(); m_statusInfoLabel->hide(); m_commentButton->hide(); m_cancelButton->hide(); // Clear formatting options like bold or italic m_commentComposer->setCurrentCharFormat(QTextCharFormat()); // Clear "editing mode", restore stuff if (m_editingMode) { m_editingMode = false; m_editingCommentId.clear(); m_commentButton->setText(tr("Comment", // Button text back to "Comment" as usual "Infinitive verb")); // With exact same comment, so it's 1 entry } m_fullMode = false; } void CommenterBlock::setFullMode(QString initialText) { m_commentComposer->show(); m_toolsButton->show(); m_statusInfoLabel->show(); m_commentButton->show(); m_cancelButton->show(); m_commentComposer->setFocus(); if (!initialText.isEmpty()) { m_commentComposer->append(initialText); // Alternative way, would insert anywhere, has other problems // m_commentComposer->insertHtml(initialText); // Delete extra newline after the quote m_commentComposer->moveCursor(QTextCursor::End); // Otherwise could delete other characters m_commentComposer->textCursor().deletePreviousChar(); } m_fullMode = true; } void CommenterBlock::quoteComment(QString content) { this->setFullMode(content); // FIXME: connect to setFullMode(QString) directly? } void CommenterBlock::editComment(QString id, QString content) { // Avoid destroying a comment currently being composed! if (m_editingMode || m_fullMode) { QMessageBox::warning(this, tr("Error: Already composing"), tr("You can't edit a comment at this time, " "because another comment is already " "being composed.")); return; } m_editingMode = true; m_editingCommentId = id; setFullMode(); m_commentComposer->setHtml(content); m_commentComposer->moveCursor(QTextCursor::End); m_commentButton->setText("Update"); m_statusInfoLabel->setText(tr("Editing comment")); } void CommenterBlock::requestAllComments() { // Show feedback in place, regardless of statusbar message m_showAllCommentsLinkLabel->setText("∼  " // ~ + tr("Loading comments...")); connect(m_pumpController, &PumpController::commentsNotReceived, this, &CommenterBlock::onCommentsNotReceived); emit allCommentsRequested(); } /* * Called when commentPosted() is emmited by PumpController, * which means comment posted (or updated) successfully. * */ void CommenterBlock::onPostingCommentOk(QString postId) { if (postId != m_parentPostId) { // Not related to this post... return; } // Clear info message m_statusInfoLabel->clear(); // Comment was added successfully, so we can re-enable things this->setEnabled(true); // Erase the text from the comment box... m_commentComposer->erase(); // Show the comment area, even if empty, in case comments are not reloaded ok m_showAllCommentsLinkLabel->show(); m_commentsScrollArea->show(); // and since we're done posting the comment, hide this setMinimumMode(); disconnect(m_pumpController, &PumpController::commentPosted, this, &CommenterBlock::onPostingCommentOk); disconnect(m_pumpController, &PumpController::commentPostingFailed, this, &CommenterBlock::onPostingCommentFailed); m_getAllCommentsTimer->start(3000); // Request comments after a delay } /* * Executed when commentPostingFailed() signal is received from PumpController * */ void CommenterBlock::onPostingCommentFailed(QString postId) { if (postId != m_parentPostId) { // Not related to this post... return; } qDebug() << "Posting the comment failed, re-enabling Commenter"; // Alert about the error m_statusInfoLabel->setText(tr("Posting comment failed.\n\nTry again.")); // Re-enable things, so user can try again this->setEnabled(true); m_commentComposer->setFocus(); disconnect(m_pumpController, &PumpController::commentPostingFailed, this, &CommenterBlock::onPostingCommentFailed); disconnect(m_pumpController, &PumpController::commentPosted, this, &CommenterBlock::onPostingCommentOk); } void CommenterBlock::onCommentsNotReceived(QString id) { if (id == m_parentPostId) { m_reloadErrorString = tr("An error occurred") + " — "; this->updateShowAllLink(); } } void CommenterBlock::sendComment() { qDebug() << "Commenter character count:" << m_commentComposer->textCursor().document()->characterCount(); // If there's some text in the comment, send it if (m_commentComposer->textCursor().document()->characterCount() > 1) { connect(m_pumpController, &PumpController::commentPosted, this, &CommenterBlock::onPostingCommentOk); connect(m_pumpController, &PumpController::commentPostingFailed, this, &CommenterBlock::onPostingCommentFailed); if (!m_editingMode) { m_statusInfoLabel->setText(tr("Sending comment...")); emit commentSent(m_commentComposer->toHtml()); } else { m_statusInfoLabel->setText(tr("Updating comment...")); emit commentUpdated(m_editingCommentId, m_commentComposer->toHtml()); } this->setDisabled(true); } else { m_statusInfoLabel->setText(tr("Comment is empty.")); qDebug() << "Can't post, comment is empty"; } } /* * Called by the QTimer * */ void CommenterBlock::scrollCommentsToBottom() { this->adjustCommentsWidth(); this->adjustCommentArea(); m_commentsScrollArea->verticalScrollBar()->triggerAction(QScrollBar::SliderToMaximum); } /*****************************************************************************/ /********************************* PROTECTED *********************************/ /*****************************************************************************/ void CommenterBlock::resizeEvent(QResizeEvent *event) { //qDebug() << "CommenterBlock::resizeEvent()" // << event->oldSize() << ">" << event->size(); this->adjustCommentsWidth(); this->redrawComments(); //m_commentsWidget->adjustSize(); //m_commentsScrollArea->adjustSize(); this->adjustCommentArea(); event->accept(); } dianara-v1.4.1/src/bannernotification.h0000664000175000017500000000301713202671127016224 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 BANNERNOTIFICATION_H #define BANNERNOTIFICATION_H #include #include #include #include #include class BannerNotification : public QWidget { Q_OBJECT public: explicit BannerNotification(QWidget *parent = 0); ~BannerNotification(); signals: void updateRequested(); void bannerCancelled(); public slots: void onOk(); void onCancel(); private: QHBoxLayout *m_mainLayout; QWidget *m_containerWidget; QHBoxLayout *m_containerLayout; QLabel *m_iconLabel; QLabel *m_descriptionLabel; QPushButton *m_okButton; QPushButton *m_cancelButton; }; #endif // BANNERNOTIFICATION_H dianara-v1.4.1/src/comment.h0000644000175000017500000000630413211035261014003 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "globalobject.h" #include "mischelpers.h" #include "timestamp.h" #include "asobject.h" #include "avatarbutton.h" #include "hclabel.h" class Comment : public QFrame { Q_OBJECT public: explicit Comment(PumpController *pumpController, GlobalObject *globalObject, ASObject *commentObject, QWidget *parent = 0); ~Comment(); void updateDataFromObject(ASObject *object); void fixLikeLabelText(); void setLikesCount(int count, QVariantList namesVariantList); void setFuzzyTimestamps(); void syncAvatarFollowState(); void setCommentContents(); void onResize(); void getPendingImages(); QString getObjectId(); void setHint(QString color=""); void setCommentDeleted(QString deletedTime); signals: void commentQuoteRequested(QString content); void commentEditRequested(QString id, QString content); public slots: void likeComment(QString clickedLink); void saveCommentSelectedText(); void quoteComment(); void editComment(); void deleteComment(); void showUrlInfo(QString url); void redrawImages(QString imageUrl); protected: virtual void leaveEvent(QEvent *event); virtual void resizeEvent(QResizeEvent *event); private: QHBoxLayout *m_mainLayout; QVBoxLayout *m_leftLayout; QVBoxLayout *m_rightLayout; QHBoxLayout *m_rightTopLayout; AvatarButton *m_avatarButton; QLabel *m_fullNameLabel; HClabel *m_timestampLabel; QLabel *m_contentLabel; HClabel *m_likesCountLabel; QWidget *m_hintWidget; QLabel *m_likeLabel; QLabel *m_quoteLabel; QLabel *m_editLabel; QLabel *m_deleteLabel; QString m_commentId; QString m_objectType; QString m_commentAuthorId; bool m_commentIsLiked; bool m_commentIsDeleted; bool m_commentHasImages; QString m_createdAt; QString m_updatedAt; QString m_commentOriginalText; QStringList m_pendingImagesList; QString m_commentSelectedText; PumpController *m_pumpController; GlobalObject *m_globalObject; }; #endif // COMMENT_H dianara-v1.4.1/src/colorpicker.h0000664000175000017500000000310413202670041014653 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 COLORPICKER_H #define COLORPICKER_H #include #include #include #include #include #include #include class ColorPicker : public QWidget { Q_OBJECT public: explicit ColorPicker(QString description, QString initialColorString, QWidget *parent = 0); ~ColorPicker(); void setButtonColor(QColor color); QString getCurrentColor(); signals: public slots: void changeColor(); private: QHBoxLayout *m_layout; QLabel *m_descriptionLabel; QCheckBox *m_checkBox; QPixmap m_buttonPixmap; QPushButton *m_button; QColor m_currentColor; }; #endif // COLORPICKER_H dianara-v1.4.1/src/firstrunwizard.cpp0000644000175000017500000001715513206637103016006 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "firstrunwizard.h" FirstRunWizard::FirstRunWizard(AccountDialog *accountDialog, ProfileEditor *profileEditor, ConfigDialog *configDialog, HelpWidget *helpWidget, GlobalObject *globalObject, QWidget *parent) : QWidget(parent) { this->setWindowTitle(tr("Welcome Wizard") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("tools-wizard")); this->setWindowFlags(Qt::Window); this->setMinimumSize(420, 460); this->resize(520, 620); m_accountDialog = accountDialog; m_profileEditor = profileEditor; m_configDialog = configDialog; m_helpWidget = helpWidget; m_explanationLabel = new QLabel("" + tr("Welcome to Dianara!") + "" + "

              " + tr("This wizard will help you " "get started.") + " " + tr("You can access this window again " "at any time from the Help menu.") + "

              " + tr("The first step is setting up " "your account, by using the " "following button:"), this); m_explanationLabel->setWordWrap(true); m_explanationLabel->setAlignment(Qt::AlignTop); // Config account m_configAccountButton = new QPushButton(QIcon::fromTheme("dialog-password", QIcon(":/images/button-password.png")), tr("Configure your &account"), this); connect(m_configAccountButton, &QAbstractButton::clicked, m_accountDialog, &QWidget::show); // Edit profile m_editProfileLabel = new QLabel(tr("Once you have configured your " "account, it's recommended that you " "edit your profile and add an avatar " "and some other information, if you " "haven't done so already."), this); m_editProfileLabel->setWordWrap(true); m_editProfileLabel->setAlignment(Qt::AlignTop); m_editProfileButton = new QPushButton(QIcon::fromTheme("user-properties", QIcon(":/images/no-avatar.png")), tr("&Edit your profile"), this); //m_editProfileButton->setDisabled(true); // TMP FIXME connect(m_editProfileButton, &QAbstractButton::clicked, m_profileEditor, &QWidget::show); // Public posting m_publicPostsLabel = new QLabel(tr("By default, Dianara will post only " "to your followers, but it's " "recommended that you post to " "Public, at least sometimes."), this); m_publicPostsLabel->setWordWrap(true); m_publicPostsLabel->setAlignment(Qt::AlignTop); m_publicPostsCheckbox = new QCheckBox(tr("Post to &Public by default"), this); m_publicPostsCheckbox->setChecked(globalObject->getPublicPostsByDefault()); // Access to program help m_helpButton = new QPushButton(QIcon::fromTheme("system-help", QIcon(":/images/menu-find.png")), tr("Open general program &help window"), this); connect(m_helpButton, &QAbstractButton::clicked, m_helpWidget, &QWidget::show); // Bottom m_showAgainCheckbox = new QCheckBox(tr("&Show this again next time " "Dianara starts"), this); m_closeButton = new QPushButton(QIcon::fromTheme("window-close", QIcon(":/images/button-close.png")), tr("&Close"), this); connect(m_closeButton, &QAbstractButton::clicked, this, &QWidget::close); // Layout m_bottomLayout = new QHBoxLayout(); m_bottomLayout->addWidget(m_showAgainCheckbox); m_bottomLayout->addWidget(m_closeButton, 0, Qt::AlignRight); m_mainLayout = new QVBoxLayout(); m_mainLayout->addWidget(m_explanationLabel); m_mainLayout->addSpacing(12); m_mainLayout->addWidget(m_configAccountButton, 0, Qt::AlignCenter); m_mainLayout->addStretch(1); m_mainLayout->addWidget(m_editProfileLabel); m_mainLayout->addSpacing(12); m_mainLayout->addWidget(m_editProfileButton, 0, Qt::AlignCenter); m_mainLayout->addStretch(1); m_mainLayout->addWidget(m_publicPostsLabel); m_mainLayout->addSpacing(12); m_mainLayout->addWidget(m_publicPostsCheckbox, 0, Qt::AlignCenter); m_mainLayout->addStretch(1); m_mainLayout->addSpacing(16); m_mainLayout->addWidget(m_helpButton, 0, Qt::AlignCenter); m_mainLayout->addStretch(1); m_mainLayout->addSpacing(16); m_mainLayout->addLayout(m_bottomLayout); this->setLayout(m_mainLayout); QSettings settings; m_showAgainCheckbox->setChecked(settings.value("FirstRunWizard/showWizard", true).toBool()); qDebug() << "FirstRunWizard created"; } FirstRunWizard::~FirstRunWizard() { qDebug() << "FirstRunWizard destroyed"; } /*****************************************************************************/ /*********************************** SLOTS ***********************************/ /*****************************************************************************/ /*****************************************************************************/ /********************************* PROTECTED *********************************/ /*****************************************************************************/ void FirstRunWizard::closeEvent(QCloseEvent *event) { QSettings settings; settings.setValue("FirstRunWizard/showWizard", m_showAgainCheckbox->isChecked()); settings.sync(); // Sync public posting option m_configDialog->setPublicPosts(m_publicPostsCheckbox->isChecked()); this->hide(); // close() would kill the program if mainWindow was hidden this->deleteLater(); event->ignore(); } dianara-v1.4.1/src/groupsmanager.cpp0000664000175000017500000001222013210564061015546 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "groupsmanager.h" GroupsManager::GroupsManager(PumpController *pumpController, QWidget *parent) : QWidget(parent) { m_pumpController = pumpController; this->setWindowTitle("GROUPS MANAGER" " - Dianara"); this->setWindowIcon(QIcon::fromTheme("user-group-properties")); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::ApplicationModal); this->setMinimumSize(480, 460); m_newGroupNameLineEdit = new QLineEdit(this); m_newGroupNameLineEdit->setPlaceholderText("Name for the new group"); m_newGroupSummaryLineEdit = new QLineEdit(this); m_newGroupSummaryLineEdit->setPlaceholderText("Summary"); m_newGroupDescLineEdit = new QLineEdit(this); m_newGroupDescLineEdit->setPlaceholderText("Longer description of the group"); m_createGroupButton = new QPushButton(QIcon::fromTheme("user-group-new"), "CREATE GROUP", this); connect(m_createGroupButton, &QAbstractButton::clicked, this, &GroupsManager::createGroup); m_joinLeaveGroupIdLineEdit = new QLineEdit(this); m_joinLeaveGroupIdLineEdit->setPlaceholderText("ID of the group you wish " "to join or leave"); m_joinGroupButton = new QPushButton(QIcon::fromTheme("list-add-user"), "JOIN!", this); connect(m_joinGroupButton, &QAbstractButton::clicked, this, &GroupsManager::joinGroup); m_leaveGroupButton = new QPushButton(QIcon::fromTheme("list-remove-user"), "LEAVE", this); connect(m_leaveGroupButton, &QAbstractButton::clicked, this, &GroupsManager::leaveGroup); m_closeAction = new QAction(this); m_closeAction->setShortcut(QKeySequence(Qt::Key_Escape)); connect(m_closeAction, &QAction::triggered, this, &QWidget::hide); this->addAction(m_closeAction); // Layout m_layout = new QVBoxLayout(); m_layout->setAlignment(Qt::AlignTop); m_layout->addWidget(m_newGroupNameLineEdit); m_layout->addWidget(m_newGroupSummaryLineEdit); m_layout->addWidget(m_newGroupDescLineEdit); m_layout->addWidget(m_createGroupButton); m_layout->addStretch(1); m_layout->addWidget(m_joinLeaveGroupIdLineEdit); m_layout->addWidget(m_joinGroupButton); m_layout->addWidget(m_leaveGroupButton); this->setLayout(m_layout); qDebug() << "GroupsManager created"; } GroupsManager::~GroupsManager() { qDebug() << "GroupsManager destroyed"; } //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////// SLOTS ///////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// void GroupsManager::createGroup() { if (m_newGroupNameLineEdit->text().trimmed().isEmpty()) { QMessageBox::warning(this, "ERROR", "Groups require a name."); return; } qDebug() << "Creating group..."; m_pumpController->createGroup(m_newGroupNameLineEdit->text().trimmed(), m_newGroupSummaryLineEdit->text().trimmed(), m_newGroupDescLineEdit->text().trimmed()); m_newGroupNameLineEdit->clear(); m_newGroupSummaryLineEdit->clear(); m_newGroupDescLineEdit->clear(); } void GroupsManager::deleteGroup() { } void GroupsManager::joinGroup() { if (m_joinLeaveGroupIdLineEdit->text().trimmed().isEmpty()) { QMessageBox::warning(this, "ERROR", "ID of group to join is empty."); return; } qDebug() << "Joining group..."; m_pumpController->joinGroup(m_joinLeaveGroupIdLineEdit->text().trimmed()); m_joinLeaveGroupIdLineEdit->clear(); } void GroupsManager::leaveGroup() { if (m_joinLeaveGroupIdLineEdit->text().trimmed().isEmpty()) { QMessageBox::warning(this, "ERROR", "ID of group to leave is empty."); return; } qDebug() << "Leaving group..."; m_pumpController->leaveGroup(m_joinLeaveGroupIdLineEdit->text().trimmed()); m_joinLeaveGroupIdLineEdit->clear(); } dianara-v1.4.1/src/asactivity.cpp0000664000175000017500000003165113206623175015074 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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, QString userId, QObject *parent) : QObject(parent) { m_id = activityMap.value("id").toString(); m_ownUserId = userId; // Get the author of the _activity_ (not the object) m_author = new ASPerson(activityMap.value("actor").toMap(), this); // Get the object included with the activity: the post itself, or whatever m_object = new ASObject(activityMap.value("object").toMap(), this); // If the object is a person, as in an activity where someone adds someone else if (m_object->getType() == QStringLiteral("person")) { m_personObject = new ASPerson(activityMap.value("object").toMap(), this); } else { // Otherwise, at least initialize empty, but valid m_personObject = new ASPerson(QVariantMap(), this); } // Get possible "target" m_target = new ASObject(activityMap.value("target").toMap(), this); // FIXME: add bools hasAuthor, hasObject and hasTarget, maybe? // Activity verb: post, share... m_verb = activityMap.value("verb").toString(); if (m_verb == QStringLiteral("share")) { m_shared = true; m_sharedByName = m_author->getNameWithFallback(); m_sharedById = m_author->getId(); m_sharedByAvatar = m_author->getAvatarUrl(); } else { m_shared = false; } // Timestamps m_createdAt = activityMap.value("published").toString(); m_updatedAt = activityMap.value("updated").toString(); // Software used to generate the activity: webUI, a client, a service... m_generator = activityMap.value("generator").toMap().value("displayName").toString(); // Content of the activity, usually a description like "User followed someone" m_content = activityMap.value("content").toString(); // Audience: To QVariantMap aToMap; foreach (QVariant toVariant, activityMap.value("to").toList()) { aToMap = toVariant.toMap(); QString toId = aToMap.value("id").toString(); if (toId == QStringLiteral("http://activityschema.org/collection/public")) { m_recipientsToString += QStringLiteral("") + tr("Public") + QStringLiteral(", "); } else { QString displayName = aToMap.value("displayName").toString().trimmed(); // If name's empty, and ID is from a person if (displayName.isEmpty() && toId.startsWith(QStringLiteral("acct:"))) { displayName = "<" + ASPerson::cleanupId(toId) + ">"; } #ifdef SHOWWRONGTOCC if (displayName.isEmpty()) // if STILL empty, some weirdness is there { displayName = "-?-"; } #endif if (!displayName.isEmpty()) { if (ASPerson::cleanupId(toId) == m_ownUserId) { displayName = "" + displayName + ""; } m_recipientsToString += "" + displayName + ", "; } m_recipientsIdList.append(ASPerson::cleanupId(toId)); } } m_recipientsToString.remove(-2, 2); // remove last comma and space // and Cc QVariantMap aCcMap; foreach (QVariant ccVariant, activityMap.value("cc").toList()) { aCcMap = ccVariant.toMap(); QString ccId = aCcMap.value("id").toString(); if (ccId == QStringLiteral("http://activityschema.org/collection/public")) { m_recipientsCcString += "" + tr("Public") + ", "; } else { QString displayName = aCcMap.value("displayName").toString().trimmed(); // If name's empty, and ID is from a person if (displayName.isEmpty() && ccId.startsWith("acct:")) { displayName = "<" + ASPerson::cleanupId(ccId) + ">"; } #ifdef SHOWWRONGTOCC if (displayName.isEmpty()) // if STILL empty, for some reason... { displayName = "-?-"; } #endif if (!displayName.isEmpty()) { if (ASPerson::cleanupId(ccId) == m_ownUserId) { displayName = "" + displayName + ""; } m_recipientsCcString += "" + displayName + ", "; } m_recipientsIdList.append(ASPerson::cleanupId(ccId)); } } m_recipientsCcString.remove(-2, 2); qDebug() << "ASActivity created" << m_id; } ASActivity::~ASActivity() { qDebug() << "ASActivity destroyed" << m_id; } ////// Getters! ASPerson *ASActivity::author() { return m_author; } ASObject *ASActivity::object() { return m_object; } ASObject *ASActivity::target() { return m_target; } ASPerson *ASActivity::personObject() { return m_personObject; } QString ASActivity::getId() { return m_id; } QString ASActivity::getVerb() { return m_verb; } QString ASActivity::getGenerator() { return m_generator; } QString ASActivity::getCreatedAt() { return m_createdAt; } QString ASActivity::getUpdatedAt() { return m_updatedAt; } QString ASActivity::getContent() { return m_content; } QString ASActivity::getToString() { return m_recipientsToString; } QString ASActivity::getCCString() { return m_recipientsCcString; } QStringList ASActivity::getRecipientsIdList() { return m_recipientsIdList; } bool ASActivity::isShared() { return m_shared; } QString ASActivity::getSharedByName() { return m_sharedByName; } QString ASActivity::getSharedById() { return m_sharedById; } QString ASActivity::getSharedByAvatar() { return m_sharedByAvatar; } /* * Create a string to be used as a tooltip * */ QString ASActivity::generateTooltip() { QString activityTooltip; // Content QString activityObjectContent = m_object->getContent(); if (m_object->getDeletedTime().isEmpty()) { QString objectType = m_object->getType(); if (ASObject::canDisplayObject(objectType)) // False also for empty type { if (!activityObjectContent.startsWith(", so add extra newline activityObjectContent.prepend("
              "); } activityObjectContent.prepend("[ " + ASObject::getTranslatedType(objectType) + " ]
              "); } } else { activityObjectContent.prepend("[" + m_object->getDeletedOnString() + "]"); } if (!activityObjectContent.isEmpty()) { // Object's author name and ID QString activityObjectAuthorName = m_object->author()->getNameWithFallback(); QString activityObjectAuthorId = m_object->author()->getId(); // If there's a name and/or an ID, show them if (!activityObjectAuthorName.isEmpty()) { activityTooltip = "" + activityObjectAuthorName + "
              " "" + activityObjectAuthorId + "" "

              "; // horizontal rule as separator } // Title, only if there's content, too; redundant otherwise QString activityObjectTitle = m_object->getTitle(); if (!activityObjectTitle.isEmpty()) { activityTooltip.append("" + activityObjectTitle + "" "

              "); } activityTooltip.append(activityObjectContent + "
              "); // Object ID, might be interesting to show if it's a person if (m_object->getType() == QStringLiteral("person")) { activityTooltip.append("
              " + m_object->getId() + "

              "); } } // Target info QString activityTargetName = m_target->getTitle(); QString activityTargetUrl = m_target->getUrl(); if (activityTargetUrl.isEmpty()) { activityTargetUrl = m_target->getId(); } if (!activityTargetUrl.isEmpty()) { activityTooltip.append("
              >> "); // Horizontal rule, and >> if (!activityTargetName.isEmpty()) { activityTooltip.append(QString("%1
              %2") .arg(activityTargetName) .arg(activityTargetUrl)); } else { activityTooltip.append("" + activityTargetUrl + ""); } // Show type of target object activityTooltip.append("
              " + ASObject::getTranslatedType(m_target->getType())); } // Remove last
              , if needed if (activityTooltip.endsWith("
              ")) { activityTooltip.remove(-4, 4); // Check again, for double
              's if (activityTooltip.endsWith("
              ")) { activityTooltip.remove(-4, 4); } } activityTooltip = activityTooltip.trimmed(); // If there's something, ensure it gets treated as HTML if (!activityTooltip.isEmpty()) { activityTooltip.prepend(""); } return activityTooltip; } /* * Create a snippet of the activity and its object * */ QString ASActivity::generateSnippet(int charLimit) { QString snippet; QString authorName = m_object->author()->getNameWithFallback(); // If author name IS empty, probably means the author of the activity is the // author of the object (like posting a comment), so this wouldn't be useful if (m_object->author()->getId() != this->author()->getId() && !authorName.isEmpty()) { /* * For some reason, when creating a structure like: * * Something by name: * * a line break appears before the nickname. * Putting everything into a

              fixes it; or making it piece by piece * */ snippet.append("♦ " // Diamond symbols -- Options: • ≡ "" + tr("%1 by %2", "1=kind of object: note, comment, etc; " "2=author's name") .arg(ASObject::getTranslatedType(m_object->getType())) .arg("" "" + authorName + "") + ":"); } QString objectContent = m_object->getContent(); if (!objectContent.isEmpty()) { /* //// Disabled: Showing the title is always redundant //// considering how the Pump.io core generates the //// activities descriptions. QString objectTitle = m_object->getTitle(); if (!objectTitle.isEmpty()) { snippet.append("" + objectTitle + "
              "); } */ QTextDocument textDocument; textDocument.setHtml(objectContent); // Limit characters in snippet if (textDocument.characterCount() > charLimit) { QTextCursor cursor = textDocument.find(QRegExp(".*"), charLimit); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); cursor.removeSelectedText(); cursor.insertHtml("  [...]"); } snippet.append(MiscHelpers::cleanupHtml(textDocument.toHtml())); } return snippet.trimmed(); } void ASActivity::setFilterMatches(QVariantMap newFilterMatches) { m_filterMatches = newFilterMatches; } QVariantMap ASActivity::getFilterMatches() { return m_filterMatches; } dianara-v1.4.1/src/minorfeeditem.h0000644000175000017500000000561613210605313015175 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "globalobject.h" #include "timestamp.h" #include "asactivity.h" #include "post.h" #include "avatarbutton.h" #include "filterchecker.h" #include "filtermatcheswidget.h" class MinorFeedItem : public QFrame { Q_OBJECT public: explicit MinorFeedItem(ASActivity *activity, bool highlightedByFilter, PumpController *pumpController, GlobalObject *globalObject, QWidget *parent = 0); ~MinorFeedItem(); void setItemAsNew(bool isNew, bool informFeed); bool isNew(); void setFuzzyTimeStamp(); void syncAvatarFollowState(); int getItemHighlightType(); QString getActivityId(); signals: void itemRead(bool wasHighlighted); public slots: void openOriginalPost(); void showUrlInfo(QString url); protected: virtual void mousePressEvent(QMouseEvent *event); virtual void leaveEvent(QEvent *event); virtual void paintEvent(QPaintEvent *event); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_topLayout; QVBoxLayout *m_leftLayout; QVBoxLayout *m_rightLayout; QHBoxLayout *m_rightLowerLayout; int m_verbIconType; QStringList m_verbIcons; int m_verbIconSize; int m_verbIconSizeSubtle; AvatarButton *m_avatarButton; QLabel *m_timestampLabel; QLabel *m_activityDescriptionLabel; QPushButton *m_openButton; QVariantMap m_originalObjectMap; QVariantMap m_inReplyToMap; AvatarButton *m_targetAvatarButton; bool m_haveTargetAvatar; FilterMatchesWidget *m_filterMatchesWidget; bool m_itemIsOwn; bool m_itemIsNew; int m_itemHighlightType; QString m_createdAtTimestamp; QString m_activityId; PumpController *m_pumpController; GlobalObject *m_globalObject; }; #endif // MINORFEEDITEM_H dianara-v1.4.1/src/accountdialog.cpp0000664000175000017500000004131313202676316015525 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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(tr("Account Configuration") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("dialog-password", QIcon(":/images/button-password.png"))); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::ApplicationModal); this->setMinimumSize(640, 520); QSettings settings; this->resize(settings.value("AccountDialog/accountWindowSize", QSize(760, 620)).toSize()); m_pumpController = pumpController; connect(m_pumpController, &PumpController::openingAuthorizeUrl, this, &AccountDialog::showAuthorizationUrl); connect(m_pumpController, &PumpController::authorizationStatusChanged, this, &AccountDialog::showAuthorizationStatus); connect(m_pumpController, &PumpController::authorizationSucceeded, this, &AccountDialog::onAuthorizationSucceeded); QFont helpFont; helpFont.setPointSize(helpFont.pointSize() - 1); m_help1Label = new QLabel(tr("First, enter your Webfinger ID, " "your pump.io address.") + "
              " + tr("Your address looks like " "username@pumpserver.org, and " "you can find it in your profile, " "in the web interface.") + "
              " + tr("If your profile is at " "https://pump.example/yourname, then " "your address is yourname@pump.example") + "

              " + tr("If you don't have an account yet, " "you can sign up for one at %1. " "This link will take you to a " "random public server.", "1=link to website") .arg("" "pump.io/tryit.html") + "

              " + tr("If you need help: %1") .arg("" + tr("Pump.io User Guide") + ""), this); m_help1Label->setWordWrap(true); m_help1Label->setFont(helpFont); m_help1Label->setOpenExternalLinks(true); m_userIdIconLabel = new QLabel(this); m_userIdIconLabel->setPixmap(QIcon::fromTheme("preferences-desktop-user", QIcon(":/images/no-avatar.png")) .pixmap(64,64) .scaledToWidth(64, Qt::SmoothTransformation)); m_userIdLabel = new QLabel("" + tr("Your Pump.io address:") + "", this); m_userIdLineEdit = new QLineEdit(this); QString userIdTooltip = tr("Your address, like username@pumpserver.org"); m_userIdLineEdit->setPlaceholderText(userIdTooltip); m_userIdLineEdit->setToolTip("" + userIdTooltip); // HTMLized for wordwrap connect(m_userIdLineEdit, &QLineEdit::returnPressed, this, &AccountDialog::askForToken); m_getVerifierButton = new QPushButton(QIcon::fromTheme("object-unlocked"), tr("Get &Verifier Code"), this); m_getVerifierButton->setToolTip("" + tr("After clicking this button, a web " "browser will open, requesting " "authorization for Dianara")); connect(m_getVerifierButton, &QAbstractButton::clicked, this, &AccountDialog::askForToken); separatorLine = new QFrame(this); // --------------- separatorLine->setFrameStyle(QFrame::HLine); m_help2Label = 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 translators "Don't translate the VERIFIER word!"), this); m_help2Label->setWordWrap(true); m_help2Label->setFont(helpFont); m_verifierIconLabel = new QLabel(this); m_verifierIconLabel->setPixmap(QIcon::fromTheme("dialog-password", QIcon(":/images/button-password.png")) .pixmap(64,64) .scaledToWidth(64, Qt::SmoothTransformation)); m_verifierLabel = new QLabel("" + tr("Verifier code:") + "", this); QString verifierTooltip = tr("Enter or paste the verifier code provided " "by your Pump server here"); m_verifierLineEdit = new QLineEdit(this); m_verifierLineEdit->setPlaceholderText(verifierTooltip); m_verifierLineEdit->setToolTip("" + verifierTooltip); connect(m_verifierLineEdit, &QLineEdit::textChanged, this, &AccountDialog::onVerifierChanged); m_authorizeButton = new QPushButton(QIcon::fromTheme("security-high"), tr("&Authorize Application"), this); connect(m_authorizeButton, &QAbstractButton::clicked, this, &AccountDialog::setVerifierCode); connect(m_verifierLineEdit, &QLineEdit::returnPressed, m_authorizeButton, &QAbstractButton::click); // To notify invalid ID or empty verifier code // Cleared when typing in any of the two fields m_errorsLabel = new QLabel(this); connect(m_userIdLineEdit, &QLineEdit::textChanged, m_errorsLabel, &QLabel::clear); QFont authorizationStatusFont; authorizationStatusFont.setPointSize(authorizationStatusFont.pointSize() + 2); authorizationStatusFont.setBold(true); m_authorizationStatusLabel = new QLabel(this); m_authorizationStatusLabel->setOpenExternalLinks(true); m_authorizationStatusLabel->setFont(authorizationStatusFont); m_saveButton = new QPushButton(QIcon::fromTheme("document-save", QIcon(":/images/button-save.png")), tr("&Save Details"), this); m_saveButton->setDisabled(true); // Disabled initially connect(m_saveButton, &QAbstractButton::clicked, this, &AccountDialog::saveDetails); m_cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("&Cancel"), this); connect(m_cancelButton, &QAbstractButton::clicked, this, &QWidget::hide); // Unlock button, visible when the account is already authorized, + info m_unlockExplanationLabel = new QLabel("" + tr("Your account is properly " "configured.") + "" "

              " + tr("Press Unlock if you wish " "to configure a different " "account.") + "


              ", this); m_unlockExplanationLabel->setAlignment(Qt::AlignHCenter); m_unlockButton = new QPushButton(QIcon::fromTheme("object-unlocked"), tr("&Unlock"), this); connect(m_unlockButton, &QAbstractButton::clicked, this, &AccountDialog::unlockDialog); //////////////////////////////////////////////////////////////////// Layout m_idLayout = new QHBoxLayout(); m_idLayout->addWidget(m_userIdIconLabel); m_idLayout->addSpacing(4); m_idLayout->addWidget(m_userIdLabel); m_idLayout->addWidget(m_userIdLineEdit); m_idLayout->addWidget(m_getVerifierButton); m_verifierLayout = new QHBoxLayout(); m_verifierLayout->addWidget(m_verifierIconLabel); m_verifierLayout->addSpacing(4); m_verifierLayout->addWidget(m_verifierLabel); m_verifierLayout->addWidget(m_verifierLineEdit); m_verifierLayout->addWidget(m_authorizeButton); m_buttonsLayout = new QHBoxLayout(); m_buttonsLayout->setAlignment(Qt::AlignRight); m_buttonsLayout->addWidget(m_saveButton); m_buttonsLayout->addWidget(m_cancelButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->addWidget(m_unlockExplanationLabel, 6); m_mainLayout->addWidget(m_unlockButton, 0, Qt::AlignHCenter); m_mainLayout->addWidget(m_help1Label, 6); m_mainLayout->addSpacing(2); m_mainLayout->addStretch(1); m_mainLayout->addLayout(m_idLayout, 2); m_mainLayout->addSpacing(4); m_mainLayout->addStretch(1); m_mainLayout->addWidget(separatorLine); m_mainLayout->addStretch(1); m_mainLayout->addSpacing(4); m_mainLayout->addWidget(m_help2Label, 2); m_mainLayout->addSpacing(2); m_mainLayout->addStretch(1); m_mainLayout->addLayout(m_verifierLayout, 2); m_mainLayout->addWidget(m_errorsLabel, 1, Qt::AlignCenter); m_mainLayout->addSpacing(1); m_mainLayout->addWidget(m_authorizationStatusLabel, 1, Qt::AlignCenter); m_mainLayout->addStretch(2); m_mainLayout->addSpacing(8); m_mainLayout->addLayout(m_buttonsLayout, 1); this->setLayout(m_mainLayout); // Load saved User ID, and authorization status m_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 m_verifierLineEdit->setDisabled(true); m_authorizeButton->setDisabled(true); qDebug() << "Account dialog created"; } AccountDialog::~AccountDialog() { qDebug() << "Account dialog destroyed"; } void AccountDialog::setLockMode(bool locked) { if (locked) { m_help1Label->hide(); separatorLine->hide(); m_help2Label->hide(); m_unlockExplanationLabel->show(); m_unlockButton->show(); m_verifierLineEdit->setDisabled(true); m_authorizeButton->setDisabled(true); } else { m_help1Label->show(); separatorLine->show(); m_help2Label->show(); m_unlockExplanationLabel->hide(); m_unlockButton->hide(); } m_userIdLineEdit->setDisabled(locked); m_getVerifierButton->setDisabled(locked); } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////// SLOTS ////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void AccountDialog::askForToken() { QSettings settings; settings.setValue("isApplicationAuthorized", false); // Until steps are completed // Don't fail if there are spaces before or after the ID, they're only human ;) m_userIdLineEdit->setText(m_userIdLineEdit->text().trimmed()); // Match username@server.tld AND check for only 1 @ sign if (QRegExp("\\S+@(\\S+\\.\\S+|localhost.*)").exactMatch(m_userIdLineEdit->text()) && m_userIdLineEdit->text().count("@") == 1) { // Clear verifier field and re-enable it m_verifierLineEdit->clear(); m_verifierLineEdit->setEnabled(true); // Show message about the web browser that will be started m_errorsLabel->setText("[ " + tr("A web browser will start now, " "where you can get the verifier code") + " ]"); m_pumpController->setNewUserId(m_userIdLineEdit->text()); m_pumpController->getToken(); } else // userID does not match user@hostname.domain { m_errorsLabel->setText("[ " + tr("Your Pump address is invalid") + " ]"); qDebug() << "userID is invalid!"; } } void AccountDialog::setVerifierCode() { qDebug() << "AccountDialog::setVerifierCode()" << m_verifierLineEdit->text(); if (!m_verifierLineEdit->text().trimmed().isEmpty()) { m_pumpController->authorizeApplication(m_verifierLineEdit->text() .trimmed()); } else { m_errorsLabel->setText("[ " + tr("Verifier code is empty") + " ]"); } } void AccountDialog::onVerifierChanged(QString newText) { m_authorizeButton->setDisabled(newText.isEmpty()); m_errorsLabel->clear(); } void AccountDialog::onAuthorizationSucceeded() { m_saveButton->setEnabled(true); m_errorsLabel->clear(); } void AccountDialog::showAuthorizationStatus(bool authorized) { if (authorized) { m_authorizationStatusLabel->setText(QString::fromUtf8("\342\234\224 ") // Check mark + tr("Dianara is authorized to " "access your data")); } else { m_authorizationStatusLabel->clear(); } this->setLockMode(m_pumpController->currentlyAuthorized()); } /* * Show the authorization URL in a label, * in case the browser doesn't open automatically * */ void AccountDialog::showAuthorizationUrl(QUrl url, bool browserLaunched) { QString message = tr("If the browser doesn't open automatically, " "copy this address manually") + ":
              " + url.toString() + ""; m_authorizationStatusLabel->setText(message); if (!browserLaunched) { m_errorsLabel->setText("[ " + tr("Unable to open web browser!") + " ]"); } } /* * Save the new userID and inform other parts of the program about it * */ void AccountDialog::saveDetails() { QString newUserId = m_userIdLineEdit->text().trimmed(); if (newUserId.isEmpty() || !newUserId.contains("@")) { return; // If no user ID, or with no "@", ignore // FIXME } QSettings settings; settings.setValue("userID", newUserId); settings.sync(); m_errorsLabel->clear(); // Clear previous error messages, if any // If it's authorized, disable the Save Details button if (m_pumpController->currentlyAuthorized()) { m_saveButton->setDisabled(true); // Set wheels in motion only if the auth process is complete emit userIdChanged(newUserId); } this->hide(); } void AccountDialog::unlockDialog() { this->setLockMode(false); } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////////// PROTECTED //////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void AccountDialog::hideEvent(QHideEvent *event) { this->setLockMode(m_pumpController->currentlyAuthorized()); QSettings settings; if (settings.isWritable()) { settings.setValue("AccountDialog/accountWindowSize", this->size()); } event->accept(); } dianara-v1.4.1/src/filtermatcheswidget.h0000644000175000017500000000257513210105565016411 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 FILTERMATCHESWIDGET_H #define FILTERMATCHESWIDGET_H #include #include #include #include #include class FilterMatchesWidget : public QFrame { Q_OBJECT public: explicit FilterMatchesWidget(QVariantMap filterMatchesMap, QWidget *parent = 0); ~FilterMatchesWidget(); QString tagList(QStringList tagsList); signals: public slots: private: QHBoxLayout *m_layout; QLabel *m_contentLabel; }; #endif // FILTERMATCHESWIDGET_H dianara-v1.4.1/src/draftsmanager.h0000664000175000017500000000526513210124402015161 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 DRAFTSMANAGER_H #define DRAFTSMANAGER_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include "globalobject.h" #include "datafile.h" #include "mischelpers.h" class DraftsManager : public QWidget { Q_OBJECT public: explicit DraftsManager(GlobalObject *globalObject, QWidget *parent = nullptr); ~DraftsManager(); void loadDraftsFromFile(); void saveDraftsToFile(); void saveDraft(QString title, QString body, QString type, QString attachment, QVariantMap audience, int position); QString generateDraftId(QString title, QString body); void updateDraftId(QString id); QMenu *getDraftMenu(); signals: void draftSelected(QString id, QString title, QString body, QString type, QString attachment, QVariantMap audience, int position); void saveDraftRequested(); void windowShown(); public slots: void onDraftSelectedFromMenu(QAction *selectedAction); void onDraftSelectedFromList(int row); void deleteSelectedDraft(); protected: virtual void showEvent(QShowEvent *event); virtual void hideEvent(QHideEvent *event); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_buttonsLayout; QListWidget *m_listWidget; QLabel *m_previewLabel; QPushButton *m_deleteButton; QPushButton *m_closeButton; QAction *m_closeAction; QMenu *m_draftsMenu; QMenu *m_loadMenu; QAction *m_saveAction; QAction *m_showManagerAction; QString m_currentDraftId; GlobalObject *m_globalObject; }; #endif // DRAFTSMANAGER_H dianara-v1.4.1/src/minorfeed.h0000664000175000017500000000654713203367507014340 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "pumpcontroller.h" #include "globalobject.h" #include "mischelpers.h" #include "minorfeeditem.h" #include "asactivity.h" #include "filterchecker.h" class MinorFeed : public QFrame { Q_OBJECT public: explicit MinorFeed(PumpController::requestTypes minorFeedType, PumpController *pumpController, GlobalObject *globalObject, FilterChecker *filterChecker, QWidget *parent = 0); ~MinorFeed(); void clearContents(); void removeOldItems(int minimumToKeep); void insertSeparator(int position); void markAllAsRead(); void updateFuzzyTimestamps(); void syncActivityWithTimelines(ASActivity *activity); signals: void newItemsCountChanged(int itemCount, int highlightedCount); void newItemsReceived(PumpController::requestTypes m_feedType, int itemCount, int highlightedCount, int filteredCount, int pendingForNextUpdate); void objectUpdated(ASObject *object); void objectLiked(QString objectId, QString objectType, QString actorId, QString actorName, QString actorUrl); void objectUnliked(QString objectId, QString objectType, QString actorId); void objectReplyAdded(ASObject *object); void objectDeleted(ASObject *object); public slots: void updateFeed(); void getMoreActivities(); void setFeedContents(QVariantList activitiesList, QString previous, QString next, int totalItemCount); void onUpdateFailed(int requestType); void decreaseNewItemsCount(bool wasHighlighted); void updateAvatarFollowStates(); private: QString m_prevLink; QString m_nextLink; int m_fullFeedItemCount; int m_pendingToReceiveNextTime; PumpController::requestTypes m_feedType; int m_feedBatchItemCount; bool m_firstLoad; bool m_gettingNew; QString m_previousNewestActivityId; QList m_itemsInFeed; int m_unreadItemsCount; int m_highlightedItemsCount; QVBoxLayout *m_mainLayout; QVBoxLayout *m_itemsLayout; QPushButton *m_getPendingButton; QPushButton *m_getOlderButton; QFrame *m_separatorFrame; PumpController *m_pumpController; GlobalObject *m_globalObject; FilterChecker *m_filterChecker; }; #endif // MINORFEED_H dianara-v1.4.1/src/firstrunwizard.h0000644000175000017500000000441313206637060015446 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 FIRSTRUNWIZARD_H #define FIRSTRUNWIZARD_H #include #include #include #include #include #include #include #include #include #include "accountdialog.h" #include "profileeditor.h" #include "configdialog.h" #include "helpwidget.h" #include "globalobject.h" class FirstRunWizard : public QWidget { Q_OBJECT public: explicit FirstRunWizard(AccountDialog *accountDialog, ProfileEditor *profileEditor, ConfigDialog *configDialog, HelpWidget *helpWidget, GlobalObject *globalObject, QWidget *parent = 0); ~FirstRunWizard(); signals: public slots: protected: virtual void closeEvent(QCloseEvent *event); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_bottomLayout; QLabel *m_explanationLabel; QPushButton *m_configAccountButton; QLabel *m_editProfileLabel; QPushButton *m_editProfileButton; QLabel *m_publicPostsLabel; QCheckBox *m_publicPostsCheckbox; QPushButton *m_helpButton; QCheckBox *m_showAgainCheckbox; QPushButton *m_closeButton; // Widgets from MainWindow AccountDialog *m_accountDialog; ProfileEditor *m_profileEditor; ConfigDialog *m_configDialog; HelpWidget *m_helpWidget; }; #endif // FIRSTRUNWIZARD_H dianara-v1.4.1/src/globalobject.h0000644000175000017500000001544613174452276015020 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 GLOBALOBJECT_H #define GLOBALOBJECT_H #include #include #include #include #include #include #include #include class GlobalObject : public QObject { Q_OBJECT public: explicit GlobalObject(QObject *parent = 0); ~GlobalObject(); // General options void syncGeneralSettings(); // Font options void syncFontSettings(QString postTitleFont, QString postContentsFont, QString commentsFont, QString minorFeedFont); QString getPostTitleFont(); QString getPostContentsFont(); QString getCommentsFont(); QString getMinorFeedFont(); // Color options void syncColorSettings(QStringList newColorList); QStringList getColorsList(); QString getColor(int colorIndex); // Timeline options void syncTimelinesSettings(int pppMain, int pppOther, bool showDeleted, bool hideDuplicates, bool jumpToNew, int minorFeedSnippets, int snippetsChars, int snippetsCharsHl, int mfAvatarIdx, int mfVerbIconType); int getPostsPerPageMain(); int getPostsPerPageOther(); bool getShowDeleted(); bool getHideDuplicates(); bool getJumpToNewOnUpdate(); int getMinorFeedSnippetsType(); int getSnippetsCharLimit(); int getSnippetsCharLimitHl(); int getMfAvatarIndex(); QSize getMfAvatarSize(); int getMfIconType(); // Post options void syncPostSettings(int postAvatarIdx, int commentAvatarIdx, bool extendedShares, bool showExtraInfo, bool hlAuthorComments, bool hlOwnComments, bool postIgnoreSslInImages, bool fullImages); int getPostAvatarIndex(); QSize getPostAvatarSize(); int getCommentAvatarIndex(); QSize getCommentAvatarSize(); bool getPostExtendedShares(); bool getPostShowExtraInfo(); bool getPostHLAuthorComments(); bool getPostHLOwnComments(); bool getPostIgnoreSslInImages(); bool getPostFullImages(); // Composer options void syncComposerSettings(bool publicPosts, bool filenameAsTitle, bool showCharCounter); bool getPublicPostsByDefault(); bool getUseFilenameAsTitle(); bool getShowCharacterCounter(); // Privacy options void syncPrivacySettings(bool silentFollows, bool silentLists, bool silentLiking); bool getSilentFollows(); bool getSilentLists(); bool getSilentLikes(); // Notification options void syncNotificationSettings(bool notifyTaskbar); bool getNotifyInTaskbar(); // Tray options void syncTrayOptions(bool hideInTrayStartup); bool getHideInTray(); /////////////////////////////////////////////////////////////////////////// void setDataDirectory(QString dataDir); QString getDataDirectory(); void createMessageForContact(QString id, QString name, QString url); void browseUserMessages(QString userId, QString userName, QIcon userAvatar, QString userOutbox); void editPost(QString originalPostId, QString type, QString title, QString contents); QSortFilterProxyModel *getNickCompletionModel(); void addToNickCompletionModel(QString id, QString name, QString url); void removeFromNickCompletionModel(QString id); void clearNickCompletionModel(); void sortNickCompletionModel(); QPair getDataForNick(QString id); void setStatusMessage(QString message); void logMessage(QString message, QString url=""); void storeTimelineHeight(int height); int getTimelineHeight(); QSize getAvatarSizeForIndex(int index); void notifyProgramShutdown(); bool isProgramShuttingDown(); signals: void messagingModeRequested(QString id, QString name, QString url); void userTimelineRequested(QString userId, QString userName, QIcon userAvatar, QString userOutbox); void postEditRequested(QString originalPostId, QString type, QString title, QString contents); void messageForStatusBar(QString message); void messageForLog(QString message, QString url); void programShuttingDown(); public slots: private: // General options //// (not handled here yet) // Font options QString postTitleFontInfo; QString postContentsFontInfo; QString commentsFontInfo; QString minorFeedFontInfo; // Color options QStringList colorsList; // Timeline options int postsPerPageMain; int postsPerPageOther; bool showDeletedPosts; bool hideDuplicatedPosts; bool jumpToNewOnUpdate; int minorFeedSnippetsType; int snippetsCharLimit; int snippetsCharLimitHl; int mfAvatarIndex; QSize mfAvatarSize; int mfIconType; // Post options int postAvatarIndex; QSize postAvatarSize; int commentAvatarIndex; QSize commentAvatarSize; bool postExtendedShares; bool postShowExtraInfo; bool postHLAuthorComments; bool postHLOwnComments; bool postIgnoreSslInImages; bool postFullImages; // Composer options bool publicPostsByDefault; bool useFilenameAsTitle; bool showCharacterCounter; // Privacy options bool silentFollowing; bool silentListsHandling; bool silentLiking; // Notification options bool notifyInTaskbar; // Tray options bool hideInTray; ////////////////////////////////////////////////////////////////////////// // Other stuff QString dataDirectory; QStandardItemModel *nickCompletionModel; QSortFilterProxyModel *filterCompletionModel; int timelineHeight; bool programClosing; }; #endif // GLOBALOBJECT_H dianara-v1.4.1/src/filterchecker.cpp0000664000175000017500000001226113210105422015503 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "filterchecker.h" FilterChecker::FilterChecker(QObject *parent) : QObject(parent) { qDebug() << "FilterChecker created"; } FilterChecker::~FilterChecker() { qDebug() << "FilterChecker destroyed"; } /* * Set or update filter rules; called from the FilterEditor * */ void FilterChecker::setFilters(QVariantList newFiltersList) { m_filteredContent.clear(); m_filteredAuthor.clear(); m_filteredGenerator.clear(); m_filteredDescription.clear(); // Define new filter values foreach (QVariant filter, newFiltersList) { QString actionType = filter.toMap().value("action").toString(); int filterType = filter.toMap().value("type").toInt(); QString filterText = filter.toMap().value("text").toString(); QStringList pair; pair << filterText << actionType; switch (filterType) { case 0: // post content m_filteredContent.append(pair); break; case 1: // author m_filteredAuthor.append(pair); break; case 2: // application (generator) m_filteredGenerator.append(pair); break; case 3: // activity description m_filteredDescription.append(pair); break; default: break; } } qDebug() << "FilterChecker() filters updated, with" << newFiltersList.length() << "filters"; } /* * Return NoFiltering, FilterOut or Highlight * */ int FilterChecker::validateActivity(ASActivity *activity) { QVariantMap filterMatches; int filterAction = NoFiltering; // Innocent until proven guilty! QStringList matchingContent; QStringList matchingAuthor; QStringList matchingGenerator; QStringList matchingDescription; const QString postTitle = activity->object()->getTitle(); const QString postContents = activity->object()->getContent(); foreach (QStringList contents, m_filteredContent) { if (postContents.contains(contents.first(), Qt::CaseInsensitive) || postTitle.contains(contents.first(), Qt::CaseInsensitive)) { qDebug() << "Filtering item because of Post Content:" << contents.first(); filterAction = contents.last().toInt(); matchingContent.append(contents.first()); } } const QString activityAuthorId = activity->author()->getId(); const QString objectAuthorId = activity->object()->author()->getId(); foreach (QStringList authorId, m_filteredAuthor) { if (activityAuthorId.contains(authorId.first(), Qt::CaseInsensitive) || objectAuthorId.contains(authorId.first(), Qt::CaseInsensitive)) { qDebug() << "Filtering item because of Author ID:" << authorId.first(); filterAction = authorId.last().toInt(); matchingAuthor.append(authorId.first()); } } const QString activityGenerator = activity->getGenerator(); foreach (QStringList generator, m_filteredGenerator) { if (activityGenerator.contains(generator.first(), Qt::CaseInsensitive)) { qDebug() << "Filtering item because of Application (generator):" << generator.first(); filterAction = generator.last().toInt(); matchingGenerator.append(generator.first()); } } // Remove links from activity description, so it's easier to write filtering rules const QString activityDescription = MiscHelpers::htmlWithoutLinks(activity->getContent()); foreach (QStringList description, m_filteredDescription) { if (activityDescription.contains(description.first(), Qt::CaseInsensitive)) { qDebug() << "Filtering item because of Activity Description:" << description.first(); filterAction = description.last().toInt(); matchingDescription.append(description.first()); } } filterMatches.insert("filterAction", filterAction); filterMatches.insert("matchingContent", matchingContent); filterMatches.insert("matchingAuthor", matchingAuthor); filterMatches.insert("matchingGenerator", matchingGenerator); filterMatches.insert("matchingDescription", matchingDescription); activity->setFilterMatches(filterMatches); return filterAction; } dianara-v1.4.1/src/siteuserslist.cpp0000644000175000017500000001477613216304304015634 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "siteuserslist.h" SiteUsersList::SiteUsersList(PumpController *pumpController, GlobalObject *globalObject, QWidget *parent) : QWidget(parent) { m_pumpController = pumpController; m_explanationLabel = new QLabel(tr("You can get a list of the newest " "users registered on your server " "by clicking the button below."), this); m_explanationLabel->setWordWrap(true); m_explanationLabel->setAlignment(Qt::AlignTop | Qt::AlignHCenter); m_explanationLabel->setMargin(8); m_getListButton = new QPushButton(QIcon::fromTheme("system-users", QIcon(":/images/button-users.png")), tr("Get list of users from your server"), this); connect(m_getListButton, &QAbstractButton::clicked, this, &SiteUsersList::getList); m_infoLinksLabel = new QLabel(tr("More resources to find users:") + "", this); m_infoLinksLabel->setWordWrap(true); m_infoLinksLabel->setAlignment(Qt::AlignTop); m_infoLinksLabel->setMargin(8); m_infoLinksLabel->setOpenExternalLinks(true); m_serverInfoLabel = new QLabel(this); m_serverInfoLabel->setWordWrap(true); m_serverInfoLabel->setAlignment(Qt::AlignCenter); m_serverInfoLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Raised); m_serverInfoLabel->setMargin(4); m_serverInfoLabel->hide(); m_closeListButton = new QPushButton(QIcon::fromTheme("window-close", QIcon(":/images/button-close.png")), QString(), this); m_closeListButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); m_closeListButton->setShortcut(QKeySequence("Ctrl+W")); m_closeListButton->setToolTip(tr("Close list") + " (" + m_closeListButton->shortcut() .toString(QKeySequence::NativeText) + ")"); connect(m_closeListButton, &QAbstractButton::clicked, this, &SiteUsersList::closeList); m_closeListButton->hide(); m_userList = new ContactList(m_pumpController, globalObject, QStringLiteral("site-users"), this); m_userList->hide(); connect(m_pumpController, &PumpController::siteUserListReceived, this, &SiteUsersList::setListContents); m_middleLayout = new QHBoxLayout(); m_middleLayout->addWidget(m_serverInfoLabel); m_middleLayout->addWidget(m_closeListButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->setContentsMargins(0, 0, 0, 0); m_mainLayout->addWidget(m_explanationLabel, 0, Qt::AlignVCenter); m_mainLayout->addWidget(m_getListButton, 0, Qt::AlignTop | Qt::AlignHCenter); m_mainLayout->addWidget(m_infoLinksLabel, 0, Qt::AlignTop); m_mainLayout->addLayout(m_middleLayout); m_mainLayout->addWidget(m_userList); this->setLayout(m_mainLayout); qDebug() << "SiteUsersList created"; } SiteUsersList::~SiteUsersList() { qDebug() << "SiteUsersList destroyed"; } /*****************************************************************************/ /*********************************** SLOTS ***********************************/ /*****************************************************************************/ void SiteUsersList::getList() { m_pumpController->getSiteUserList(); m_explanationLabel->hide(); m_getListButton->hide(); m_infoLinksLabel->hide(); m_serverInfoLabel->setText(tr("Loading...")); m_serverInfoLabel->show(); m_closeListButton->show(); m_userList->clearListContents(); m_userList->show(); } void SiteUsersList::setListContents(QVariantList userList, int totalUsers) { m_serverInfoLabel->setText(tr("%1 users in %2", "%1 = user count, %2 = server name") .arg(QLocale::system().toString(totalUsers)) .arg(m_pumpController->currentServerDomain())); m_userList->setListContents(userList); } void SiteUsersList::closeList() { m_explanationLabel->show(); m_getListButton->show(); m_infoLinksLabel->show(); m_serverInfoLabel->hide(); m_closeListButton->hide(); m_userList->clearListContents(); m_userList->hide(); } dianara-v1.4.1/src/minorfeeditem.cpp0000644000175000017500000005004013210605335015523 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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, bool highlightedByFilter, PumpController *pumpController, GlobalObject *globalObject, QWidget *parent) : QFrame(parent) { m_pumpController = pumpController; m_globalObject = globalObject; m_itemIsNew = false; m_haveTargetAvatar = false; // This sizePolicy prevents cut messages, and the huge space at the end // of the feed, after clicking "Older Activities" several times this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); activity->setParent(this); // reparent the passed activity // Store activity ID so MinorFeed can reconstruct "next" url's m_activityId = activity->getId(); QFont mainFont; mainFont.setPointSize(mainFont.pointSize() - 2); QString authorId = activity->author()->getId(); m_itemIsOwn = (authorId == m_pumpController->currentUserId()); m_avatarButton = new AvatarButton(activity->author(), m_pumpController, m_globalObject, m_globalObject->getMfAvatarSize(), this); m_createdAtTimestamp = activity->getCreatedAt(); QString timestampTooltip = "" + Timestamp::localTimeDate(m_createdAtTimestamp) + ""; QString generator = activity->getGenerator(); if (!generator.isEmpty()) { timestampTooltip.append("
              " + tr("Using %1", "Application used to generate this activity") .arg(generator)); } QString toField = activity->getToString(); QString ccField = activity->getCCString(); if (!toField.isEmpty() || !ccField.isEmpty()) { timestampTooltip.append("
              "); // Extra separation before To or Cc if (!toField.isEmpty()) { timestampTooltip.append("
              " + tr("To: %1", "1=people to whom this activity was sent") .arg(toField)); } if (!ccField.isEmpty()) { timestampTooltip.append("
              " + tr("Cc: %1", "1=people to whom this activity was sent as CC") .arg(ccField)); } } m_timestampLabel = new QLabel(this); mainFont.setBold(true); m_timestampLabel->setFont(mainFont); m_timestampLabel->setWordWrap(true); m_timestampLabel->setToolTip(timestampTooltip.trimmed()); m_timestampLabel->setAlignment(Qt::AlignCenter); m_timestampLabel->setAutoFillBackground(true); m_timestampLabel->setForegroundRole(QPalette::Text); m_timestampLabel->setBackgroundRole(QPalette::Base); m_timestampLabel->setFrameStyle(QFrame::Panel | QFrame::Raised); this->setFuzzyTimeStamp(); // The description itself, like "JaneDoe updated a note", m_activityDescriptionLabel = new QLabel(this); // To be filled later QFont descriptionFont; descriptionFont.fromString(m_globalObject->getMinorFeedFont()); m_activityDescriptionLabel->setFont(descriptionFont); m_activityDescriptionLabel->setWordWrap(true); m_activityDescriptionLabel->setOpenExternalLinks(true); m_activityDescriptionLabel->setAlignment(Qt::AlignTop); m_activityDescriptionLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::MinimumExpanding); connect(m_activityDescriptionLabel, &QLabel::linkHovered, this, &MinorFeedItem::showUrlInfo); // Tooltip contents QString activityTooltip = activity->generateTooltip(); if (!activityTooltip.isEmpty()) { // Set the activity info as tooltip for description label m_activityDescriptionLabel->setToolTip(activityTooltip); // Set a different mouse cursor for the description label, as a hint to wait for tooltip info m_activityDescriptionLabel->setCursor(Qt::WhatsThisCursor); } m_originalObjectMap = activity->object()->getOriginalObject(); m_inReplyToMap = activity->object()->getInReplyTo(); QString inReplyToAuthorId = ASPerson::cleanupId(m_inReplyToMap .value("author").toMap() .value("id").toString()); // Highlight this item if it's about the user, with different colors m_itemHighlightType = -1; // False // We are in the recipient list of the activity if (activity->getRecipientsIdList().contains(m_pumpController->currentUserId())) { m_itemHighlightType = 0; } // Activity is in reply to something made by us if (inReplyToAuthorId == m_pumpController->currentUserId()) { m_itemHighlightType = 1; } // We are the object; someone added us, etc. if (activity->object()->getId() == m_pumpController->currentUserId()) { m_itemHighlightType = 2; } // The object is ours; someone favorited our note, or something if (activity->object()->author()->getId() == m_pumpController->currentUserId()) { m_itemHighlightType = 3; } //// Special case: highlighting the item because there's a filter rule for it if (highlightedByFilter && m_itemHighlightType == -1) // Only if there's no other reason for HL { m_itemHighlightType = 4; } if (m_itemHighlightType != -1) { // Unless the user ID is also empty! if (!m_pumpController->currentUserId().isEmpty()) { QString highlightItemColor = m_globalObject->getColor(m_itemHighlightType); if (QColor::isValidColor(highlightItemColor)) // Valid color { // CSS for horizontal gradient from configured color to transparent QString css = QString("MinorFeedItem " "{ background-color: " "qlineargradient(spread:pad, " "x1:0, y1:0, x2:1, y2:0, " "stop:0 %1, stop:1 rgba(0, 0, 0, 0)); " "}") .arg(highlightItemColor); this->setStyleSheet(css); } else // If there's no valid color, highlight with a border { this->setFrameStyle(QFrame::Panel); } } } // Set the activity description with optional snippet QString activityDescription = activity->getContent(); // Add a snippet if configured to do so if (m_globalObject->getMinorFeedSnippetsType() != 3) // 3=Never { if (m_globalObject->getMinorFeedSnippetsType() == 2 // 2=Always || m_itemHighlightType != -1) // 0/1/2 and it's highlighted { if (m_globalObject->getMinorFeedSnippetsType() > 0 // Always or any highlighted || authorId != m_pumpController->currentUserId()) // or highlighted but not ours (0) { // FIXME 1.4.x: fix this spaghetti mess!! // TODO: option to not show when the object of the activity is ours // i.e. "JohnDoe liked a note", our note int snippetCharLimit; if (m_itemHighlightType == -1) { snippetCharLimit = m_globalObject->getSnippetsCharLimit(); } else { snippetCharLimit = m_globalObject->getSnippetsCharLimitHl(); } QString snippet = activity->generateSnippet(snippetCharLimit); if (!snippet.isEmpty()) { activityDescription.append(QStringLiteral(" 
              ") + snippet); } } } } m_activityDescriptionLabel->setText(activityDescription); //////////////////////////////////////////////////////// Layout m_leftLayout = new QVBoxLayout(); m_leftLayout->setAlignment(Qt::AlignTop); m_leftLayout->setContentsMargins(1, 0, 1, 0); m_leftLayout->setSpacing(1); m_leftLayout->addWidget(m_avatarButton, 0, Qt::AlignTop | Qt::AlignLeft); m_leftLayout->addStretch(0); // Original post available (inReplyTo) or object available (note, image...) if (!m_inReplyToMap.isEmpty() || (ASObject::canDisplayObject(activity->object()->getType()) && activity->object()->getDeletedTime().isEmpty()) ) { m_openButton = new QPushButton(QStringLiteral("+"), this); m_openButton->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum); m_openButton->setToolTip("" + tr("Open referenced post")); connect(m_openButton, &QAbstractButton::clicked, this, &MinorFeedItem::openOriginalPost); m_leftLayout->addWidget(m_openButton, 0, Qt::AlignHCenter); } m_rightLowerLayout = new QHBoxLayout(); m_rightLowerLayout->setContentsMargins(0, 0, 0, 0); m_rightLowerLayout->addWidget(m_activityDescriptionLabel, 1); // This may also contain an AvatarButton for an "object person" m_rightLayout = new QVBoxLayout(); m_rightLayout->setAlignment(Qt::AlignTop); m_rightLayout->addWidget(m_timestampLabel); m_rightLayout->addLayout(m_rightLowerLayout); // If the object is a person, such as someone following someone else, add an AvatarButton for them if (activity->object()->getType() == QStringLiteral("person")) { ASPerson *personObject = activity->personObject(); if (personObject->getId() != authorId) // Avoid cases like "JohnDoe updated JohnDoe" { m_haveTargetAvatar = true; m_targetAvatarButton = new AvatarButton(personObject, m_pumpController, m_globalObject, m_globalObject->getMfAvatarSize(), // 28,28 this); m_rightLowerLayout->addWidget(m_targetAvatarButton, 0, Qt::AlignBottom); } } m_topLayout = new QHBoxLayout(); m_topLayout->setContentsMargins(0, 0, 0, 0); int iconSpacing = 0; m_verbIconType = m_globalObject->getMfIconType(); if (m_verbIconType != 0) { m_verbIcons = MiscHelpers::iconsForActivity(activity->getVerb()); // Handle the different styles to prepare the paintEvent() m_verbIconSize = m_globalObject->getMfAvatarSize().width(); m_verbIconSizeSubtle = m_verbIconSize * 1.4; if (m_verbIconType == 1 || m_verbIconType == 3) { iconSpacing = m_verbIconSize; } else { iconSpacing = m_verbIconSizeSubtle / 2; } } if (m_itemIsOwn) // For our items, text first { m_topLayout->addLayout(m_rightLayout, 20); if (m_verbIconType < 3) // 1 and 2 are icon-first (and this is reversed) { m_topLayout->addLayout(m_leftLayout, 1); m_topLayout->addSpacing(iconSpacing); } else // 3 and 4 are avatar-first { m_topLayout->addSpacing(iconSpacing); m_topLayout->addLayout(m_leftLayout, 1); } } else // Normal item, not ours, layout with avatar/icon first { if (m_verbIconType < 3) // 1 and 2 are icon-first { m_topLayout->addSpacing(iconSpacing); m_topLayout->addLayout(m_leftLayout, 1); } else { m_topLayout->addLayout(m_leftLayout, 1); m_topLayout->addSpacing(iconSpacing); } m_topLayout->addLayout(m_rightLayout, 20); } m_mainLayout = new QVBoxLayout(); m_mainLayout->setContentsMargins(2, 1, 2, 4); m_mainLayout->addLayout(m_topLayout); if (highlightedByFilter) // Show which filtering rules caused this item to be highlighted { QVariantMap filterMatches = activity->getFilterMatches(); if (filterMatches.value("filterAction").toInt() == FilterChecker::Highlight) { m_filterMatchesWidget = new FilterMatchesWidget(filterMatches, this); m_mainLayout->addWidget(m_filterMatchesWidget); // FIXME -- TODO: make this widget optional } } this->setLayout(m_mainLayout); qDebug() << "MinorFeedItem created"; } MinorFeedItem::~MinorFeedItem() { qDebug() << "MinorFeedItem destroyed"; } /* * Pseudo-highlight for new items * */ void MinorFeedItem::setItemAsNew(bool isNew, bool informFeed) { m_itemIsNew = isNew; if (m_itemIsNew) { this->setAutoFillBackground(true); this->setBackgroundRole(QPalette::Mid); m_timestampLabel->setStyleSheet("QLabel " "{ background-color: " " qlineargradient(spread:pad," " x1:0, y1:0, x2:1, y2:0," " stop:0 palette(highlight)," " stop:0.2 palette(base)," " stop:0.8 palette(base)," " stop:1 palette(highlight));" "}" "QLabel:hover " "{ color: palette(highlighted-text); " " background-color: palette(highlight)" "}"); } else { this->setAutoFillBackground(false); this->setBackgroundRole(QPalette::Window); m_timestampLabel->setStyleSheet("QLabel:hover " "{ color: palette(highlighted-text); " " background-color: palette(highlight)" "}"); if (informFeed) { bool wasHighlighted = false; if (m_itemHighlightType != -1) { wasHighlighted = true; } emit itemRead(wasHighlighted); } } } bool MinorFeedItem::isNew() { return m_itemIsNew; } /* * Set/Update the fuzzy timestamp * * This will be called from time to time * */ void MinorFeedItem::setFuzzyTimeStamp() { m_timestampLabel->setText(Timestamp::fuzzyTime(m_createdAtTimestamp)); } void MinorFeedItem::syncAvatarFollowState() { m_avatarButton->syncFollowState(); if (m_haveTargetAvatar) { m_targetAvatarButton->syncFollowState(); } } int MinorFeedItem::getItemHighlightType() { return m_itemHighlightType; } QString MinorFeedItem::getActivityId() { return m_activityId; } /****************************************************************************/ /******************************** SLOTS *************************************/ /****************************************************************************/ void MinorFeedItem::openOriginalPost() { // Create a fake activity for the object QVariantMap originalPostMap; if (!m_inReplyToMap.isEmpty()) { originalPostMap.insert("object", m_inReplyToMap); originalPostMap.insert("actor", m_inReplyToMap.value("author") .toMap()); } else { originalPostMap.insert("object", m_originalObjectMap); originalPostMap.insert("actor", m_originalObjectMap.value("author") .toMap()); } ASActivity *originalPostActivity = new ASActivity(originalPostMap, m_pumpController->currentUserId(), this); Post *referencedPost = new Post(originalPostActivity, false, // Not highlighted true, // Post is standalone m_pumpController, m_globalObject, nullptr); // Make it an independent window referencedPost->show(); connect(m_pumpController, &PumpController::commentsReceived, referencedPost, &Post::setAllComments); referencedPost->requestCommenterComments(); } void MinorFeedItem::showUrlInfo(QString url) { if (!url.isEmpty()) { m_pumpController->showTransientMessage(url); qDebug() << "Link hovered in Minor Feed:" << url; } else { m_pumpController->showTransientMessage(QString()); } } /****************************************************************************/ /******************************* PROTECTED **********************************/ /****************************************************************************/ /* * On mouse click in any part of the item, set it as read * */ void MinorFeedItem::mousePressEvent(QMouseEvent *event) { if (m_itemIsNew) { this->setItemAsNew(false, // Mark as not new true); // Inform the feed // Avoid flickering of the "new" effect later m_timestampLabel->repaint(); } event->accept(); } /* * Ensure URL info in statusbar is hidden when the mouse leaves the item * */ void MinorFeedItem::leaveEvent(QEvent *event) { m_pumpController->showTransientMessage(QString()); event->accept(); } void MinorFeedItem::paintEvent(QPaintEvent *event) { if (m_verbIconType == 0 || m_verbIcons.size() < 2) { // Set not to show activity icons, or icon definitions are wrong event->ignore(); return; } //////////// FIXME - TODO -- This needs a lot of optimizing all around QIcon icon = QIcon::fromTheme(m_verbIcons.first(), QIcon(m_verbIcons.last())); bool subtle = false; if (m_verbIconType == 2 || m_verbIconType == 4) { subtle = true; } QPixmap iconPixmap; int iconOffset = 0; if (subtle) { iconPixmap = icon.pixmap(m_verbIconSizeSubtle, m_verbIconSizeSubtle); if (m_verbIconType == 4) // icon-after, subtle { iconOffset = m_verbIconSize / 2; // Based on original icon size } } else { iconPixmap = icon.pixmap(m_verbIconSize, m_verbIconSize); if (m_verbIconType == 3) // icon-after { iconOffset = m_verbIconSize + m_topLayout->spacing(); } } int iconPos; if (m_itemIsOwn) { iconPos = this->width() - iconPixmap.width() - iconOffset; } else { iconPos = 0 + iconOffset; } QPainter painter(this); if (subtle) { painter.setOpacity(0.3); } painter.drawPixmap(iconPos, 0, iconPixmap); event->accept(); } dianara-v1.4.1/src/mainwindow.h0000644000175000017500000002507213210073415014522 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include #include #include #include #include #include #ifdef QT_DBUS_LIB #include #include "dbusinterface.h" #endif #include #include #include "accountdialog.h" #include "configdialog.h" #include "pumpcontroller.h" #include "notifications.h" #include "publisher.h" #include "timeline.h" #include "post.h" #include "contactmanager.h" #include "minorfeed.h" #include "profileeditor.h" #include "filtereditor.h" #include "filterchecker.h" #include "logviewer.h" #include "helpwidget.h" #include "groupsmanager.h" #include "globalobject.h" #include "userposts.h" #include "firstrunwizard.h" #include "bannernotification.h" class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); ~MainWindow(); void prepareDataDirectory(); void createMenus(); void createToolbar(); virtual QMenu *createPopupMenu(); void createStatusbarWidgets(); enum StatusType { Initializing, Autoupdating, Stopped }; void setStateIcon(StatusType statusType); void createTrayIcon(); void setTrayIconPixmap(int newCount = 0, int highlightedCount = 0); void loadSettings(); void loadMainWindowConfig(); void saveSettings(); void enableIgnoringSslErrors(); void enableNoHttpsMode(); Post *findPostInTimelines(QString id, bool *ok); void loadPostsEverSeen(); void savePostsEverSeen(); public slots: void updateUserID(QString newUserID); void updateConfigSettings(); void toggleWidgetsByAuthorization(bool authorized); void onInitializationComplete(); void postInit(); void showErrorPopup(QString message); void trayControl(QSystemTrayIcon::ActivationReason reason); void showTrayFallbackMessage(QString title, QString message, int duration); void updateProfileData(QString avatarUrl, QString fullName, QString hometown, QString bio, QString eMail); void toggleAutoUpdates(bool checked); void onTimelineAutoupdate(); void updateAllTimelines(); void onUpdateRequestViaBanner(); void onUpdateDelayedViaBanner(); void updateMainAndMinorTimelines(); void updateMainActivityMinorTimelines(); void updateFavoritesTimeline(); void updateActionsFeed(); void scrollMainTimelineTo(QAbstractSlider::SliderAction sliderAction); void scrollDirectTimelineTo(QAbstractSlider::SliderAction sliderAction); void scrollActivityTimelineTo(QAbstractSlider::SliderAction sliderAction); void scrollFavoritesTimelineTo(QAbstractSlider::SliderAction sliderAction); void scrollMainTimelineToWidget(QWidget *widget); void scrollDirectTimelineToWidget(QWidget *widget); void scrollActivityTimelineToWidget(QWidget *widget); void scrollFavoritesTimelineToWidget(QWidget *widget); void scrollToNewPosts(); void notifyTimelineUpdate(PumpController::requestTypes timelineType, int newPostCount, int highlightCount, int directPostCount, int hlByFilterCount, int deletedPostCount, int filteredPostCount, int pendingPostCount, QString currentPage); void setTimelineTabTitle(PumpController::requestTypes timelineType, int newPostCount, int highlightCount, int totalFeedPostCount); void setTitleAndTrayInfo(int currentTab); void setMinorFeedTitle(int newItemsCount, int newHighlightedItemsCount); void setMentionsFeedTitle(int newItemsCount, int newHighlightedItemsCount); void notifyMinorFeedUpdate(PumpController::requestTypes feedType, int newItemsCount, int highlightedCount, int filteredCount, int pendingItemsCount); void storeAvatar(QByteArray avatarData, QString avatarUrl); void storeImage(QByteArray imageData, QString imageUrl); void setStatusBarMessage(QString message); void setTransientStatusMessage(QString message); void markAllAsRead(); void startPost(QString title="", QString content=""); void refreshAllTimestamps(); void adjustTimelineSizes(); void showUserTimeline(QString userId, QString userName, QIcon userAvatar, QString userOutbox); void toggleLockedPanels(bool locked); void toggleSidePanel(bool shown); void toggleToolbar(bool shown); void toggleStatusBar(bool shown); void toggleFullscreen(bool enabled); void toggleMeanwhileFeed(); void toggleMentionsFeed(); void toggleActionsFeed(); void showFirstRunWizard(); void visitWebSite(); void visitBugTracker(); void visitPumpGuide(); void visitTips(); void visitUserList(); void visitPumpStatus(); void aboutDianara(); void toggleMainWindow(bool firstTime=false); void onNotificationAction(uint id, QString action); void showAuthError(QString title, QString message); void onSessionManagerQuitRequest(QSessionManager &manager); void quitProgram(QString reason=""); protected: virtual void closeEvent(QCloseEvent *event); virtual void resizeEvent(QResizeEvent *event); private: ////////////////////////////////////// Menus QMenu *sessionMenu; QMenu *viewMenu; QMenu *settingsMenu; QMenu *helpMenu; QMenu *trayContextMenu; QAction *trayTitleSeparatorAction; QAction *trayShowWindowAction; QAction *sessionUpdateMainTimeline; QAction *sessionUpdateDirectTimeline; QAction *sessionUpdateActivityTimeline; QAction *sessionUpdateFavoritesTimeline; QAction *sessionUpdateMinorFeedMain; QAction *sessionUpdateMinorFeedDirect; QAction *sessionUpdateMinorFeedActivity; QAction *sessionAutoUpdates; QAction *sessionMarkAllAsRead; QAction *sessionPostNote; QAction *sessionQuit; QAction *viewLockPanels; QAction *viewSidePanel; QAction *viewToolbar; QAction *viewStatusBar; QAction *viewFullscreenAction; QAction *viewLogAction; QAction *settingsEditProfile; QAction *settingsAccount; QAction *settingsFilters; QAction *settingsConfigure; QAction *helpBasicHelp; QAction *helpShowWizard; QAction *helpVisitWebsite; QAction *helpVisitBugTracker; QAction *helpVisitPumpGuide; QAction *helpVisitPumpTips; QAction *helpVisitPumpUserList; QAction *helpVisitPumpStatus; QAction *helpAbout; ////////////////////////////////////// End menus // Info label for the menu QHBoxLayout *menuInfoLayout; QWidget *menuInfoWidget; QLabel *menuInfoLabel; // Toolbar QToolBar *mainToolBar; // Statusbar widgets QToolButton *statusStateButton; QToolButton *statusLogButton; QPushButton *statusAccountButton; // Shown when no account is configured bool statusAccountButtonUsed; QProgressBar *initializationProgressBar; QDockWidget *sideDockWidget; QWidget *sideDockTitleWidget; QWidget *leftSideWidget; QVBoxLayout *leftLayout; QHBoxLayout *leftTopLayout; QVBoxLayout *userInfoLayout; QToolBox *leftPanel; QAction *showMeanwhileFeed; QAction *showMentionsFeed; QAction *showActionsFeed; QWidget *rightSideWidget; QVBoxLayout *rightLayout; QTabWidget *tabWidget; int tabsPosition; bool tabsMovable; // User profile widgets QPushButton *avatarIconButton; QString avatarURL; QLabel *fullNameLabel; QLabel *userIdLabel; QLabel *userHometownLabel; QSystemTrayIcon *trayIcon; bool trayIconAvailable; int trayIconType; QPixmap trayCustomPixmap; int trayCurrentNewCount; int trayCurrentHLCount; GlobalObject *globalObject; AccountDialog *accountDialog; ProfileEditor *profileEditor; ConfigDialog *configDialog; FilterChecker *filterChecker; FilterEditor *filterEditor; LogViewer *logViewer; HelpWidget *helpWidget; PumpController *pumpController; FDNotifications *fdNotifier; MinorFeed *meanwhileFeed; MinorFeed *mentionsFeed; MinorFeed *actionsFeed; BannerNotification *bannerNotification; TimeLine *mainTimeline; QScrollArea *mainTimelineScrollArea; TimeLine *directTimeline; QScrollArea *directTimelineScrollArea; TimeLine *activityTimeline; QScrollArea *activityTimelineScrollArea; TimeLine *favoritesTimeline; QScrollArea *favoritesTimelineScrollArea; ContactManager *contactManager; Publisher *publisher; #ifdef QT_DBUS_LIB DBusInterface *dbusInterface; #endif bool firstRun; QString dataDirectory; // will have /images, /avatars, etc bool reallyQuitProgram; bool initializationComplete; int postIdsToStore; // Timer stuff int updateInterval; QTimer *updateTimer; QTimer *timestampsTimer; QTimer *delayedResizeTimer; QTimer *postInitTimer; QTimer *favoritesReloadTimer; QTimer *userDidSomethingTimer; QTimer *delayedScrollTimer; // Statusbar stuff QString oldStatusBarMessage; QString previousStatusFeedInfo; // user account-related data QString userID; }; #endif // MAINWINDOW_H dianara-v1.4.1/src/groupsmanager.h0000664000175000017500000000341413210564053015221 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 GROUPSMANAGER_H #define GROUPSMANAGER_H #include #include #include #include #include #include #include #include "pumpcontroller.h" class GroupsManager : public QWidget { Q_OBJECT public: explicit GroupsManager(PumpController *pumpController, QWidget *parent = 0); ~GroupsManager(); signals: public slots: void createGroup(); void deleteGroup(); void joinGroup(); void leaveGroup(); private: QVBoxLayout *m_layout; QLineEdit *m_newGroupNameLineEdit; QLineEdit *m_newGroupSummaryLineEdit; QLineEdit *m_newGroupDescLineEdit; QPushButton *m_createGroupButton; QLineEdit *m_joinLeaveGroupIdLineEdit; QPushButton *m_joinGroupButton; QPushButton *m_leaveGroupButton; QAction *m_closeAction; PumpController *m_pumpController; }; #endif // GROUPSMANAGER_H dianara-v1.4.1/src/asperson.cpp0000664000175000017500000001104113206623201014523 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "asperson.h" ASPerson::ASPerson(QVariantMap personMap, QObject *parent) : QObject(parent) { m_id = this->cleanupId(personMap.value("id").toString()); m_name = personMap.value("displayName").toString().trimmed(); m_avatar = personMap.value("image").toMap() .value("url").toString(); m_url = personMap.value("url").toString(); m_hometown = personMap.value("location").toMap() .value("displayName").toString().trimmed(); m_bio = personMap.value("summary").toString(); m_outboxLink = personMap.value("links").toMap() .value("activity-outbox").toMap() .value("href").toString(); m_followed = personMap.value("pump_io").toMap() .value("followed").toBool(); m_createdAt = personMap.value("published").toString(); m_updatedAt = personMap.value("updated").toString(); //qDebug() << "ASPerson created" << m_id; } ASPerson::~ASPerson() { //qDebug() << "ASPerson destroyed" << m_id; } void ASPerson::updateDataFromPerson(ASPerson *person) { m_id = person->getId(); m_name = person->getNameWithFallback(); m_avatar = person->getAvatarUrl(); m_url = person->getUrl(); m_hometown = person->getHometown(); m_bio = person->getBio(); m_outboxLink = person->getOutboxLink(); m_followed = person->isFollowed(); m_createdAt = person->getCreatedAt(); m_updatedAt = person->getupdatedAt(); } QString ASPerson::getId() { return m_id; } QString ASPerson::cleanupId(QString id) { if (id.startsWith(QStringLiteral("acct:"))) { id.remove(0, 5); } return id; } QString ASPerson::getName() { return m_name; } QString ASPerson::getNameWithFallback() { if (!m_name.isEmpty()) { return m_name; } else { return m_id; } } QString ASPerson::makeNameIdString(QString userName, QString userId) { QString nameAndId = userName.trimmed(); if (nameAndId.isEmpty()) { nameAndId = userId; } else { nameAndId.append(QStringLiteral(" (") + userId + QStringLiteral(")")); } return nameAndId; } QString ASPerson::getHometown() { return m_hometown; } QString ASPerson::getBio() { return m_bio; } QString ASPerson::getAvatarUrl() { return m_avatar; } QString ASPerson::getUrl() { return m_url; } QString ASPerson::getTooltipInfo() { if (m_id.isEmpty()) { return QString(); // If there's no ID, there's no valid person data } // FIXME: make these fields safe for HTML QString tooltipContents; if (!m_name.trimmed().isEmpty()) { tooltipContents.append("" + m_name.trimmed() + "
              "); } tooltipContents.append("" + m_id + ""); if (!m_hometown.isEmpty() || !m_bio.isEmpty()) { // More data coming, add a line tooltipContents.append(QStringLiteral("

              ")); } if (!m_hometown.isEmpty()) { tooltipContents.append("" + tr("Hometown") + ": " + m_hometown); tooltipContents.append(QStringLiteral("

              ")); } if (!m_bio.isEmpty()) { tooltipContents.append(m_bio); } tooltipContents.replace("\n", QStringLiteral("
              ")); // HTML newlines return tooltipContents; } QString ASPerson::getOutboxLink() { return m_outboxLink; } bool ASPerson::isFollowed() { return this->m_followed; } QString ASPerson::getCreatedAt() { return this->m_createdAt; } QString ASPerson::getupdatedAt() { return this->m_updatedAt; } dianara-v1.4.1/src/bannernotification.cpp0000664000175000017500000001106413202671461016561 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "bannernotification.h" BannerNotification::BannerNotification(QWidget *parent) : QWidget(parent) { /* * In the future, this widget might be used to notify different things. * * For now, it's only used to passively notify about pending autoupdates. * */ m_iconLabel = new QLabel(this); m_iconLabel->setPixmap(QIcon::fromTheme("dialog-information", // "clock", maybe? QIcon(":/images/feed-clock.png")) // FIXME: proper fallback .pixmap(32, 32)); m_descriptionLabel = new QLabel(tr("Timelines were not automatically " "updated to avoid interruptions."), this); m_descriptionLabel->setAlignment(Qt::AlignCenter); m_descriptionLabel->setWordWrap(true); // CSS for text color only; container widget has CSS for the background m_descriptionLabel->setStyleSheet("QLabel" "{ color: palette(highlighted-text); }"); m_descriptionLabel->setToolTip("" + tr("This happens when it is time to " "autoupdate the timelines, but you are " "not at the top of the first page, to " "avoid interruptions while you read")); m_okButton = new QPushButton(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), tr("Update now"), this); connect(m_okButton, &QAbstractButton::clicked, this, &BannerNotification::onOk); m_cancelButton = new QPushButton(QIcon::fromTheme("window-close", QIcon(":/images/button-close.png")), QString(), this); m_cancelButton->setToolTip("" + tr("Hide this message")); connect(m_cancelButton, &QAbstractButton::clicked, this, &BannerNotification::onCancel); m_containerLayout = new QHBoxLayout(); m_containerLayout->addWidget(m_iconLabel); m_containerLayout->addWidget(m_descriptionLabel, 1); m_containerLayout->addWidget(m_okButton); m_containerLayout->addWidget(m_cancelButton); this->m_containerWidget = new QWidget(this); m_containerWidget->setObjectName("BannerWidget"); m_containerWidget->setStyleSheet("QWidget#BannerWidget " "{ background-color: palette(highlight); " " color: palette(highlighted-text); " " padding: 2px; " " border-radius: 12px " "}"); m_containerWidget->setLayout(m_containerLayout); m_mainLayout = new QHBoxLayout(); m_mainLayout->setContentsMargins(0, 0, 0, 0); m_mainLayout->addWidget(m_containerWidget); this->setLayout(m_mainLayout); qDebug() << "BannerNotification created"; } BannerNotification::~BannerNotification() { qDebug() << "BannerNotification destroyed"; } /*****************************************************************************/ /*********************************** SLOTS ***********************************/ /*****************************************************************************/ void BannerNotification::onOk() { this->hide(); emit updateRequested(); } void BannerNotification::onCancel() { this->hide(); emit bannerCancelled(); } dianara-v1.4.1/src/accountdialog.h0000664000175000017500000000473313202674364015200 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include "pumpcontroller.h" class AccountDialog : public QWidget { Q_OBJECT public: AccountDialog(PumpController *pumpController, QWidget *parent = 0); ~AccountDialog(); void setLockMode(bool locked); signals: void userIdChanged(QString newUserId); public slots: void askForToken(); void setVerifierCode(); void onVerifierChanged(QString newText); void onAuthorizationSucceeded(); void showAuthorizationStatus(bool authorized); void showAuthorizationUrl(QUrl url, bool browserLaunched); void saveDetails(); void unlockDialog(); protected: virtual void hideEvent(QHideEvent *event); private: QVBoxLayout *m_mainLayout; QLabel *m_help1Label; QHBoxLayout *m_idLayout; QLabel *m_userIdIconLabel; QLabel *m_userIdLabel; QLineEdit *m_userIdLineEdit; QPushButton *m_getVerifierButton; QFrame *separatorLine; QLabel *m_help2Label; QHBoxLayout *m_verifierLayout; QLabel *m_verifierIconLabel; QLabel *m_verifierLabel; QLineEdit *m_verifierLineEdit; QPushButton *m_authorizeButton; QLabel *m_errorsLabel; QLabel *m_authorizationStatusLabel; QLabel *m_unlockExplanationLabel; QPushButton *m_unlockButton; QHBoxLayout *m_buttonsLayout; QPushButton *m_saveButton; QPushButton *m_cancelButton; PumpController *m_pumpController; }; #endif // ACCOUNT_H dianara-v1.4.1/src/publisher.h0000664000175000017500000001147513212021477014352 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include "composer.h" #include "pumpcontroller.h" #include "globalobject.h" #include "mischelpers.h" #include "audienceselector.h" #include "draftsmanager.h" class Publisher : public QWidget { Q_OBJECT public: explicit Publisher(PumpController *pumpController, GlobalObject *globalObject, QWidget *parent = 0); ~Publisher(); void syncFromConfig(); void setEmptyMediaData(); void setTitleAndContent(QString title, QString content); QVariantMap getAudienceMap(); void setAudienceFromMap(QVariantMap audienceMap); void setMediaModeWidgets(); void findMediaFile(); void toggleWidgetsWhileSending(bool widgetsEnabled); bool isFullMode(); bool emptyContents(); void reportUnreadableFile(QString filename, QFileInfo fileInfo); signals: public slots: void setMinimumMode(); void setFullMode(); void setPictureMode(); void setAudioMode(); void setVideoMode(); void setFileMode(); void onFileDropped(QString fileUrl); void cancelMediaMode(); void setEditingMode(QString postId, QString postType, QString postTitle, QString postText); void startMessageForContact(QString id, QString name, QString url); void addNickToRecipients(QString id, QString name, QString url, QString listType); void onDraftSelected(QString id, QString title, QString body, QString type, QString attachment, QVariantMap audience, int position); void onSaveDraftRequested(); void onCancelSavingDraftRequested(); void onPublishingOk(); void onPublishingFailed(); void onToPublicSelected(); void onToFollowersSelected(); void onCcPublicSelected(); void onCcFollowersSelected(); void updateAudienceToLabels(); void updateAudienceCcLabels(); void updateListsMenus(QVariantList listsList); void showHighlightedUrl(QString url); void sendPost(); void onSelectMediaFilePressed(); void updateProgressBar(qint64 sent, qint64 total); void updateCharacterCounter(); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_titleLayout; QGridLayout *m_mediaLayout; QGridLayout *m_buttonsLayout; QLabel *m_titleLabel; QLineEdit *m_titleLineEdit; QLabel *m_mediaInfoLabel; QPushButton *m_selectMediaButton; QPushButton *m_removeMediaButton; QLabel *m_pictureLabel; QProgressBar *m_uploadProgressBar; QPushButton *m_toolsButton; DraftsManager *m_draftsManager; QMenu *m_draftsMenu; QPushButton *m_draftsButton; Composer *m_composerBox; AudienceSelector *m_toAudienceSelector; QPushButton *m_toSelectorButton; QMenu *m_toSelectorMenu; QLabel *m_toAudienceLabel; AudienceSelector *m_ccAudienceSelector; QPushButton *m_ccSelectorButton; QMenu *m_ccSelectorMenu; QLabel *m_ccAudienceLabel; #ifdef GROUPSUPPORT QLabel *m_groupIdLabel; QLineEdit *m_groupIdLineEdit; #endif QPushButton *m_addMediaButton; QMenu *m_addMediaMenu; QAction *m_addMediaImageAction; QAction *m_addMediaAudioAction; QAction *m_addMediaVideoAction; QAction *m_addMediaFileAction; QLabel *m_charCounterLabel; QLabel *m_statusInfoLabel; QPushButton *m_postButton; QPushButton *m_cancelButton; bool m_onlyToFollowers; QString m_mediaFilename; QString m_mediaContentType; QString m_lastUsedDirectory; QString m_postType; bool m_editingMode; QString m_editingPostId; bool m_fullMode; QNetworkReply *m_uploadNetworkReply; PumpController *m_pumpController; GlobalObject *m_globalObject; }; #endif // PUBLISHER_H dianara-v1.4.1/src/peoplewidget.h0000664000175000017500000000524613206664715015057 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #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(); QStandardItem *createContactItem(ASPerson *contact); enum WidgetType { EmbeddedWidget, StandaloneWidget }; signals: void contactSelected(QIcon contactIcon, QString contactString, QString contactName, QString contactId, QString contactUrl); void addButtonPressed(QIcon contactIcon, QString contactString, QString contactName, QString contactId, QString contactUrl); public slots: void filterList(QString searchTerms); void updateAllContactsList(QString listType, QVariantList contactsVariantList, int totalReceivedCount); void addContact(ASPerson *contact); void removeContact(ASPerson *contact); void returnContact(); void returnClickedContact(QModelIndex modelIndex); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_buttonsLayout; QLabel *m_searchLabel; QLineEdit *m_searchLineEdit; QListView *m_allContactsListView; QStandardItemModel *m_itemModel; QSortFilterProxyModel *m_filterModel; QPushButton *m_addToButton; QPushButton *m_cancelButton; PumpController *m_pumpController; }; #endif // PEOPLEWIDGET_H dianara-v1.4.1/src/composer.h0000644000175000017500000000625213211612416014175 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 // Needed in Qt5 #include #include // For QCompleter's popup #include #include #include #include #include #include #include "globalobject.h" #include "mischelpers.h" class Composer : public QTextEdit { Q_OBJECT public: Composer(GlobalObject *globalObject, bool forPublisher, QWidget *parent = 0); ~Composer(); void erase(); void insertLink(QString url, QString title=""); void requestCompletion(QString partialNick); void hideInfoMessage(); QPushButton *getToolsButton(); void setPlainPasteEnabled(bool state); signals: void focusReceived(); void editingFinished(); void editingCancelled(); void cancelSavingDraftRequested(); void focusTitleRequested(); void nickInserted(QString id, QString name, QString url, QString listType); void fileDropped(QString fileUrl); void errorHappened(QString errorMessage); public slots: void makeNormal(); void makeBold(); void makeItalic(); void makeUnderline(); void makeStrikethrough(); void makeHeader(); void makeList(); void makeTable(); void makePreformatted(); void makeQuote(); void makeLink(); void insertImage(); void insertLine(); void insertSymbol(QAction *action); void pasteAsPlaintext(); void insertCompletedNick(QModelIndex nickData); void cancelPost(); protected: virtual void focusInEvent(QFocusEvent *event); virtual void dropEvent(QDropEvent *event); virtual void keyPressEvent(QKeyEvent *event); virtual void contextMenuEvent(QContextMenuEvent *event); virtual void insertFromMimeData(const QMimeData *source); private: QPushButton *m_toolsButton; QMenu *m_toolsMenu; QMenu *m_symbolsMenu; QAction *m_pastePlaintextAction; QMenu *m_customContextMenu; QCompleter *m_nickCompleter; QTableView *m_popupTableView; QString m_clickToPostString; bool m_forPublisher; GlobalObject *m_globalObject; }; #endif // COMPOSER_H dianara-v1.4.1/src/timestamp.cpp0000664000175000017500000001171413202664765014723 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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(Qt::DefaultLocaleLongDate); 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; // At this point, timeDifference is in SECONDS if (timeDifference < 60) // less than a minute, or FUTURE { if (timeDifference >= 0) { localTimeDateString = tr("Just now"); } else // negative difference, so timestamp is in the future { localTimeDateString = tr("In the future"); } } 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("Yesterday"); } else { localTimeDateString = tr("%1 days ago").arg(timeDifference); } } else { timeDifference /= 30; // convert to months, approx. 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.4.1/src/listsmanager.cpp0000664000175000017500000004137613204341771015407 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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) { m_pumpController = pumpController; connect(m_pumpController, &PumpController::personListReceived, this, &ListsManager::setPersonsInList); connect(m_pumpController, &PumpController::personAddedToList, this, &ListsManager::addPersonItemToList); connect(m_pumpController, &PumpController::personRemovedFromList, this, &ListsManager::removePersonItemFromList); // To show the current lists m_listsTreeWidget = new QTreeWidget(this); m_listsTreeWidget->setColumnCount(2); m_listsTreeWidget->setHeaderLabels(QStringList() << tr("Name") << tr("Members")); m_listsTreeWidget->setSortingEnabled(true); m_listsTreeWidget->sortByColumn(0, Qt::AscendingOrder); m_listsTreeWidget->setAlternatingRowColors(true); m_listsTreeWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); connect(m_listsTreeWidget, &QTreeWidget::itemSelectionChanged, this, &ListsManager::enableDisableDeleteButtons); // Buttons to add/remove people from lists, and the lists; Initially disabled m_addPersonButton = new QPushButton(QIcon::fromTheme("list-add-user", QIcon(":/images/list-add.png")), tr("Add Mem&ber"), this); m_addPersonButton->setDisabled(true); connect(m_addPersonButton, &QAbstractButton::clicked, this, &ListsManager::showAddPersonDialog); m_removePersonButton = new QPushButton(QIcon::fromTheme("list-remove-user", QIcon(":/images/list-remove.png")), tr("&Remove Member"), this); m_removePersonButton->setDisabled(true); connect(m_removePersonButton, &QAbstractButton::clicked, this, &ListsManager::removePerson); m_deleteListButton = new QPushButton(QIcon::fromTheme("edit-table-delete-row", QIcon(":/images/button-delete.png")), tr("&Delete Selected List"), this); m_deleteListButton->setDisabled(true); connect(m_deleteListButton, &QAbstractButton::clicked, this, &ListsManager::deleteList); // Groupbox for the "create new list" stuff m_newListGroupbox = new QGroupBox(tr("Add New &List"), this); m_newListNameLineEdit = new QLineEdit(this); m_newListNameLineEdit->setPlaceholderText(tr("Type a name for the new list...")); m_newListNameLineEdit->setClearButtonEnabled(true); connect(m_newListNameLineEdit, &QLineEdit::textChanged, this, &ListsManager::enableDisableCreateButton); connect(m_newListNameLineEdit, &QLineEdit::returnPressed, this, &ListsManager::createList); m_newListDescTextEdit = new QTextEdit(this); m_newListDescTextEdit->setPlaceholderText(tr("Type an optional description here")); m_newListDescTextEdit->setToolTip(m_newListDescTextEdit->placeholderText()); m_newListDescTextEdit->setTabChangesFocus(true); m_createListButton = new QPushButton(QIcon::fromTheme("edit-table-insert-row-above", QIcon(":/images/list-add.png")), tr("Create L&ist"), this); m_createListButton->setDisabled(true); // Disabled until user types a name connect(m_createListButton, &QAbstractButton::clicked, this, &ListsManager::createList); // Widget to find and select a contact m_peopleWidget = new PeopleWidget(tr("&Add to List"), PeopleWidget::StandaloneWidget, m_pumpController, this); connect(m_peopleWidget, &PeopleWidget::addButtonPressed, this, &ListsManager::addPerson); // Layout m_buttonsLayout = new QHBoxLayout(); m_buttonsLayout->addWidget(m_addPersonButton, 0, Qt::AlignLeft); m_buttonsLayout->addWidget(m_removePersonButton, 0, Qt::AlignLeft); m_buttonsLayout->addStretch(1); m_buttonsLayout->addWidget(m_deleteListButton, 0, Qt::AlignRight); m_groupboxLeftLayout = new QVBoxLayout(); m_groupboxLeftLayout->addWidget(m_newListNameLineEdit); m_groupboxLeftLayout->addWidget(m_newListDescTextEdit); m_groupboxMainLayout = new QHBoxLayout(); m_groupboxMainLayout->addLayout(m_groupboxLeftLayout); m_groupboxMainLayout->addWidget(m_createListButton, 0, Qt::AlignBottom); m_newListGroupbox->setLayout(m_groupboxMainLayout); m_mainLayout = new QVBoxLayout(); m_mainLayout->addWidget(m_listsTreeWidget, 5); m_mainLayout->addLayout(m_buttonsLayout, 1); m_mainLayout->addStretch(1); m_mainLayout->addWidget(m_newListGroupbox, 3); this->setLayout(m_mainLayout); qDebug() << "ListsManager created"; } ListsManager::~ListsManager() { qDebug() << "ListsManager destroyed"; } void ListsManager::setListsList(QVariantList listsList) { qDebug() << "Setting person lists contents"; // Disable to avoid users messing with it while it loads this->setDisabled(true); m_listsTreeWidget->clear(); m_personListsUrlList.clear(); QString listName; QString listDescription; QString listMembersCount; QVariant listId; QVariant listUrl; foreach (QVariant list, listsList) { QVariantMap listMap = list.toMap(); listName = listMap.value("displayName").toString(); listDescription = listMap.value("content").toString(); if (!listDescription.isEmpty()) { listDescription.prepend(""); // HTMLize it so it gets wordwrapped } listMembersCount = listMap.value("members").toMap() .value("totalItems").toString(); listId = listMap.value("id"); listUrl = listMap.value("members").toMap().value("url"); qDebug() << "list ID:" << listId.toString(); qDebug() << "list URL:" << listUrl.toString(); QTreeWidgetItem *listItem = new QTreeWidgetItem(); listItem->setText(0, listName); listItem->setToolTip(0, listDescription); listItem->setText(1, listMembersCount); listItem->setToolTip(1, listDescription); listItem->setData(0, Qt::UserRole, listId); listItem->setData(0, Qt::UserRole + 1, listUrl); m_listsTreeWidget->addTopLevelItem(listItem); QString membersUrl = listMap.value("members").toMap() .value("url").toString(); m_personListsUrlList.append(membersUrl); } // Get list of people in each list foreach (QString url, m_personListsUrlList) { m_pumpController->getPersonList(url); } // m_listsTreeWidget->expandAll(); m_listsTreeWidget->resizeColumnToContents(0); // Re-enable this->setEnabled(true); } QTreeWidgetItem *ListsManager::createPersonItem(QString id, QString name, QString avatarFile) { QTreeWidgetItem *personItem = new QTreeWidgetItem(); avatarFile = MiscHelpers::getCachedAvatarFilename(avatarFile); QPixmap avatarPixmap = QPixmap(avatarFile); if (avatarPixmap.isNull()) { personItem->setIcon(0, QIcon::fromTheme("user-identity", QIcon(":/images/no-avatar.png"))); } else { personItem->setIcon(0, QIcon(avatarPixmap)); } personItem->setText(0, name); personItem->setText(1, id); return personItem; } /****************************************************************************/ /********************************* 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; // Find out which TreeWidgetItem matches this list QTreeWidgetItem *listItem = 0; // Avoid 'not initialized' warning foreach (QTreeWidgetItem *item, m_listsTreeWidget->findItems(QString(), Qt::MatchContains)) { listItem = item; // Data in UserRole+1 is the Members Url if (listItem->data(0, Qt::UserRole + 1).toString() == listUrl) { break; } } foreach (QVariant personVariant, personList) { // FIXME: this should use ASPerson() QVariantMap personMap = personVariant.toMap(); QString id = ASPerson::cleanupId(personMap.value("id").toString()); QString name = personMap.value("displayName").toString(); QString avatar = personMap.value("image").toMap().value("url").toString(); if (listItem != 0) // Ensure it's initialized! { QTreeWidgetItem *childItem = this->createPersonItem(id, name, avatar); listItem->addChild(childItem); } } } void ListsManager::createList() { QString listName = m_newListNameLineEdit->text().trimmed(); QString listDescription = m_newListDescTextEdit->toPlainText().trimmed(); listDescription.replace("\n", "
              "); if (!listName.isEmpty()) { qDebug() << "Creating list:" << listName; m_pumpController->createPersonList(listName, listDescription); m_newListNameLineEdit->clear(); m_newListDescTextEdit->clear(); } else { qDebug() << "Error: List name is empty!"; } } void ListsManager::deleteList() { if (m_listsTreeWidget->currentItem() == 0) // if nothing selected, so NULL... { return; } QString listName = m_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"), QString(), 1, 1); if (confirmation == 0) { // Avoid the possibility of deleting twice! m_deleteListButton->setDisabled(true); QString listId = m_listsTreeWidget->currentItem()->data(0, Qt::UserRole) .toString(); m_pumpController->deletePersonList(listId); } else { qDebug() << "Confirmation cancelled, not deleting the list"; } } /* * Enable or disable the "Create List" button * */ void ListsManager::enableDisableCreateButton(QString listName) { if (listName.trimmed().isEmpty()) { m_createListButton->setDisabled(true); } else { m_createListButton->setEnabled(true); } } /* * Enable or disable the "remove person" and "delete list" buttons * according to what's currently selected * */ void ListsManager::enableDisableDeleteButtons() { // A list is selected if (m_listsTreeWidget->currentItem()->parent() == 0) { m_deleteListButton->setEnabled(true); m_removePersonButton->setDisabled(true); } else // One of the items inside a list is selected { m_deleteListButton->setDisabled(true); m_removePersonButton->setEnabled(true); } // Either way... m_addPersonButton->setEnabled(true); } void ListsManager::showAddPersonDialog() { if (m_listsTreeWidget->currentItem() == 0) // No list selected { return; } // Show the people widget, which will return the selected contact in a SIGNAL m_peopleWidget->resize(520, this->height()); m_peopleWidget->resetWidget(); m_peopleWidget->show(); } void ListsManager::addPerson(QIcon icon, QString contactString, QString contactName, QString contactId, QString contactUrl) { Q_UNUSED(icon) Q_UNUSED(contactString) Q_UNUSED(contactName) Q_UNUSED(contactUrl) m_peopleWidget->hide(); if (contactId.isEmpty()) { return; } QString listId; if (m_listsTreeWidget->currentItem()->parent() == 0) // If a list is selected { listId = m_listsTreeWidget->currentItem()->data(0, Qt::UserRole) .toString(); } else // If a member is selected, get its matching list { listId = m_listsTreeWidget->currentItem()->parent()->data(0, Qt::UserRole) .toString(); } this->setDisabled(true); m_pumpController->addPersonToList(listId, "acct:" + contactId); } void ListsManager::removePerson() { if (m_listsTreeWidget->currentItem() == 0) // Nothing selected, so it's NULL { return; } QString personId = m_listsTreeWidget->currentItem()->text(1); QString personName = m_listsTreeWidget->currentItem()->text(0); if (personName.isEmpty()) { personName = personId; } QString listId; QString listName; if (m_listsTreeWidget->currentItem()->parent() != 0) // Ensure it's not a list { listId = m_listsTreeWidget->currentItem()->parent()->data(0, Qt::UserRole) .toString(); listName = m_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"), QString(), 1, 1); if (confirmation == 0) { this->setDisabled(true); m_pumpController->removePersonFromList(listId, QStringLiteral("acct:") + personId); } else { qDebug() << "Confirmation cancelled, 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, QString avatarUrl) { QTreeWidgetItem *newItem = this->createPersonItem(personId, personName, avatarUrl); QTreeWidgetItem *selectedItem = m_listsTreeWidget->currentItem(); // When the list itself is NOT selected, but a child, point to the parent if (selectedItem->parent() != 0) { selectedItem = selectedItem->parent(); } selectedItem->addChild(newItem); // Update member counter selectedItem->setText(1, QString::number(selectedItem->childCount())); this->setEnabled(true); } void ListsManager::removePersonItemFromList(QString personId) { if (personId == m_listsTreeWidget->currentItem()->text(1)) { QTreeWidgetItem *parentList = m_listsTreeWidget->currentItem()->parent(); delete m_listsTreeWidget->currentItem(); // Update member counter parentList->setText(1, QString::number(parentList->childCount())); } this->setEnabled(true); } dianara-v1.4.1/src/pageselector.h0000664000175000017500000000407413206632327015034 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 PAGESELECTOR_H #define PAGESELECTOR_H #include #include #include #include #include #include #include #include #include #include class PageSelector : public QWidget { Q_OBJECT public: explicit PageSelector(QWidget *parent = 0); ~PageSelector(); void showForPage(int currentPage, int totalPageCount); signals: void pageJumpRequested(int pageNumber); public slots: void setToFirstPage(); void setToLastPage(); void decreasePage(); void increasePage(); void goToPage(); void onPageNumberEntered(); void onPageNumberChanged(int selectedPage); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_topLayout; QHBoxLayout *m_middleLayout; QHBoxLayout *m_bottomLayout; QLabel *m_messageLabel; QSpinBox *m_pageNumberSpinbox; QLabel *m_rangeLabel; QPushButton *m_firstButton; QPushButton *m_lastButton; QPushButton *m_newerButton; QSlider *m_pageNumberSlider; QPushButton *m_olderButton; QPushButton *m_goButton; QPushButton *m_closeButton; QAction *m_closeAction; int m_initialPage; }; #endif // PAGESELECTOR_H dianara-v1.4.1/src/contactcard.cpp0000644000175000017500000003023613207641631015173 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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, GlobalObject *globalObject, ASPerson *asPerson, QWidget *parent) : QFrame(parent) { m_pumpController = pumpController; m_globalObject = globalObject; this->setFrameStyle(QFrame::Box | QFrame::Raised); this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); // Left widget, user avatar m_avatarLabel = new QLabel(this); // FIXME -- Use an AvatarButton? m_contactAvatarUrl = asPerson->getAvatarUrl(); // Get local file name, which is stored in base64 hash form QString avatarFilename = MiscHelpers::getCachedAvatarFilename(m_contactAvatarUrl); // Load avatar if already cached const bool validAvatar = this->setAvatar(avatarFilename); if (!validAvatar && !m_contactAvatarUrl.isEmpty()) { connect(m_pumpController, &PumpController::avatarStored, this, &ContactCard::redrawAvatar); // Download avatar for next time m_pumpController->enqueueAvatarForDownload(m_contactAvatarUrl); } // Center widget, user info QFont nameFont; nameFont.setBold(true); nameFont.setUnderline(true); m_contactName = asPerson->getName(); // WithFallback()? FIXME m_contactId = asPerson->getId(); m_contactUrl = asPerson->getUrl(); m_contactOutbox = asPerson->getOutboxLink(); const bool cardIsOwn = (m_contactId == m_pumpController->currentUserId()); QString userInfoString = QString("%1
              ").arg(m_contactName); userInfoString.append(QString("%1
              ").arg(m_contactId)); userInfoString.append(QStringLiteral("") + tr("Hometown") + QString(": %1").arg(asPerson->getHometown()) + QStringLiteral("")); QString userCreationDate = asPerson->getCreatedAt(); if (!userCreationDate.isEmpty()) { userInfoString.append(QStringLiteral("
              ") + tr("Joined: %1") .arg(Timestamp::fuzzyTime(userCreationDate)) + QStringLiteral("")); } else { userCreationDate = asPerson->getupdatedAt(); userInfoString.append(QStringLiteral("
              ") + tr("Updated: %1") .arg(Timestamp::fuzzyTime(userCreationDate)) + QStringLiteral("")); } m_userInfoLabel = new QLabel(this); m_userInfoLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); m_userInfoLabel->setText(userInfoString); m_userInfoLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); m_userInfoLabel->setWordWrap(true); m_userInfoLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::MinimumExpanding); // Bio as tooltip for the whole contact card QString contactBio = asPerson->getBio().replace("\n", QStringLiteral("
              ")); if (!contactBio.isEmpty()) { this->setToolTip("" // Make it rich text, so that it gets wordwrapped + tr("Bio for %1", "Abbreviation for Biography, " "but you can use the full word; " "%1=contact name") .arg(m_contactName) + "" "

              " + contactBio); } else { if (m_contactName.isEmpty()) { this->setToolTip(QStringLiteral("") + tr("This user doesn't have a biography")); } else { this->setToolTip(QStringLiteral("") + tr("No biography for %1", "%1=contact name").arg(m_contactName)); } } // Right column, buttons m_followButton = new QPushButton(this); m_followButton->setFlat(true); if (asPerson->isFollowed()) { this->setButtonToUnfollow(); } else { if (!cardIsOwn) { this->setButtonToFollow(); } else // Ourselves! { m_followButton->setText(QString()); m_followButton->setDisabled(true); } } m_openProfileAction = new QAction(QIcon::fromTheme("internet-web-browser", QIcon(":/images/button-download.png")), tr("Open Profile in Web Browser"), this); connect(m_openProfileAction, &QAction::triggered, this, &ContactCard::openProfileInBrowser); if (m_contactUrl.isEmpty()) // Disable if there is no URL { m_openProfileAction->setDisabled(true); } m_sendMessageAction = new QAction(QIcon::fromTheme("document-edit", QIcon(":/images/button-edit.png")), tr("Send Message"), this); m_sendMessageAction->setDisabled(cardIsOwn); connect(m_sendMessageAction, &QAction::triggered, this, &ContactCard::setMessagingModeForContact); m_browsePostsAction = new QAction(QIcon::fromTheme("edit-find", QIcon(":/images/menu-find.png")), tr("Browse Messages"), this); connect(m_browsePostsAction, &QAction::triggered, this, &ContactCard::browseContactPosts); /* m_addToListMenu = new QMenu(tr("In Lists..."), this); m_addToListMenu->setIcon(QIcon::fromTheme("format-list-unordered")); m_addToListMenu->addAction("fake list 1")->setCheckable(true); // FIXME... m_addToListMenu->addAction("fake list 2")->setCheckable(true); m_addToListMenu->addAction("fake list 3")->setCheckable(true); */ m_optionsMenu = new QMenu(QStringLiteral("*user-options*"), this); m_optionsMenu->addAction(m_openProfileAction); m_optionsMenu->addAction(m_sendMessageAction); m_optionsMenu->addAction(m_browsePostsAction); m_browsePostsAction->setEnabled(m_pumpController->urlIsInOurHost(m_contactOutbox)); //m_optionsMenu->addMenu(m_addToListMenu); // Don't include it for now -- FIXME m_optionsButton = new QPushButton(QIcon::fromTheme("user-properties", QIcon(":/images/no-avatar.png")), tr("User Options"), this); m_optionsButton->setFlat(true); m_optionsButton->setMenu(m_optionsMenu); // Layout m_rightLayout = new QVBoxLayout(); m_rightLayout->setAlignment(Qt::AlignTop); m_rightLayout->addWidget(m_followButton); m_rightLayout->addWidget(m_optionsButton); m_mainLayout = new QHBoxLayout(); m_mainLayout->addWidget(m_avatarLabel, 0, Qt::AlignTop); m_mainLayout->addWidget(m_userInfoLabel, 1); m_mainLayout->addLayout(m_rightLayout); this->setLayout(m_mainLayout); qDebug() << "ContactCard created" << m_contactId; } ContactCard::~ContactCard() { qDebug() << "ContactCard destroyed" << m_contactId; } void ContactCard::setButtonToFollow() { m_followButton->setIcon(QIcon::fromTheme("list-add", QIcon(":/images/list-add.png"))); m_followButton->setText(tr("Follow")); connect(m_followButton, &QAbstractButton::clicked, this, &ContactCard::followContact); disconnect(m_followButton, &QAbstractButton::clicked, this, &ContactCard::unfollowContact); } void ContactCard::setButtonToUnfollow() { m_followButton->setIcon(QIcon::fromTheme("list-remove", QIcon(":/images/list-remove.png"))); m_followButton->setText(tr("Stop Following")); connect(m_followButton, &QAbstractButton::clicked, this, &ContactCard::unfollowContact); disconnect(m_followButton, &QAbstractButton::clicked, this, &ContactCard::followContact); } bool ContactCard::setAvatar(QString avatarFilename) { bool validAvatar = false; QPixmap avatarPixmap = QPixmap(avatarFilename) .scaledToWidth(64, Qt::SmoothTransformation); if (!avatarPixmap.isNull()) { m_avatarLabel->setPixmap(avatarPixmap); validAvatar = true; } if (!validAvatar) { // Placeholder image m_avatarLabel->setPixmap(QIcon::fromTheme("user-identity", QIcon(":/images/no-avatar.png")) .pixmap(64, 64)); } return validAvatar; } QString ContactCard::getNameAndIdString() { return QString("%1 <%2>").arg(m_contactName, m_contactId); } QString ContactCard::getId() { return m_contactId; } /**************************************************************************/ /******************************** SLOTS ***********************************/ /**************************************************************************/ void ContactCard::followContact() { m_pumpController->followContact(m_contactId); this->setButtonToUnfollow(); // FIXME: should wait for proper signal } void ContactCard::unfollowContact() { const QString nameWithId = ASPerson::makeNameIdString(m_contactName, m_contactId); int confirmation = QMessageBox::question(this, tr("Stop following?"), tr("Are you sure you want to " "stop following %1?") .arg(nameWithId), tr("&Yes, stop following"), tr("&No"), QString(), 1, 1); if (confirmation == 0) { m_pumpController->unfollowContact(m_contactId); this->setButtonToFollow(); // FIXME: should wait for proper signal -- TODO } } void ContactCard::openProfileInBrowser() { MiscHelpers::openUrl(m_contactUrl, this); } void ContactCard::setMessagingModeForContact() { m_globalObject->createMessageForContact(m_contactId, m_contactName, m_contactUrl); } void ContactCard::browseContactPosts() { const QPixmap contactPixmap(m_avatarLabel->pixmap()->copy()); m_globalObject->browseUserMessages(m_contactId, m_contactName, QIcon(contactPixmap), m_contactOutbox + "/major"); // TMP! FIXME } /* * Redraw contact's avatar after it's been downloaded and stored * */ void ContactCard::redrawAvatar(QString avatarUrl, QString avatarFilename) { if (avatarUrl == m_contactAvatarUrl) { disconnect(m_pumpController, &PumpController::avatarStored, this, &ContactCard::redrawAvatar); this->setAvatar(avatarFilename); } } dianara-v1.4.1/src/contactmanager.cpp0000644000175000017500000004775713216304114015704 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "contactmanager.h" ContactManager::ContactManager(PumpController *pumpController, GlobalObject *globalObject, QWidget *parent) : QWidget(parent) { m_pumpController = pumpController; m_globalObject = globalObject; m_followingManually = false; // After receiving a contact list, update it connect(m_pumpController, &PumpController::contactListReceived, this, &ContactManager::setContactListsContents); // After receiving the list of lists, update it connect(m_pumpController, &PumpController::listsListReceived, this, &ContactManager::setListsListContents); // After having checked if a webfinger ID is valid, follow it (or not) connect(m_pumpController, &PumpController::contactVerified, this, &ContactManager::followContact); // If a person can't be followed at the moment, re-enable widgets to try again connect(m_pumpController, &PumpController::cannotFollowNow, this, &ContactManager::onCantFollowNow); m_mainLayout = new QVBoxLayout(); m_mainLayout->setAlignment(Qt::AlignTop); QString webfingerHelpMessage = tr("username@server.org or " "https://server.org/username"); m_topLayout = new QHBoxLayout(); m_enterAddressLabel = new QLabel(tr("&Enter address to follow:"), this); m_enterAddressLabel->setToolTip("" + webfingerHelpMessage); // HTML tags for wordwrap // Ensure it will get focus first, before addressLineEdit m_enterAddressLabel->setFocusPolicy(Qt::StrongFocus); m_addressLineEdit = new QLineEdit(this); m_addressLineEdit->setPlaceholderText(webfingerHelpMessage); m_addressLineEdit->setToolTip("" + webfingerHelpMessage); // HTML tags to get wordwrap m_addressLineEdit->setClearButtonEnabled(true); connect(m_addressLineEdit, &QLineEdit::textChanged, this, &ContactManager::toggleFollowButton); connect(m_addressLineEdit, &QLineEdit::returnPressed, this, &ContactManager::validateContactId); m_enterAddressLabel->setBuddy(m_addressLineEdit); m_followButton = new QPushButton(QIcon::fromTheme("list-add-user", QIcon(":/images/list-add.png")), tr("&Follow"), this); m_followButton->setDisabled(true); // Disabled until an address is typed connect(m_followButton, &QAbstractButton::clicked, this, &ContactManager::validateContactId); m_topLayout->addWidget(m_enterAddressLabel); m_topLayout->addWidget(m_addressLineEdit); m_topLayout->addWidget(m_followButton); m_mainLayout->addLayout(m_topLayout); m_mainLayout->addSpacing(4); // Widgets for list of 'following' and 'followers' m_followingWidget = new ContactList(m_pumpController, m_globalObject, QStringLiteral("following"), this); connect(m_pumpController, &PumpController::contactFollowed, m_followingWidget, &ContactList::addSingleContact); connect(m_pumpController, &PumpController::contactUnfollowed, m_followingWidget, &ContactList::removeSingleContact); connect(m_followingWidget, &ContactList::contactCountChanged, this, &ContactManager::changeFollowingCount); m_followersWidget = new ContactList(m_pumpController, m_globalObject, QStringLiteral("followers"), this); // Widget for the list of 'person lists' m_listsManager = new ListsManager(m_pumpController, this); m_listsScrollArea = new QScrollArea(this); m_listsScrollArea->setWidget(m_listsManager); m_listsScrollArea->setWidgetResizable(true); m_listsScrollArea->setFrameStyle(QFrame::NoFrame); // Widget for the list of site users; users from your own server m_siteUsersList = new SiteUsersList(m_pumpController, m_globalObject, this); // Options menu m_optionsMenu = new QMenu(QStringLiteral("*options-menu*"), this); m_optionsMenu->addAction(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), tr("Reload Followers"), this, SLOT(refreshFollowers())); m_optionsMenu->addAction(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), tr("Reload Following"), this, SLOT(refreshFollowing())); m_optionsMenu->addSeparator(); m_optionsMenu->addAction(QIcon::fromTheme("document-export", QIcon(":/images/button-download.png")), tr("Export Followers"), this, SLOT(exportFollowers())); m_optionsMenu->addAction(QIcon::fromTheme("document-export", QIcon(":/images/button-download.png")), tr("Export Following"), this, SLOT(exportFollowing())); m_optionsMenu->addSeparator(); m_optionsMenu->addAction(QIcon::fromTheme("view-refresh", QIcon(":/images/menu-refresh.png")), tr("Reload Lists"), this, SLOT(refreshPersonLists())); m_optionsButton = new QPushButton(QIcon::fromTheme("configure", QIcon(":/images/button-configure.png")), QString(), this); m_optionsButton->setMenu(m_optionsMenu); m_tabWidget = new QTabWidget(this); m_tabWidget->addTab(m_followersWidget, QIcon::fromTheme("meeting-observer", QIcon(":/images/no-avatar.png")), QString()); m_tabWidget->addTab(m_followingWidget, QIcon::fromTheme("meeting-participant", QIcon(":/images/no-avatar.png")), QString()); m_tabWidget->addTab(m_listsScrollArea, QIcon::fromTheme("preferences-contact-list", QIcon(":/images/button-edit.png")), QString()); m_tabWidget->addTab(m_siteUsersList, QIcon::fromTheme("system-users", QIcon(":/images/button-users.png")), tr("&Neighbors")); m_tabWidget->setCornerWidget(m_optionsButton); m_followersCount = 0; m_followingCount = 0; m_listsCount = 0; this->setTabLabels(); m_mainLayout->addWidget(m_tabWidget); this->setLayout(m_mainLayout); qDebug() << "Contact manager created"; } ContactManager::~ContactManager() { qDebug() << "Contact manager destroyed"; } void ContactManager::setTabLabels() { m_tabWidget->setTabText(0, tr("Follo&wers") + QString(" (%1)") .arg(QLocale::system() .toString(m_followersCount))); m_tabWidget->setTabText(1, tr("Followin&g") + QString(" (%1)") .arg(QLocale::system() .toString(m_followingCount))); m_tabWidget->setTabText(2, tr("&Lists") + QString(" (%1)").arg(m_listsCount)); // Not worth localizing } /* * Write the list of contacts (following or followers) * to a file selected by the user * */ void ContactManager::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-" + m_pumpController->currentUsername() + "-" + listType; QString filename = QFileDialog::getSaveFileName(this, dialogTitle, QDir::homePath() + "/" + suggestedFilename, QString()).trimmed(); if (filename.isEmpty()) // If dialog was cancelled, do nothing { return; } qDebug() << "Exporting to:" << filename; QFile exportFile(filename); if (exportFile.open(QIODevice::WriteOnly)) { if (listType == "following") { exportFile.write(m_followingWidget->getContactsStringForExport() .toLocal8Bit()); } else // "followers" { exportFile.write(m_followersWidget->getContactsStringForExport() .toLocal8Bit()); } exportFile.close(); } else { QMessageBox::critical(this, tr("Error"), tr("Cannot export to this file:") + "\n" + filename + "\n\n" + tr("Please enter another file name, " "or choose a different folder.")); qDebug() << "Cannot write to that file!"; } } void ContactManager::enableManualFollowWidgets() { m_addressLineEdit->setEnabled(true); m_followButton->setEnabled(!m_addressLineEdit->text().isEmpty()); } /*****************************************************************************/ /*********************************** SLOTS ***********************************/ /*****************************************************************************/ void ContactManager::setContactListsContents(QString listType, QVariantList contactList, int totalReceivedCount) { qDebug() << "ContactManager; Setting contact list contents"; if (listType == "following") { if (totalReceivedCount <= 200) // Only for the first batch { m_followingWidget->clearListContents(); m_followingCount = 0; } m_followingWidget->setListContents(contactList); if (totalReceivedCount < m_pumpController->currentFollowingCount()) { m_pumpController->getContactList(listType, totalReceivedCount); } m_followingCount += contactList.size(); } else { if (totalReceivedCount <= 200) { m_followersWidget->clearListContents(); m_followersCount = 0; } m_followersWidget->setListContents(contactList); if (totalReceivedCount < m_pumpController->currentFollowersCount()) { m_pumpController->getContactList(listType, totalReceivedCount); } m_followersCount += contactList.size(); } // Update tab labels with number of following or followers, which were updated before this->setTabLabels(); } /* * Fill the list of lists * */ void ContactManager::setListsListContents(QVariantList listsList) { m_listsCount = listsList.count(); // Update tab labels with number of following or followers, which were updated before this->setTabLabels(); m_listsManager->setListsList(listsList); } void ContactManager::changeFollowingCount(int difference) { m_followingCount += difference; // FIXME: pumpController should be notified this->setTabLabels(); // Set "Following" tab as active, only when the contact manager is not visible // (following someone from an AvatarButton menu) if (!this->isVisible()) { m_tabWidget->setCurrentIndex(1); } } /* * Ask for the updated list of Following * */ void ContactManager::refreshFollowing() { qDebug() << "Refreshing list of Following..."; m_pumpController->getContactList("following"); } /* * Ask for the updated list of followers * */ void ContactManager::refreshFollowers() { qDebug() << "Refreshing list of Followers..."; m_pumpController->getContactList("followers"); } /* * Export list of "Following" to a text file * */ void ContactManager::exportFollowing() { qDebug() << "Exporting Following..."; exportContactsToFile("following"); } /* * Export list of "Followers" to a text file * */ void ContactManager::exportFollowers() { qDebug() << "Exporting Followers..."; exportContactsToFile("followers"); } void ContactManager::refreshPersonLists() { qDebug() << "Refreshing list of person lists..."; m_pumpController->getListsList(); } /* * Enable or disable Follow button * */ void ContactManager::toggleFollowButton(QString currentAddress) { if (currentAddress.isEmpty()) { m_followButton->setDisabled(true); } else { m_followButton->setEnabled(true); } } /* * 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. * * First step is checking, via Webfinger, if the ID exists and its server is up. * */ void ContactManager::validateContactId() { QString address = m_addressLineEdit->text().trimmed(); bool validAddress = false; qDebug() << "ContactManager::followContact(); Address entered:" << address; // First, if the address is in URL form, convert it 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); // .at(2) could also have stuff, so don't use .first() and .last() } } // Then, check that the address actually matches something@somewhere.tld if (address.contains(QRegExp(".+@.+\\..+"))) { validAddress = true; } else { qDebug() << "Invalid webfinger address!"; m_addressLineEdit->setFocus(); } if (validAddress) { m_followingManually = true; m_followingAddress = address; m_tabWidget->setFocus(); // Take focus away from the widgets that will be disabled now m_addressLineEdit->setDisabled(true); m_followButton->setDisabled(true); m_pumpController->followContact(address); } } void ContactManager::followContact(QString userId, int httpCode, bool requestTimedOut, QString serverVersion) { if (httpCode != 200) { QString hostname = userId.split('@').last(); // FIXME/TMP: Unreliable, future/alternate implementations might not use this bool isPumpServer = serverVersion.contains("pump.io"); QString specificError; if (httpCode == 404) { if (isPumpServer) { specificError = tr("The server seems to be a Pump server, " "but the account does not exist."); } else { specificError = tr("%1 doesn't seem to be a Pump server.", "%1 is a hostname") .arg(hostname); } } else { // Really only error 0 would mean unavailable server // Any other error code probably means the server is not a Pump server specificError = "••• " + tr("Following this account at this time " "will probably not work.") + ""; } // Timeout was reached or general connection error if (requestTimedOut || httpCode == 0) { specificError.append(" " + tr("The %1 server seems unavailable.", "%1 is a hostname") .arg(hostname) + ""); } if (serverVersion.isEmpty()) { serverVersion = tr("Unknown", "Refers to server version"); } int choice = QMessageBox::warning(this, tr("Error"), "" + tr("The user address %1 does not exist, or the " "%2 server is down.") .arg("" + userId + "") .arg("" + hostname + "") + "" "


              " + specificError + "

              " + tr("Server version") + ": " + serverVersion + "" "

              " + tr("Check the address, and keep in mind that " "usernames are case-sensitive.") + "



              " + tr("Do you want to try following this address anyway?") + "
              " + tr("(not recommended)") + "
              " "
              ", tr("Yes, follow anyway"), tr("No, cancel"), QString(), 1, 1); if (choice == 1) { if (m_followingManually && userId == m_followingAddress) { this->enableManualFollowWidgets(); m_addressLineEdit->setFocus(); // Re-focus to try again, maybe m_followingManually = false; m_followingAddress.clear(); } return; } } qDebug() << "About to follow this address:" << userId; m_globalObject->setStatusMessage(tr("About to follow %1...") .arg("'" + userId + "'")); m_pumpController->followVerifiedContact(userId); if (m_followingManually && userId == m_followingAddress) { enableManualFollowWidgets(); m_addressLineEdit->clear(); // Which will disable Follow button again m_followingManually = false; m_followingAddress.clear(); // Activate 'Following' tab m_tabWidget->setFocus(); m_tabWidget->setCurrentIndex(1); } } void ContactManager::onCantFollowNow(QString userId) { if (m_followingManually && userId == m_followingAddress) { enableManualFollowWidgets(); m_addressLineEdit->setFocus(); } } dianara-v1.4.1/src/contactlist.cpp0000644000175000017500000003017713210110176015226 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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, GlobalObject *globalObject, QString listType, QWidget *parent) : QWidget(parent) { m_pumpController = pumpController; m_globalObject = globalObject; // Need action defined before its target lineedit, to show localized shortcut m_focusFilterAction = new QAction(this); m_focusFilterAction->setShortcut(QKeySequence("Ctrl+F")); QString filterNote = tr("Type a partial name or ID to find a contact...") + " (" + m_focusFilterAction->shortcut() .toString(QKeySequence::NativeText) + ")"; m_filterLineEdit = new QLineEdit(this); m_filterLineEdit->setPlaceholderText(filterNote); m_filterLineEdit->setToolTip("" + filterNote); // HTMLized for wordwrapping m_filterLineEdit->setClearButtonEnabled(true); connect(m_filterLineEdit, &QLineEdit::textChanged, this, &ContactList::filterList); connect(m_focusFilterAction, &QAction::triggered, this, &ContactList::focusFilterField); this->addAction(m_focusFilterAction); const int iconSize = m_filterLineEdit->sizeHint().height(); // filterLineEdit->font().pixelSize()? m_filterIcon = new QLabel(this); m_filterIcon->setAlignment(Qt::AlignCenter); m_filterIcon->setPixmap(QIcon::fromTheme("edit-find", QIcon(":/images/menu-find.png")) .pixmap(iconSize, iconSize)); m_matchesCountLabel = new QLabel(this); m_matchesCountLabel->hide(); m_clearFilterButton = new QPushButton(QIcon::fromTheme("view-list-icons"), tr("F&ull List"), this); m_clearFilterButton->setDisabled(true); // Disabled initially, until a search happens connect(m_clearFilterButton, &QAbstractButton::clicked, m_filterLineEdit, &QLineEdit::clear); // Layout m_contactsLayout = new QVBoxLayout(); m_contactsWidget = new QWidget(this); m_contactsWidget->setLayout(m_contactsLayout); m_contactsScrollArea = new QScrollArea(this); m_contactsScrollArea->setWidget(m_contactsWidget); m_contactsScrollArea->setWidgetResizable(true); m_contactsScrollArea->setFrameStyle(QFrame::NoFrame); m_contactsScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); m_filterLayout = new QHBoxLayout(); m_filterLayout->addWidget(m_filterIcon, 0, Qt::AlignVCenter); m_filterLayout->addWidget(m_filterLineEdit); m_filterLayout->addWidget(m_matchesCountLabel); m_filterLayout->addWidget(m_clearFilterButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->addWidget(m_contactsScrollArea); m_mainLayout->addLayout(m_filterLayout); this->setLayout(m_mainLayout); // Add demo contacts QVariantMap demoContactData; QVariantMap demoContactHometown; QVariantMap demoContactFollowed; m_isFollowing = false; if (listType == "following") { m_isFollowing = true; demoContactData.insert("displayName", "Demo Contact"); demoContactData.insert("id", "democontact@pumpserver.org"); demoContactData.insert("url", "https://jancoding.wordpress.com"); demoContactFollowed.insert("followed", "true"); } else { demoContactData.insert("displayName", "Demo Follower"); demoContactData.insert("id", "demofollower@pumpserver.org"); demoContactData.insert("url", "http://dianara.nongnu.org"); demoContactFollowed.insert("followed", "false"); } demoContactHometown.insert("displayName", "Some city"); demoContactData.insert("location", demoContactHometown); demoContactData.insert("pump_io", demoContactFollowed); demoContactData.insert("published", "2013-05-01T00:00:00Z"); // Dianara's birthday ASPerson *demoContactPerson = new ASPerson(demoContactData, this); ContactCard *demoContactCard = new ContactCard(m_pumpController, m_globalObject, demoContactPerson, this); m_contactsLayout->addWidget(demoContactCard); m_contactsInList.append(demoContactCard); delete demoContactPerson; qDebug() << "ContactList created"; } ContactList::~ContactList() { qDebug() << "ContactList destroyed"; } void ContactList::clearListContents() { foreach (ContactCard *card, m_contactsInList) { m_contactsLayout->removeWidget(card); delete card; } m_contactsInList.clear(); if (m_isFollowing) { m_globalObject->clearNickCompletionModel(); } } void ContactList::setListContents(QVariantList contactList) { qDebug() << "ContactList; Setting list contents"; QStringList followingIdStringList; foreach (QVariant contact, contactList) { QVariantMap contactMap = contact.toMap(); if (!contactMap.keys().contains(QStringLiteral("id"))) { /* * Temporary hack to fix user profiles when the list comes * from the site user list, which is currently empty, * and contains ID only inside the profile property * */ QString id = ASPerson::cleanupId(contactMap.value("profile").toMap() .value("id").toString()); contactMap.insert("id", id); QVariantMap followedMap; followedMap.insert("followed", m_pumpController->userInFollowing(id)); contactMap.insert("pump_io", followedMap); id.remove("@" + m_pumpController->currentServerDomain()); contactMap.insert("displayName", id); contactMap.insert("url", m_pumpController->currentServerScheme() + m_pumpController->currentServerDomain() + "/" + id); QVariantMap hrefMap; hrefMap.insert("href", m_pumpController->currentServerScheme() + m_pumpController->currentServerDomain() + "/api/user/" + id + "/feed"); QVariantMap outboxMap; outboxMap.insert("activity-outbox", hrefMap); contactMap.insert("links", outboxMap); } ASPerson *person = new ASPerson(contactMap, this); ContactCard *contactCard = new ContactCard(m_pumpController, m_globalObject, person, this); m_contactsLayout->addWidget(contactCard); m_contactsInList.append(contactCard); if (m_isFollowing) { // Add to internal following list for PumpController followingIdStringList.append(person->getId()); // Add also to GlobalObject's model for nick completion m_globalObject->addToNickCompletionModel(person->getId(), person->getNameWithFallback(), person->getUrl()); } delete person; } ///// End foreach // Batch of contacts added to list, add them also to the internal list if (m_isFollowing) { m_pumpController->updateInternalFollowingIdList(followingIdStringList); } m_filterLineEdit->clear(); // Unfilter the list } QString ContactList::getContactsStringForExport() { QString allContactsString; foreach (ContactCard *card, m_contactsInList) { allContactsString.append(card->getNameAndIdString() + "\n"); } return allContactsString; } /*****************************************************************************/ /*********************************** SLOTS ***********************************/ /*****************************************************************************/ void ContactList::filterList(QString filterText) { qDebug() << "Filtering for contacts matching:" << filterText; if (!filterText.isEmpty()) { int matchCount = 0; foreach (ContactCard *card, m_contactsInList) { if (card->getNameAndIdString().contains(filterText, Qt::CaseInsensitive)) { card->show(); ++matchCount; } else { card->hide(); } } m_matchesCountLabel->setText(QString("(%1)") .arg(QLocale::system() .toString(matchCount))); m_matchesCountLabel->show(); m_clearFilterButton->setEnabled(true); } else // If no filter at all, more optimized version showing all { foreach (ContactCard *card, m_contactsInList) { card->show(); } m_matchesCountLabel->setText(QString()); m_matchesCountLabel->hide(); m_clearFilterButton->setDisabled(true); } } void ContactList::addSingleContact(ASPerson *contact) { ContactCard *card = new ContactCard(m_pumpController, m_globalObject, contact, this); m_contactsLayout->insertWidget(0, card); m_contactsInList.append(card); emit contactCountChanged(1); // This check is actually unnecessary, since this slot is only called if (m_isFollowing) // for the 'following' list { QStringList contactsToAdd; contactsToAdd.append(contact->getId()); m_pumpController->updateInternalFollowingIdList(contactsToAdd); // Add also to GlobalObject's model for nick completion m_globalObject->addToNickCompletionModel(contact->getId(), contact->getNameWithFallback(), contact->getUrl()); } // Other slots use this object, but this should be safe contact->deleteLater(); } void ContactList::removeSingleContact(ASPerson *contact) { foreach (ContactCard *card, m_contactsInList) { /* Ignore disabled cards, to avoid substracting more than once, in case * there were more ContactCards for the same contact, from following * and unfollowing previously. * */ if (card->isEnabled() && card->getId() == contact->getId()) { emit contactCountChanged(-1); m_pumpController->removeFromInternalFollowingList(contact->getId()); // Remove from GlobalObject's model for nick completion, too m_globalObject->removeFromNickCompletionModel(contact->getId()); card->setDisabled(true); } } contact->deleteLater(); } /* * When pressing Ctrl+F, focus filter field, * or select all text if already focused * */ void ContactList::focusFilterField() { if (m_filterLineEdit->hasFocus()) { m_filterLineEdit->selectAll(); } else { m_filterLineEdit->setFocus(); } } dianara-v1.4.1/src/datafile.cpp0000644000175000017500000000476413202667734014475 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "datafile.h" DataFile::DataFile(QString filename, QObject *parent) : QObject(parent) { m_dataFile = new QFile(filename); if (!m_dataFile->exists()) { // Initialize empty file if (!m_dataFile->open(QIODevice::WriteOnly)) { qDebug() << "** Error initializing data file: " << filename; } m_dataFile->write(QByteArray()); m_dataFile->close(); } qDebug() << "DataFile created for " << filename; } DataFile::~DataFile() { qDebug() << "DataFile destroyed for " << m_dataFile->fileName(); } /* * Read map list data from disk in JSON format. * */ QVariantList DataFile::loadData() { QVariantList loadedList; bool parsedOk = false; m_dataFile->open(QIODevice::ReadOnly); const QByteArray rawData = m_dataFile->readAll(); m_dataFile->close(); QJsonDocument jsonDocument; jsonDocument = QJsonDocument::fromJson(rawData); loadedList = jsonDocument.toVariant().toList(); parsedOk = !jsonDocument.isNull(); if (!parsedOk) { // TMP FIXME loadedList.clear(); qDebug() << "** Error parsing JSON file"; } return loadedList; } /* * Save VariantList data to disk in JSON format. * */ bool DataFile::saveData(QVariantList list) { QByteArray byteData; QJsonDocument jsonDocument; jsonDocument = QJsonDocument::fromVariant(list); byteData = jsonDocument.toJson(QJsonDocument::Indented); m_dataFile->open(QIODevice::WriteOnly); int written = m_dataFile->write(byteData); m_dataFile->close(); if (written < 1) // 0 bytes, or -1 for Error { return false; } return true; } dianara-v1.4.1/src/timeline.h0000644000175000017500000001345113206604002014147 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include "pumpcontroller.h" #include "globalobject.h" #include "asobject.h" #include "asactivity.h" #include "post.h" #include "filterchecker.h" #include "pageselector.h" class TimeLine : public QWidget { Q_OBJECT public: TimeLine(PumpController::requestTypes timelineType, PumpController *pumpController, GlobalObject *globalObject, FilterChecker *filterChecker, QWidget *parent = 0); ~TimeLine(); void createKeyboardActions(); void setCustomUrl(QString url); void clearTimeLineContents(bool showMessage=true); void removeOldPosts(int minimumToKeep); void insertSeparator(int position); int getCurrentPage(); int getTotalPages(); int getTotalPosts(); void updateCurrentPageNumber(); void syncPostsPerPage(); void enablePaginationButtons(); void disablePaginationButtons(); void resizePosts(QList postsToResize, bool resizeAll=false); void markPostsAsRead(); void updateFuzzyTimestamps(); bool commentingOnAnyPost(); void notifyBlockedUpdates(); QList getPostsInTimeline(); QFrame *getSeparatorWidget(); void showMessage(QString message); signals: void scrollTo(QAbstractSlider::SliderAction sliderAction); void timelineRendered(PumpController::requestTypes timelineType, int newPostCount, int highlightedPostsCount, int directPostsCount, int highlightedByFilterCount, int deletedPostsCount, int filteredPostsCount, int pendingForNextUpdate, QString currentPageAndTotal); void unreadPostsCountChanged(PumpController::requestTypes timelineType, int newPostCount, int highlightedCount, int fullTimelinePostCount); void commentingOnPost(QWidget *commenterWidget); public slots: void setTimeLineContents(QVariantList postList, QString previousLink, QString nextLink, int totalItems); void onUpdateFailed(int requestType); void updatePostsFromMinorFeed(ASObject *object); void addLikesFromMinorFeed(QString objectId, QString objectType, QString actorId, QString actorName, QString actorUrl); void removeLikesFromMinorFeed(QString objectId, QString objectType, QString actorId); void addReplyFromMinorFeed(ASObject *object); void setPostsDeletedFromMinorFeed(ASObject *object); void setLikesInPost(QVariantList likesList, QString originatingPostUrl); void setCommentsInPost(QVariantList commentsList, QString originatingPostUrl); //void setSharesInPost(...) void goToFirstPage(); void goToPreviousPage(); void goToNextPage(); void goToSpecificPage(int pageNumber); void getNewPending(); void showPageSelector(); void scrollUp(); void scrollDown(); void scrollPageUp(); void scrollPageDown(); void scrollToTop(); void scrollToBottom(); void decreaseUnreadPostsCount(bool wasHighlighted); void updateAvatarFollowStates(); protected: private: QVBoxLayout *m_mainLayout; QVBoxLayout *m_postsLayout; QHBoxLayout *m_bottomLayout; QPushButton *m_getNewPendingButton; QLabel *m_infoLabel; QWidget *m_postsWidget; QFrame *m_separatorFrame; QPushButton *m_firstPageButton; QPushButton *m_previousPageButton; QPushButton *m_currentPageButton; QPushButton *m_nextPageButton; QString m_customUrl; QString m_previousPageLink; QString m_nextPageLink; int m_fullTimelinePostCount; int m_postsPendingForNextTime; bool m_firstLoad; bool m_gettingNew; bool m_wasOnFirstPage; int m_timelineOffset; int m_oldTimelineOffset; int m_postsPerPage; int m_unreadPostsCount; int m_highlightedPostsCount; QString m_currentPageTotalString; // QActions to enhance keyboard control QAction *m_scrollUpAction; QAction *m_scrollDownAction; QAction *m_scrollPageUpAction; QAction *m_scrollPageDownAction; QAction *m_scrollTopAction; QAction *m_scrollBottomAction; QAction *m_previousPageAction; QAction *m_nextPageAction; QAction *m_showPageSelectorAction; PageSelector *m_pageSelector; PumpController::requestTypes m_timelineType; bool m_isFavoritesTimeline; QString m_previousNewestPostId; QList m_postsInTimeline; QStringList m_objectsIdList; PumpController *m_pumpController; GlobalObject *m_globalObject; FilterChecker *m_filterChecker; }; #endif // TIMELINE_H dianara-v1.4.1/src/contactmanager.h0000664000175000017500000000602113204345057015336 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 CONTACTMANAGER_H #define CONTACTMANAGER_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pumpcontroller.h" #include "contactlist.h" #include "listsmanager.h" #include "globalobject.h" #include "siteuserslist.h" class ContactManager : public QWidget { Q_OBJECT public: ContactManager(PumpController *pumpController, GlobalObject *globalObject, QWidget *parent = 0); ~ContactManager(); void setTabLabels(); void exportContactsToFile(QString listType); void enableManualFollowWidgets(); signals: public slots: void setContactListsContents(QString listType, QVariantList contactList, int totalReceivedCount); void setListsListContents(QVariantList listsList); void changeFollowingCount(int difference); void refreshFollowing(); void refreshFollowers(); void exportFollowing(); void exportFollowers(); void refreshPersonLists(); void toggleFollowButton(QString currentAddress); void validateContactId(); void followContact(QString userId, int httpCode, bool requestTimedOut, QString serverVersion); void onCantFollowNow(QString userId); protected: private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_topLayout; QLabel *m_enterAddressLabel; QLineEdit *m_addressLineEdit; QPushButton *m_followButton; QTabWidget *m_tabWidget; ContactList *m_followingWidget; int m_followingCount; ContactList *m_followersWidget; int m_followersCount; ListsManager *m_listsManager; QScrollArea *m_listsScrollArea; int m_listsCount; SiteUsersList *m_siteUsersList; QPushButton *m_optionsButton; QMenu *m_optionsMenu; bool m_followingManually; QString m_followingAddress; PumpController *m_pumpController; GlobalObject *m_globalObject; }; #endif // CONTACTMANAGER_H dianara-v1.4.1/src/peoplewidget.cpp0000664000175000017500000002167413216304223015377 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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) { m_pumpController = pumpController; connect(m_pumpController, &PumpController::contactListReceived, this, &PeopleWidget::updateAllContactsList); connect(m_pumpController, &PumpController::contactFollowed, this, &PeopleWidget::addContact); connect(m_pumpController, &PumpController::contactUnfollowed, this, &PeopleWidget::removeContact); this->setMinimumSize(240, 280); m_searchLabel = new QLabel(tr("&Search:"), this); m_searchLineEdit = new QLineEdit(this); m_searchLineEdit->setPlaceholderText(tr("Enter a name here to search for it")); m_searchLineEdit->setClearButtonEnabled(true); m_searchLabel->setBuddy(m_searchLineEdit); connect(m_searchLineEdit, &QLineEdit::textChanged, this, &PeopleWidget::filterList); m_itemModel = new QStandardItemModel(this); m_filterModel = new QSortFilterProxyModel(this); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSourceModel(m_itemModel); m_allContactsListView = new QListView(this); m_allContactsListView->setAlternatingRowColors(true); m_allContactsListView->setEditTriggers(QAbstractItemView::NoEditTriggers); m_allContactsListView->setDragDropMode(QListView::DragDrop); m_allContactsListView->setDefaultDropAction(Qt::CopyAction); m_allContactsListView->setModel(m_filterModel); connect(m_allContactsListView, &QAbstractItemView::activated, this, &PeopleWidget::returnClickedContact); m_addToButton = new QPushButton(QIcon::fromTheme("list-add", QIcon(":/images/list-add.png")), buttonText, this); connect(m_addToButton, &QAbstractButton::clicked, this, &PeopleWidget::returnContact); // Layout m_buttonsLayout = new QHBoxLayout(); m_buttonsLayout->addWidget(m_addToButton); m_buttonsLayout->addStretch(); m_mainLayout = new QVBoxLayout(); m_mainLayout->addWidget(m_searchLabel); m_mainLayout->addWidget(m_searchLineEdit); m_mainLayout->addSpacing(2); m_mainLayout->addWidget(m_allContactsListView); m_mainLayout->addSpacing(4); m_mainLayout->addLayout(m_buttonsLayout); this->setLayout(m_mainLayout); if (type == EmbeddedWidget) { //allContactsListWidget->setSelectionMode(QListView::ExtendedSelection); // FIXME 1.4.x: For now, Single selection mode in both cases m_allContactsListView->setSelectionMode(QListView::SingleSelection); } else // StandaloneWidget { this->setWindowTitle(tr("Add a contact to a list") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("system-users", QIcon(":/images/button-users.png"))); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::WindowModal); this->setMinimumSize(520, 500); m_allContactsListView->setSelectionMode(QListView::SingleSelection); m_cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("&Cancel"), this); connect(m_cancelButton, &QAbstractButton::clicked, this, &QWidget::hide); m_buttonsLayout->addWidget(m_cancelButton); } qDebug() << "PeopleWidget created"; } PeopleWidget::~PeopleWidget() { qDebug() << "PeopleWidget destroyed"; } void PeopleWidget::resetWidget() { m_allContactsListView->scrollToTop(); m_searchLineEdit->clear(); // Might also trigger filterLists() m_searchLineEdit->setFocus(); } QStandardItem *PeopleWidget::createContactItem(ASPerson *contact) { QStandardItem *item; QString singleContactString = contact->getNameWithFallback() + " <" + contact->getId() + ">"; QString avatarFilename = MiscHelpers::getCachedAvatarFilename(contact->getAvatarUrl()); QPixmap avatarPixmap = QPixmap(avatarFilename); if (!avatarPixmap.isNull()) { item = new QStandardItem(QIcon(avatarPixmap), singleContactString); } else { item = new QStandardItem(QIcon::fromTheme("user-identity", QIcon(":/images/no-avatar.png")), singleContactString); } item->setToolTip(contact->getTooltipInfo()); item->setData(contact->getNameWithFallback(), Qt::UserRole + 1); item->setData(contact->getId(), Qt::UserRole + 2); item->setData(contact->getUrl(), Qt::UserRole + 3); return item; } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// SLOTS ////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// /* * Filter the list of all contacts based on what * the user entered in the search box * */ void PeopleWidget::filterList(QString searchTerms) { m_filterModel->setFilterFixedString(searchTerms); m_filterModel->sort(0); } /* * 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 { m_itemModel->clear(); } foreach (QVariant contactVariant, contactsVariantList) { ASPerson *contact = new ASPerson(contactVariant.toMap(), this); QStandardItem *item = this->createContactItem(contact); m_itemModel->appendRow(item); delete contact; } m_filterModel->sort(0); // Sort by first column } void PeopleWidget::addContact(ASPerson *contact) { m_itemModel->appendRow(this->createContactItem(contact)); contact->deleteLater(); } void PeopleWidget::removeContact(ASPerson *contact) { foreach (QStandardItem *item, m_itemModel->findItems("<" + contact->getId() + ">", Qt::MatchEndsWith)) { m_itemModel->removeRow(item->row()); } contact->deleteLater(); } /* * Send current contact icon and string in a SIGNAL * * Used when selecting a row and clicking the "add" button * */ void PeopleWidget::returnContact() { if (m_allContactsListView->currentIndex().row() != -1) { QStandardItem *item = m_itemModel->itemFromIndex(m_filterModel->mapToSource(m_allContactsListView->currentIndex())); if (item != 0) { emit addButtonPressed(item->icon(), item->text(), item->data(Qt::UserRole + 1).toString(), item->data(Qt::UserRole + 2).toString(), item->data(Qt::UserRole + 3).toString()); } } } /* * Used when clicking a contact * */ void PeopleWidget::returnClickedContact(QModelIndex modelIndex) { QStandardItem *item = m_itemModel->itemFromIndex(m_filterModel->mapToSource(modelIndex)); QIcon icon; if (item != 0) // Valid item { icon = item->icon(); } emit contactSelected(icon, modelIndex.data().toString(), modelIndex.data(Qt::UserRole + 1).toString(), modelIndex.data(Qt::UserRole + 2).toString(), modelIndex.data(Qt::UserRole + 3).toString()); } dianara-v1.4.1/src/publisher.cpp0000644000175000017500000015775713216304270014720 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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, GlobalObject *globalObject, QWidget *parent) : QWidget(parent) { m_pumpController = pumpController; connect(m_pumpController, &PumpController::postPublished, this, &Publisher::onPublishingOk); connect(m_pumpController, &PumpController::postPublishingFailed, this, &Publisher::onPublishingFailed); // After receiving the list of lists, update the "Lists" submenus connect(m_pumpController, &PumpController::listsListReceived, this, &Publisher::updateListsMenus); m_globalObject = globalObject; connect(m_globalObject, &GlobalObject::messagingModeRequested, this, &Publisher::startMessageForContact); connect(m_globalObject, &GlobalObject::postEditRequested, this, &Publisher::setEditingMode); m_postType = "note"; m_editingMode = false; // False unless set from setEditingMode // after clicking "Edit" in a post m_toAudienceSelector = new AudienceSelector(m_pumpController, "to", this); connect(m_toAudienceSelector, &AudienceSelector::audienceChanged, this, &Publisher::updateAudienceToLabels); m_ccAudienceSelector = new AudienceSelector(m_pumpController, "cc", this); connect(m_ccAudienceSelector, &AudienceSelector::audienceChanged, this, &Publisher::updateAudienceCcLabels); // Track Public/Followers in To/Cc on one list to uncheck on the other connect(m_toAudienceSelector, &AudienceSelector::publicSelected, this, &Publisher::onToPublicSelected); connect(m_toAudienceSelector, &AudienceSelector::followersSelected, this, &Publisher::onToFollowersSelected); connect(m_ccAudienceSelector, &AudienceSelector::publicSelected, this, &Publisher::onCcPublicSelected); connect(m_ccAudienceSelector, &AudienceSelector::followersSelected, this, &Publisher::onCcFollowersSelected); QString titleTooltip = "" + tr("Setting a title helps make the " "Meanwhile feed more informative"); QFont titleFont; titleFont.setPointSize(titleFont.pointSize() + 1); titleFont.setBold(true); m_titleLabel = new QLabel(tr("Title") + ":", this); m_titleLabel->setFont(titleFont); m_titleLabel->setToolTip(titleTooltip); m_titleLineEdit = new QLineEdit(this); m_titleLineEdit->setPlaceholderText(tr("Add a brief title for the post here " "(recommended)")); m_titleLineEdit->setFont(titleFont); m_titleLineEdit->setToolTip(titleTooltip); m_pictureLabel = new QLabel(this); m_pictureLabel->setAlignment(Qt::AlignCenter); m_pictureLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Raised); m_pictureLabel->hide(); m_mediaInfoLabel = new QLabel(this); m_mediaInfoLabel->setWordWrap(true); m_mediaInfoLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); m_mediaInfoLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); m_selectMediaButton = new QPushButton(this); m_selectMediaButton->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); connect(m_selectMediaButton, &QAbstractButton::clicked, this, &Publisher::onSelectMediaFilePressed); m_selectMediaButton->hide(); m_removeMediaButton = new QPushButton(QIcon::fromTheme("edit-delete", QIcon(":/images/button-delete.png")), tr("Remove"), this); m_removeMediaButton->setToolTip("" + tr("Cancel the attachment, and go " "back to a regular note")); m_removeMediaButton->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); connect(m_removeMediaButton, &QAbstractButton::clicked, this, &Publisher::cancelMediaMode); m_removeMediaButton->hide(); m_uploadProgressBar = new QProgressBar(this); // Set default pixmap and "media not set" message this->setEmptyMediaData(); m_lastUsedDirectory = QDir::homePath(); // Composer m_composerBox = new Composer(m_globalObject, true, // forPublisher = true this); m_composerBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); connect(m_composerBox, &Composer::focusReceived, this, &Publisher::setFullMode); connect(m_composerBox, &Composer::editingFinished, this, &Publisher::sendPost); connect(m_composerBox, &Composer::editingCancelled, this, &Publisher::setMinimumMode); connect(m_composerBox, &Composer::nickInserted, this, &Publisher::addNickToRecipients); connect(m_composerBox, &QTextEdit::textChanged, this, &Publisher::updateCharacterCounter); connect(m_composerBox, &Composer::fileDropped, this, &Publisher::onFileDropped); // Pressing Enter in title goes to message body connect(m_titleLineEdit, SIGNAL(returnPressed()), m_composerBox, SLOT(setFocus())); // Likewise, pressing UP at the start of the body goes to title connect(m_composerBox, SIGNAL(focusTitleRequested()), m_titleLineEdit, SLOT(setFocus())); // Add formatting button exported from Composer m_toolsButton = m_composerBox->getToolsButton(); // Add menu and submenus to access drafts from the Draft Manager m_draftsManager = new DraftsManager(m_globalObject, this); connect(m_draftsManager, &DraftsManager::draftSelected, this, &Publisher::onDraftSelected); connect(m_draftsManager, &DraftsManager::saveDraftRequested, this, &Publisher::onSaveDraftRequested); connect(m_composerBox, &Composer::cancelSavingDraftRequested, this, &Publisher::onCancelSavingDraftRequested); // Set focus back to the composer upon selecting "Manage drafts" connect(m_draftsManager, &DraftsManager::windowShown, this, &Publisher::setFullMode); m_draftsButton = new QPushButton(QIcon::fromTheme("document-save-as", QIcon(":/images/button-edit.png")), tr("Drafts"), this); m_draftsMenu = m_draftsManager->getDraftMenu(); m_draftsButton->setMenu(m_draftsMenu); // To... menu m_toSelectorMenu = m_toAudienceSelector->getSelectorMenu(); m_toSelectorButton = new QPushButton(QIcon::fromTheme("system-users", QIcon(":/images/button-users.png")), tr("To..."), this); m_toSelectorButton->setToolTip("" + tr("Select who will see this post")); m_toSelectorButton->setMenu(m_toSelectorMenu); // Cc... menu m_ccSelectorMenu = m_ccAudienceSelector->getSelectorMenu(); m_ccSelectorButton = new QPushButton(QIcon::fromTheme("system-users", QIcon(":/images/button-users.png")), tr("Cc..."), this); m_ccSelectorButton->setToolTip("" + tr("Select who will get a copy of this post")); m_ccSelectorButton->setMenu(m_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 and lists selected for the To or Cc fields, if any m_toAudienceLabel = new QLabel(this); m_toAudienceLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); m_toAudienceLabel->setFont(audienceLabelsFont); // Normal m_toAudienceLabel->setWordWrap(true); m_toAudienceLabel->setOpenExternalLinks(true); connect(m_toAudienceLabel, &QLabel::linkHovered, this, &Publisher::showHighlightedUrl); m_ccAudienceLabel = new QLabel(this); m_ccAudienceLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); audienceLabelsFont.setItalic(true); m_ccAudienceLabel->setFont(audienceLabelsFont); // Italic m_ccAudienceLabel->setWordWrap(true); m_ccAudienceLabel->setOpenExternalLinks(true); connect(m_ccAudienceLabel, &QLabel::linkHovered, this, &Publisher::showHighlightedUrl); // Menu to set the picture/audio/video modes, under an "Add..." button m_addMediaImageAction = new QAction(QIcon::fromTheme("camera-photo", QIcon(":/images/attached-image.png")), tr("Picture"), this); connect(m_addMediaImageAction, &QAction::triggered, this, &Publisher::setPictureMode); m_addMediaAudioAction = new QAction(QIcon::fromTheme("audio-input-microphone", QIcon(":/images/attached-audio.png")), tr("Audio"), this); connect(m_addMediaAudioAction, &QAction::triggered, this, &Publisher::setAudioMode); m_addMediaVideoAction = new QAction(QIcon::fromTheme("camera-web", QIcon(":/images/attached-video.png")), tr("Video"), this); connect(m_addMediaVideoAction, &QAction::triggered, this, &Publisher::setVideoMode); m_addMediaFileAction = new QAction(QIcon::fromTheme("application-octet-stream", QIcon(":/images/attached-file.png")), tr("Other", "as in other kinds of files"), this); connect(m_addMediaFileAction, &QAction::triggered, this, &Publisher::setFileMode); // The menu itself m_addMediaMenu = new QMenu(QStringLiteral("add-media-menu"), this); m_addMediaMenu->addAction(m_addMediaImageAction); m_addMediaMenu->addAction(m_addMediaAudioAction); m_addMediaMenu->addAction(m_addMediaVideoAction); m_addMediaMenu->addAction(m_addMediaFileAction); // The "add..." button holding the menu m_addMediaButton = new QPushButton(QIcon::fromTheme("mail-attachment", QIcon(":/images/list-add.png")), tr("Ad&d..."), this); m_addMediaButton->setToolTip("" + tr("Upload media, like pictures or videos")); m_addMediaButton->setMenu(m_addMediaMenu); // Character counter (optional) m_charCounterLabel = new QLabel(QStringLiteral("0"), this); m_charCounterLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); // Info label for "sending" and other status information m_statusInfoLabel = new QLabel(this); m_statusInfoLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); m_statusInfoLabel->setAlignment(Qt::AlignCenter); m_statusInfoLabel->setWordWrap(true); audienceLabelsFont.setBold(false); audienceLabelsFont.setItalic(false); m_statusInfoLabel->setFont(audienceLabelsFont); connect(m_composerBox, &Composer::errorHappened, m_statusInfoLabel, &QLabel::setText); // To send the post m_postButton = new QPushButton(QIcon::fromTheme("mail-send", QIcon(":/images/button-post.png")), tr("Post", "verb"), this); m_postButton->setToolTip("" + tr("Hit Control+Enter to post with the keyboard")); connect(m_postButton, &QAbstractButton::clicked, this, &Publisher::sendPost); m_cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("Cancel"), this); m_cancelButton->setToolTip("" + tr("Cancel the post")); connect(m_cancelButton, &QAbstractButton::clicked, m_composerBox, &Composer::cancelPost); // Now the layout, starting with the Title field and "media mode" stuff m_titleLayout = new QHBoxLayout(); m_titleLayout->addWidget(m_titleLabel, 0); m_titleLayout->addSpacing(4); m_titleLayout->addWidget(m_titleLineEdit, 2); m_titleLayout->addSpacing(2); m_titleLayout->addWidget(m_toolsButton, 0); m_titleLayout->addSpacing(2); m_titleLayout->addWidget(m_draftsButton, 0); m_mediaLayout = new QGridLayout(); m_mediaLayout->setVerticalSpacing(0); m_mediaLayout->setContentsMargins(0, 0, 0, 0); m_mediaLayout->addWidget(m_pictureLabel, 1, 0, 2, 4); m_mediaLayout->addWidget(m_mediaInfoLabel, 1, 4, 1, 4, Qt::AlignTop); m_mediaLayout->addWidget(m_selectMediaButton, 2, 4, 1, 2, Qt::AlignBottom | Qt::AlignLeft); m_mediaLayout->addWidget(m_uploadProgressBar, 2, 6, 1, 2, Qt::AlignBottom | Qt::AlignRight); m_mediaLayout->addWidget(m_removeMediaButton, 2, 6, 1, 2, Qt::AlignBottom | Qt::AlignRight); m_buttonsLayout = new QGridLayout(); m_buttonsLayout->setVerticalSpacing(0); m_buttonsLayout->setContentsMargins(0, 0, 0, 0); m_buttonsLayout->addWidget(m_toSelectorButton, 0, 0, 1, 1, Qt::AlignLeft); m_buttonsLayout->addWidget(m_ccSelectorButton, 0, 1, 1, 1, Qt::AlignLeft); m_buttonsLayout->addWidget(m_addMediaButton, 0, 3, 1, 2, Qt::AlignCenter); m_buttonsLayout->addWidget(m_statusInfoLabel, 0, 5, 3, 2, Qt::AlignCenter); m_buttonsLayout->addWidget(m_postButton, 0, 7, 1, 1); // The 2 labels holding Public, Followers, Lists and people's names m_buttonsLayout->addWidget(m_toAudienceLabel, 1, 0, 2, 1); m_buttonsLayout->addWidget(m_ccAudienceLabel, 1, 1, 2, 1); // Character counter m_buttonsLayout->addWidget(m_charCounterLabel, 1, 3, 1, 2, Qt::AlignCenter); // The "Cancel post" button m_buttonsLayout->addWidget(m_cancelButton, 1, 7, 1, 1); #ifdef GROUPSUPPORT m_groupIdLabel = new QLabel("TO GROUP (ID):", this); m_buttonsLayout->addWidget(m_groupIdLabel, 3, 0, 1, 1); m_groupIdLineEdit = new QLineEdit(this); m_buttonsLayout->addWidget(m_groupIdLineEdit, 3, 1, 1, 7); #endif m_mainLayout = new QVBoxLayout(); m_mainLayout->setContentsMargins(0, 0, 0, 0); m_mainLayout->setSpacing(0); m_mainLayout->addLayout(m_titleLayout, 0); m_mainLayout->addSpacing(1); m_mainLayout->addLayout(m_mediaLayout, 0); m_mainLayout->addWidget(m_composerBox, 3); m_mainLayout->addLayout(m_buttonsLayout, 0); this->setLayout(m_mainLayout); this->setMinimumMode(); qDebug() << "Publisher created"; } Publisher::~Publisher() { qDebug() << "Publisher destroyed"; } /* * Set To:Public if "public posts as default" option for audience is enabled * */ void Publisher::syncFromConfig() { m_toAudienceSelector->setPublic(m_globalObject->getPublicPostsByDefault()); } /* * Set default "no photo" pixmap and "picture/audio/video not set" message * * Clear the filename and content type variables too */ void Publisher::setEmptyMediaData() { m_pictureLabel->setToolTip(QString()); m_mediaInfoLabel->clear(); m_mediaFilename.clear(); m_mediaContentType.clear(); } /* * Set title and content from the outside, for automation (dbus interface) * */ void Publisher::setTitleAndContent(QString title, QString content) { QString statusText; if (emptyContents()) { m_titleLineEdit->setText(title.trimmed()); m_composerBox->setText(content.trimmed()); m_composerBox->moveCursor(QTextCursor::End); statusText = tr("Note started from another application."); // FIXME? } else { statusText = tr("Ignoring new note request from another application."); // TMP message FIXME } m_statusInfoLabel->setText(statusText); } QVariantMap Publisher::getAudienceMap() { QVariantMap fullAudienceMap; // Used to warn the user when posting only to followers having none m_onlyToFollowers = true; // Will be set to false if something else is enabled fullAudienceMap.insert("to", m_toAudienceSelector->getAudienceList(&m_onlyToFollowers)); fullAudienceMap.insert("cc", m_ccAudienceSelector->getAudienceList(&m_onlyToFollowers)); #ifdef GROUPSUPPORT // Cc: Groups if (!m_groupIdLineEdit->text().trimmed().isEmpty()) { m_onlyToFollowers = false; QVariantMap groupMap; groupMap.insert("objectType", "group"); groupMap.insert("id", m_groupIdLineEdit->text().trimmed()); QVariantList ccList = fullAudienceMap.value("cc").toList(); ccList.append(groupMap); fullAudienceMap.insert("cc", groupMap); } #endif // Last check: if nothing is checked, add Cc:Followers if (fullAudienceMap.isEmpty()) // FIXME: maybe fail and warn the user instead { QVariantMap ccFollowersMap; ccFollowersMap.insert("objectType", "collection"); ccFollowersMap.insert("id", m_pumpController->currentFollowersUrl()); QVariantList ccList = fullAudienceMap.value("cc").toList(); ccList.append(ccFollowersMap); fullAudienceMap.insert("cc", ccFollowersMap); } return fullAudienceMap; } void Publisher::setAudienceFromMap(QVariantMap audienceMap) { // First, uncheck the ones that might be enabled due to config m_toAudienceSelector->setDefaultAudience(false); m_ccAudienceSelector->setDefaultAudience(false); m_ccAudienceSelector->setFollowers(false); qDebug() << "***** SETTING AUDIENCE FROM DRAFT MAP:"; qDebug() << audienceMap; foreach (QString listType, audienceMap.keys()) // "to", "cc", ATM { qDebug() << "LIST:" << listType; const QVariantList audienceList = audienceMap.value(listType).toList(); foreach (QVariant audienceItem, audienceList) { QVariantMap audienceItemMap = audienceItem.toMap(); qDebug() << "Audience item:" << audienceItemMap; QString id = audienceItemMap.value("id").toString(); if (id.isEmpty()) { // Avoid for example accepting empty Followers ID as valid break; } QString objectType = audienceItemMap.value("objectType").toString(); if (objectType == "collection") { if (id == QStringLiteral("http://activityschema.org/collection/public")) { if (listType == "to") { m_toAudienceSelector->setPublic(true); } else if (listType == "cc") { m_ccAudienceSelector->setPublic(true); } qDebug() << "Checking PUBLIC"; } else if (id == m_pumpController->currentFollowersUrl()) { if (listType == "to") { m_toAudienceSelector->setFollowers(true); } else if (listType == "cc") { m_ccAudienceSelector->setFollowers(true); } qDebug() << "Checking FOLLOWERS"; } else { // Determine if any of the person lists needs to be checked qDebug() << "Looking for LISTS to check, with ID:" << id; if (listType == "to") { m_toAudienceSelector->checkListWithId(id); } else if (listType == "cc") { m_ccAudienceSelector->checkListWithId(id); } } } else if (objectType == "person") { id = ASPerson::cleanupId(id); QPair personData = m_globalObject->getDataForNick(id); this->addNickToRecipients(id, personData.first, personData.second, listType); } else if (objectType == "group") { // TODO: Check corresponding groups, when group support is complete // GROUPSUPPORT -- FIXME -- TODO } } } } void Publisher::setMediaModeWidgets() { m_addMediaButton->setDisabled(true); m_pictureLabel->show(); m_mediaInfoLabel->show(); m_selectMediaButton->show(); m_removeMediaButton->show(); } /* * Let users find a file in their folders, for media upload * */ void Publisher::findMediaFile() { QString findDialogTitle; QString mediaTypes; QString errorTitle; QString errorMessage; if (m_postType == QStringLiteral("image")) { findDialogTitle = tr("Select one image"); mediaTypes = tr("Image files") + MiscHelpers::fileFilterString(MiscHelpers::imageExtensions()); errorTitle = tr("Invalid image"); errorMessage = tr("The image format cannot be detected.\n" "The extension might be wrong, like a GIF " "image renamed to image.jpg or similar."); } else if (m_postType == QStringLiteral("audio")) { findDialogTitle = tr("Select one audio file"); mediaTypes = tr("Audio files") + MiscHelpers::fileFilterString(MiscHelpers::audioExtensions()); errorTitle = tr("Invalid audio file"); errorMessage = tr("The audio format cannot be detected."); } else if (m_postType == QStringLiteral("video")) { findDialogTitle = tr("Select one video file"); mediaTypes = tr("Video files") + MiscHelpers::fileFilterString(MiscHelpers::videoExtensions()); errorTitle = tr("Invalid video file"); errorMessage = tr("The video format cannot be detected."); } else // File { findDialogTitle = tr("Select one file"); mediaTypes = QString(); errorTitle = tr("Invalid file"); errorMessage = tr("The file type cannot be detected."); } // Set a possible initial filename, if loading a draft or DnD'ing a file QString filename = m_mediaFilename; if (filename.isEmpty()) { filename = QFileDialog::getOpenFileName(this, findDialogTitle, m_lastUsedDirectory, mediaTypes + tr("All files") + " (*)"); } if (!filename.isEmpty()) { qDebug() << "Selected" << filename << "for upload"; QFileInfo fileInfo(filename); m_mediaContentType = MiscHelpers::getFileMimeType(filename); // Temporary protection; https://github.com/pump-io/pump.io/issues/1015 if (fileInfo.size() > 10485760) // 10 MiB; this protects the servers from abuse { QString bigImageNote; if (m_postType == QStringLiteral("image")) { bigImageNote = QStringLiteral("\n\n") + tr("Since you're uploading an image, you could " "scale it down a little or save it in a " "more compressed format, like JPG."); } QMessageBox::warning(this, tr("File is too big"), tr("Dianara currently limits file uploads " "to 10 MiB per post, to prevent possible " "storage or network problems in the " "servers.") + "\n\n" + tr("This is a temporary measure, since " "the servers cannot set their " "own limits yet.") + bigImageNote + "\n\n" + tr("Sorry for the inconvenience.")); m_mediaFilename.clear(); } else if (!fileInfo.isReadable()) // Make sure attachment can be read { this->reportUnreadableFile(filename, fileInfo); m_mediaFilename.clear(); } else if (!m_mediaContentType.isEmpty()) { m_pictureLabel->setToolTip(QStringLiteral("") // wordwrapped + filename); m_mediaFilename = filename; QString metaDataString; if (m_postType == QStringLiteral("image")) { QPixmap imagePixmap = QPixmap(filename); if (imagePixmap.isNull()) { QMessageBox::warning(this, errorTitle, errorMessage); } m_pictureLabel->setPixmap(imagePixmap.scaled(266, 150, // 16:9 Qt::KeepAspectRatio, Qt::SmoothTransformation)); metaDataString = "

              " "" + tr("Resolution", "Image resolution (size)") + ": " + MiscHelpers::resolutionString(imagePixmap.width(), imagePixmap.height()); } else { QMimeDatabase mimeDb; const QString mimeIcon = mimeDb.mimeTypeForFile(filename).iconName(); if (QIcon::hasThemeIcon(mimeIcon)) { m_pictureLabel->setPixmap(QIcon::fromTheme(mimeIcon) .pixmap(150, 150)); } } m_lastUsedDirectory = fileInfo.path(); qDebug() << "Last used directory:" << m_lastUsedDirectory; m_mediaInfoLabel->setText(QString("%1" "
              " "" + tr("Type") + ": %2" "
              " "" + tr("Size") + ": %3") .arg(fileInfo.fileName(), m_mediaContentType, MiscHelpers::fileSizeString(filename)) + metaDataString); // If there's no title, set one from the filename if (m_titleLineEdit->text().trimmed().isEmpty()) { // But only if configured to do so if (m_globalObject->getUseFilenameAsTitle()) { QString titleFromFilename = fileInfo.fileName(); // .baseName() ? titleFromFilename.replace("_", " "); titleFromFilename.replace(".", " "); m_titleLineEdit->setText(titleFromFilename); } } m_statusInfoLabel->clear(); // Remove possible previous error message } else { qDebug() << "Unknown " << m_postType << " format; Extension is probably wrong"; QMessageBox::warning(this, errorTitle, errorMessage); m_mediaContentType.clear(); m_mediaFilename.clear(); m_pictureLabel->setToolTip(QString()); } m_uploadProgressBar->hide(); } } /* * Disable some widgets while sending a post, * including during media upload * */ void Publisher::toggleWidgetsWhileSending(bool widgetsEnabled) { m_titleLineEdit->setEnabled(widgetsEnabled); m_composerBox->setEnabled(widgetsEnabled); m_toolsButton->setEnabled(widgetsEnabled); m_draftsButton->setEnabled(widgetsEnabled); m_toSelectorButton->setEnabled(widgetsEnabled); m_ccSelectorButton->setEnabled(widgetsEnabled); // Only re-enable "add" button if this was still a simple note if (m_postType == QStringLiteral("note")) { m_addMediaButton->setEnabled(widgetsEnabled); } m_selectMediaButton->setEnabled(widgetsEnabled); m_removeMediaButton->setEnabled(widgetsEnabled); m_postButton->setEnabled(widgetsEnabled); } bool Publisher::isFullMode() { return m_fullMode; } bool Publisher::emptyContents() { if (!m_titleLineEdit->text().isEmpty()) { return false; } if (!m_composerBox->toPlainText().isEmpty()) { return false; } if (!m_mediaFilename.isEmpty()) { return false; } if (m_toAudienceSelector->getRecipientsCount() > 0 || m_ccAudienceSelector->getRecipientsCount() > 0) { return false; } return true; } void Publisher::reportUnreadableFile(QString filename, QFileInfo fileInfo) { QString errorMessage = tr("The selected file cannot be accessed:") + "\n\n" + filename + "\n\n\n"; if (fileInfo.exists()) { errorMessage.append(tr("It is owned by %1.", "%1 = a username").arg(fileInfo.owner()) + "\n\n" + tr("You might not have the necessary " "permissions.")); } else { errorMessage.append(tr("File not found.")); } QMessageBox::warning(this, tr("Error"), errorMessage); qDebug() << "FILE IS NOT READABLE!! " << filename; qDebug() << "Owner: " << fileInfo.owner(); } /********************************************************************/ /***************************** SLOTS ********************************/ /********************************************************************/ void Publisher::setMinimumMode() { qDebug() << "setting Publisher to minimum mode"; m_postButton->setFocus(); // Give focus to button, // in case user shared with Ctrl+Enter m_titleLabel->hide(); m_titleLineEdit->clear(); m_titleLineEdit->hide(); m_toolsButton->hide(); m_draftsButton->hide(); // Disable possible scrollbars m_composerBox->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_composerBox->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Disable Ctrl+Shift+V for plaintext paste, to avoid conflict with Commenters m_composerBox->setPlainPasteEnabled(false); // Keep the publisher from getting focus via tab key, by accident m_composerBox->setFocusPolicy(Qt::ClickFocus); // ~1 row int composerHeight = m_titleLineEdit->fontInfo().pixelSize() * 2; // kinda TMP m_composerBox->setMinimumHeight(composerHeight); m_composerBox->setMaximumHeight(composerHeight); this->setMinimumHeight(composerHeight + 2); this->setMaximumHeight(composerHeight + 2); // Clear formatting options like bold, italic and underline m_composerBox->setCurrentCharFormat(QTextCharFormat()); m_toAudienceSelector->deletePrevious(); m_toAudienceSelector->resetLists(); m_toAudienceSelector->setDefaultAudience(m_globalObject->getPublicPostsByDefault()); m_toAudienceLabel->setText(QStringLiteral("...")); m_toAudienceLabel->repaint(); // Avoid a flicker-like effect later m_toAudienceLabel->clear(); m_toAudienceLabel->hide(); m_toSelectorButton->hide(); m_ccAudienceSelector->deletePrevious(); m_ccAudienceSelector->resetLists(); m_ccAudienceSelector->setDefaultAudience(false); m_ccAudienceLabel->setText(QStringLiteral("...")); m_ccAudienceLabel->repaint(); m_ccAudienceLabel->clear(); m_ccAudienceLabel->hide(); m_ccSelectorButton->hide(); m_statusInfoLabel->clear(); m_statusInfoLabel->hide(); m_addMediaButton->hide(); m_charCounterLabel->hide(); m_postButton->hide(); m_cancelButton->hide(); // Hide "media mode" controls this->cancelMediaMode(); // Clear "editing mode", restore stuff if (m_editingMode) { m_editingMode = false; m_editingPostId.clear(); m_postButton->setText(tr("Post", "verb")); // Button text back to "Post" as usual m_toSelectorButton->setEnabled(true); m_ccSelectorButton->setEnabled(true); } m_draftsManager->updateDraftId(QString()); // Clear draft ID m_fullMode = false; // Re-enable stuff just in case the POST request never finished this->toggleWidgetsWhileSending(true); #ifdef GROUPSUPPORT m_groupIdLabel->hide(); m_groupIdLineEdit->hide(); m_groupIdLineEdit->clear(); #endif } void Publisher::setFullMode() { qDebug() << "setting Publisher to full mode"; m_titleLabel->show(); m_titleLineEdit->show(); m_toolsButton->show(); m_draftsButton->show(); m_composerBox->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_composerBox->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_composerBox->setMaximumHeight(2048); m_composerBox->hideInfoMessage(); this->setMaximumHeight(2048); // i.e. "unlimited" // Re-enable ctrl+shift+V for plaintext paste (disabled to avoid conflicts) m_composerBox->setPlainPasteEnabled(true); // Re-enable normal focus policy, while in full mode m_composerBox->setFocusPolicy(Qt::StrongFocus); m_toSelectorButton->show(); m_toAudienceLabel->show(); updateAudienceToLabels(); m_ccSelectorButton->show(); m_ccAudienceLabel->show(); updateAudienceCcLabels(); // Avoid re-enabling the media button when re-focusing publisher, but still in media mode... if (m_addMediaButton->isHidden()) { m_addMediaButton->setEnabled(true); // If it wasn't hidden, don't re-enable } m_addMediaButton->show(); m_statusInfoLabel->show(); m_charCounterLabel->setVisible(m_globalObject->getShowCharacterCounter()); m_postButton->show(); m_cancelButton->show(); m_composerBox->setFocus(); // In case user used menu or shortcut // instead of clicking on it m_fullMode = true; #ifdef GROUPSUPPORT m_groupIdLabel->show(); m_groupIdLineEdit->show(); #endif } void Publisher::setPictureMode() { m_postType = QStringLiteral("image"); setMediaModeWidgets(); m_pictureLabel->setPixmap(QIcon::fromTheme("image-x-generic", QIcon(":/images/attached-image.png")) .pixmap(200, 150) .scaled(200, 150, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); m_pictureLabel->setToolTip(tr("Picture not set")); m_selectMediaButton->setIcon(QIcon::fromTheme("folder-image", QIcon(":/images/button-open.png"))); m_selectMediaButton->setText(tr("Select Picture...")); m_selectMediaButton->setToolTip(QStringLiteral("") + tr("Find the picture in your folders")); this->findMediaFile(); // Load attachment or show open dialog directly } void Publisher::setAudioMode() { m_postType = QStringLiteral("audio"); setMediaModeWidgets(); m_pictureLabel->setPixmap(QIcon::fromTheme("audio-x-generic", QIcon(":/images/attached-audio.png")) .pixmap(200, 150) .scaled(200, 150, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); m_pictureLabel->setToolTip(tr("Audio file not set")); m_selectMediaButton->setIcon(QIcon::fromTheme("folder-sound", QIcon(":/images/button-open.png"))); m_selectMediaButton->setText(tr("Select Audio File...")); m_selectMediaButton->setToolTip(QStringLiteral("") + tr("Find the audio file in your folders")); this->findMediaFile(); } void Publisher::setVideoMode() { m_postType = QStringLiteral("video"); setMediaModeWidgets(); m_pictureLabel->setPixmap(QIcon::fromTheme("video-x-generic", QIcon(":/images/attached-video.png")) .pixmap(200, 150) .scaled(200, 150, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); m_pictureLabel->setToolTip(tr("Video not set")); m_selectMediaButton->setIcon(QIcon::fromTheme("folder-video", QIcon(":/images/button-open.png"))); m_selectMediaButton->setText(tr("Select Video...")); m_selectMediaButton->setToolTip(QStringLiteral("") + tr("Find the video in your folders")); this->findMediaFile(); } void Publisher::setFileMode() { m_postType = QStringLiteral("file"); setMediaModeWidgets(); m_pictureLabel->setPixmap(QIcon::fromTheme("application-octet-stream", QIcon(":/images/attached-file.png")) .pixmap(200, 150) .scaled(200, 150, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); m_pictureLabel->setToolTip(tr("File not set")); m_selectMediaButton->setIcon(QIcon::fromTheme("folder", QIcon(":/images/button-open.png"))); m_selectMediaButton->setText(tr("Select File...")); m_selectMediaButton->setToolTip("" + tr("Find the file in your folders")); this->findMediaFile(); } /* * Handle drag-and-drop for files. * * Determine which kind of file it is, and set the proper post type for it. * */ void Publisher::onFileDropped(QString fileUrl) { QString fileExtension = fileUrl; fileExtension.remove(QRegExp(".*\\.")); // Remove all but the extension m_mediaFilename = fileUrl; if (MiscHelpers::imageExtensions().contains(fileExtension)) { this->setPictureMode(); } else if (MiscHelpers::audioExtensions().contains(fileExtension)) { this->setAudioMode(); } else if (MiscHelpers::videoExtensions().contains(fileExtension)) { this->setVideoMode(); } else { this->setFileMode(); } } /* * Remove the attachment from the publisher * */ void Publisher::cancelMediaMode() { m_addMediaButton->setEnabled(true); m_pictureLabel->hide(); m_mediaInfoLabel->hide(); m_selectMediaButton->hide(); m_removeMediaButton->hide(); m_uploadProgressBar->hide(); this->setEmptyMediaData(); m_postType = QStringLiteral("note"); } /* * Set Publiser to edit mode, after user clicks on "Edit" in a post * */ void Publisher::setEditingMode(QString postId, QString postType, QString postTitle, QString postText) { // Prevent the "Edit" option from destroying a post currently being composed! if (m_editingMode || m_fullMode) { 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; } m_editingMode = true; m_editingPostId = postId; m_postType = postType; setFullMode(); // Fill in the contents of the post m_titleLineEdit->setText(postTitle); m_composerBox->setText(postText); m_composerBox->moveCursor(QTextCursor::End); // Change/disable some controls m_postButton->setText(tr("Update")); m_toSelectorButton->setDisabled(true); m_ccSelectorButton->setDisabled(true); m_addMediaButton->setDisabled(true); // Clear these, so it doesn't look like the audience is different than when originally posted m_toAudienceSelector->clearPublicAndFollowers(); m_toAudienceLabel->clear(); m_ccAudienceSelector->clearPublicAndFollowers(); m_ccAudienceLabel->clear(); m_statusInfoLabel->setText(tr("Editing post.")); } void Publisher::startMessageForContact(QString id, QString name, QString url) { if (name.trimmed().isEmpty()) // Ensure "name" has some text in it { name = id; } if (m_fullMode) /////// FIXME: maybe use emptyContents() instead { QMessageBox::warning(this, tr("Error: Already composing"), tr("You can't create a message for %1 at " "this time, because a post is already " "being composed.").arg(name)); return; } setFullMode(); // Unselect Public and Followers from To/Cc m_toAudienceSelector->clearPublicAndFollowers(); m_ccAudienceSelector->clearPublicAndFollowers(); // Add user name/ID to the "To" field... m_toAudienceSelector->copyToSelected(QIcon::fromTheme("user-identity", QIcon(":/images/no-avatar.png")), QString("%1 <%2>").arg(name).arg(id), name, id, url); m_toAudienceSelector->setAudience(); // Set a default title... FIXME? m_titleLineEdit->setText(name + ":"); } /* * Add name + id to selected recipients when auto-completing a nick * * 'name' can always be expected to have a value * */ void Publisher::addNickToRecipients(QString id, QString name, QString url, QString listType) { // If for some reason a name is received without an ID, do nothing if (id.trimmed().isEmpty()) { qDebug() << "Trying to add a nick to recipients without an ID!" << name << " - " << url; return; } /* TMP/FIXME: Until audience can be modified when editing, To/Cc buttons * are disabled, and so must be the nick autocompletion. Not actually * disabled, but nick won't be added to the audience lists, since it * wouldn't really do anything. * */ if (m_editingMode) { qDebug() << "Publisher: Can't modify audience in EDITING mode"; return; } if (listType == "to") { m_toAudienceSelector->copyToSelected(QIcon::fromTheme("user-identity", QIcon(":/images/no-avatar.png")), QString("%1 <%2>").arg(name).arg(id), name, id, url); m_toAudienceSelector->setAudience(); } else if (listType == "cc") { m_ccAudienceSelector->copyToSelected(QIcon::fromTheme("user-identity", QIcon(":/images/no-avatar.png")), QString("%1 <%2>").arg(name).arg(id), name, id, url); m_ccAudienceSelector->setAudience(); } } /* * Upon selecting a draft from the Drafts>Load submenu, load it, * unless there's content already present * */ void Publisher::onDraftSelected(QString id, QString title, QString body, QString type, QString attachment, QVariantMap audience, int position) { // Make sure there's nothing in the editor if (emptyContents()) { m_postType = type; if (m_postType.isEmpty()) { m_postType = QStringLiteral("note"); } m_titleLineEdit->setText(title); m_composerBox->setText(body); if (m_postType != QStringLiteral("note")) { m_mediaFilename = attachment; if (m_postType == QStringLiteral("image")) { this->setPictureMode(); } else if (m_postType == QStringLiteral("audio")) { this->setAudioMode(); } else if (m_postType == QStringLiteral("video")) { this->setVideoMode(); } else if (m_postType == QStringLiteral("file")) { this->setFileMode(); } } else { this->cancelMediaMode(); } this->setAudienceFromMap(audience); QTextCursor textCursor = m_composerBox->textCursor(); textCursor.setPosition(position); m_composerBox->setTextCursor(textCursor); m_draftsManager->updateDraftId(id); m_statusInfoLabel->setText(tr("Draft loaded.")); } else { QMessageBox::warning(this, tr("ERROR: Already composing"), tr("You can't load a draft at this time, because " "a post is already being composed.")); // A choice to load it anyway could be given, but could cause // accidents, so taking the less destructive path for now } m_composerBox->setFocus(); } void Publisher::onSaveDraftRequested() { m_draftsManager->saveDraft(m_titleLineEdit->text(), m_composerBox->toHtml(), m_postType, m_mediaFilename, this->getAudienceMap(), m_composerBox->textCursor().position()); m_statusInfoLabel->setText(tr("Draft saved.")); m_composerBox->setFocus(); } void Publisher::onCancelSavingDraftRequested() { this->onSaveDraftRequested(); m_composerBox->erase(); this->setMinimumMode(); } /* * After the post is confirmed to have been received by the server * re-enable publisher, clear text, etc. * */ void Publisher::onPublishingOk() { m_statusInfoLabel->clear(); this->toggleWidgetsWhileSending(true); m_composerBox->erase(); // Done composing message, hide buttons until we get focus again setMinimumMode(); } /* * If there was an HTTP error while posting... * */ void Publisher::onPublishingFailed() { qDebug() << "Posting failed, re-enabling Publisher"; m_statusInfoLabel->setText(tr("Posting failed.\n\nTry again.")); this->toggleWidgetsWhileSending(true); m_uploadProgressBar->hide(); // In media mode, show the "remove" button again (hidden during upload) if (m_postType != QStringLiteral("note")) { m_removeMediaButton->show(); } m_composerBox->setFocus(); } /* * These are called when selecting Public or Followers in the menus * * When selecting "Cc: Followers", "To: Followers" gets unselected, etc. * */ void Publisher::onToPublicSelected() { m_ccAudienceSelector->setPublic(false); // Unselect "Cc: Public" } void Publisher::onToFollowersSelected() { m_ccAudienceSelector->setFollowers(false); // Unselect "Cc: Followers" } void Publisher::onCcPublicSelected() { m_toAudienceSelector->setPublic(false); // Unselect "To: Public" } void Publisher::onCcFollowersSelected() { m_toAudienceSelector->setFollowers(false); // Unselect "To: Followers" } void Publisher::updateAudienceToLabels() { m_toAudienceLabel->setText(m_toAudienceSelector->updatedAudienceLabels()); } void Publisher::updateAudienceCcLabels() { m_ccAudienceLabel->setText(m_ccAudienceSelector->updatedAudienceLabels()); } /* * Fill the "Lists" submenus with PumpController's lists info * * */ void Publisher::updateListsMenus(QVariantList listsList) { m_toAudienceSelector->setListsMenu(listsList); m_ccAudienceSelector->setListsMenu(listsList); } /* * Show the URL of a contact in the recipients list, in the status bar * * FIXME: merge with Post::showHighlightedUrl() * */ void Publisher::showHighlightedUrl(QString url) { if (!url.isEmpty()) { m_pumpController->showTransientMessage(url); qDebug() << "Highlighted recipient url in publisher:" << url; } else { m_pumpController->showTransientMessage(QString()); } } /* * Send the post (note, image, audio...) to the server * */ void Publisher::sendPost() { qDebug() << "Publisher character count:" << m_composerBox->textCursor() .document()->characterCount(); bool textIsEmpty; if (m_composerBox->textCursor().document()->characterCount() > 1) // kinda tmp { textIsEmpty = false; } else ///////// FIXME: use a variation of emptyContents() { textIsEmpty = true; } bool attachmentReadable = !m_mediaFilename.isEmpty(); if (attachmentReadable) { QFileInfo fileInfo(m_mediaFilename); // Check that it actually still exists, before sending if (!fileInfo.exists() || !fileInfo.isReadable()) { this->reportUnreadableFile(m_mediaFilename, fileInfo); attachmentReadable = false; } } // If there's some text in the post, or attached media, send it if ( (m_postType == QStringLiteral("note") && !textIsEmpty) || (m_postType != QStringLiteral("note") && attachmentReadable) || m_editingMode ) // Or editing, then the other stuff doesn't matter { QString postTitle = m_titleLineEdit->text().trimmed(); postTitle.replace("\n", " "); // Post title could have newlines if copy-pasted if (postTitle.length() > 120) // Limit title length to something sane { postTitle = postTitle.left(115) + " [...]"; } QString cleanHtmlString = MiscHelpers::cleanupHtml(m_composerBox->toHtml()); QVariantMap audienceMap = this->getAudienceMap(); // Warn user if posting only to Followers, but having none if (m_onlyToFollowers // Set in getAudienceMap() && m_pumpController->currentFollowersCount() == 0 && !m_editingMode) { qDebug() << "WARNING: You have no followers yet; Post to public?"; int action = QMessageBox::warning(this, tr("Warning: You have no followers yet"), tr("You're trying to post to your followers " "only, but you don't have any followers " "yet.") + QStringLiteral(" ") + tr("If you post like this, no one will be " "able to see your message.") + QStringLiteral("\n\n") + tr("Do you want to make the post public " "instead of followers-only?") + QStringLiteral("\n"), tr("&Yes, make it public"), tr("&No, post to my followers only"), tr("&Cancel, go back to the post"), 2, 2); // Cancel is default, and also activated with ESC if (action == 0) { m_toAudienceSelector->setPublic(true); // Check To:Public audienceMap = this->getAudienceMap(); // Process again qDebug() << "Checked To:Public"; } else if (action == 2) { qDebug() << "Posting aborted; back to post composer"; return; } } // Don't erase just yet!! Just disable until we get "200 OK" from the server. this->toggleWidgetsWhileSending(false); if (!m_editingMode) { m_statusInfoLabel->setText(tr("Posting...")); if (m_postType == QStringLiteral("note")) { m_pumpController->postNote(audienceMap, cleanHtmlString, postTitle); } else { m_uploadNetworkReply = m_pumpController->postMedia(audienceMap, cleanHtmlString, postTitle, m_mediaFilename, m_postType, m_mediaContentType); connect(m_uploadNetworkReply, &QNetworkReply::uploadProgress, this, &Publisher::updateProgressBar); // This will be automatically disconnected when // the QNetworkReply is automatically deleted! // The "remove" button uses the same space as the progress bar m_removeMediaButton->hide(); m_uploadProgressBar->setValue(0); m_uploadProgressBar->show(); } } else { m_statusInfoLabel->setText(tr("Updating...")); m_pumpController->updatePost(m_editingPostId, m_postType, cleanHtmlString, postTitle); } } else { if (m_postType == "note") { m_statusInfoLabel->setText(tr("Post is empty.")); } else // Image, audio... { m_statusInfoLabel->setText(tr("File not selected.")); } qDebug() << "Can't send post: text is empty, or no attachment"; } } void Publisher::onSelectMediaFilePressed() { QString oldMediaFilename = m_mediaFilename; m_mediaFilename.clear(); this->findMediaFile(); // If the dialog to find a file was cancelled, restore if (m_mediaFilename.isEmpty()) { m_mediaFilename = oldMediaFilename; } } void Publisher::updateProgressBar(qint64 sent, qint64 total) { m_uploadProgressBar->setRange(0, total); m_uploadProgressBar->setValue(sent); m_uploadProgressBar->setToolTip(tr("%1 KiB of %2 KiB uploaded") .arg(QLocale::system() .toString(sent / 1024)) .arg(QLocale::system() .toString(total / 1024))); } void Publisher::updateCharacterCounter() { int charCount = m_composerBox->textCursor().document()->characterCount() - 1; m_charCounterLabel->setText(QLocale::system().toString(charCount)); } dianara-v1.4.1/src/pumpcontroller.cpp0000644000175000017500000034364513221336442016003 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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) { m_userAgentString = "Dianara/1.4.1"; m_serverScheme = "https://"; // Default, unless --nohttps is used m_postsPerPageMain = 20; m_postsPerPageOther = 10; m_proxyUsesAuth = false; m_ignoreSslErrors = false; m_ignoreSslInImages = false; m_silentFollows = false; m_silentListsHandling = false; m_silentLikes = false; qDebug() << "PumpController: about to initialize QOAuth.\n" << "** If you get a crash now, make sure your build of QOAuth is " "built with the same Qt version as Dianara."; m_qoauth = new QOAuth::Interface(this); m_qoauth->setRequestTimeout(60000); // 1 minute timeout QSettings settings; m_clientId = settings.value("clientID").toString(); m_clientSecret = settings.value("clientSecret").toString(); m_qoauth->setConsumerKey(m_clientId.toLocal8Bit()); m_qoauth->setConsumerSecret(m_clientSecret.toLocal8Bit()); m_applicationAuthorized = settings.value("isApplicationAuthorized", false).toBool(); if (m_applicationAuthorized) { qDebug() << "Dianara is already authorized for user ID:" << settings.value("userID").toString(); m_token = settings.value("token").toString().toLocal8Bit(); m_tokenSecret = settings.value("tokenSecret").toString().toLocal8Bit(); qDebug() << "Using token" << m_token; qDebug() << "And token secret" << m_tokenSecret.left(5) + "********** (hidden)"; } // Handle responses to HTTP requests connect(&m_nam, &QNetworkAccessManager::finished, this, &PumpController::requestFinished); // Handle SSL errors; ignore for images if option is set, // or for everything if --ignoresslerrors is used connect(&m_nam, &QNetworkAccessManager::sslErrors, this, &PumpController::sslErrorsHandler); m_userFollowingCount = 0; m_userFollowersCount = 0; m_initialDataStep = 0; m_initialDataAttempts = 0; m_initialDataTimer = new QTimer(this); m_initialDataTimer->setSingleShot(false); // Triggered constantly until stopped connect(m_initialDataTimer, &QTimer::timeout, this, &PumpController::getInitialData); m_webfingerCheckTimer = new QTimer(this); m_webfingerCheckTimer->setSingleShot(true); connect(m_webfingerCheckTimer, &QTimer::timeout, this, &PumpController::onValidationTimeout); m_webfingerCheckTimedOut = false; qDebug() << "PumpController created"; } PumpController::~PumpController() { qDebug() << "PumpController destroyed"; } void PumpController::setProxyConfig(QNetworkProxy::ProxyType proxyType, QString hostname, int port, bool useAuth, QString user, QString password) { QNetworkProxy proxy; proxy.setType(proxyType); proxy.setHostName(hostname); proxy.setPort(port); if (useAuth) { proxy.setUser(user); proxy.setPassword(password); // If using auth, and proxy type is set if (proxyType != QNetworkProxy::NoProxy) { m_proxyUsesAuth = true; } } m_nam.setProxy(proxy); m_qoauth->networkAccessManager()->setProxy(proxy); qDebug() << "Proxy config applied:" << hostname << port << user; } bool PumpController::needsProxyPassword() { if (m_proxyUsesAuth && m_nam.proxy().password().isEmpty()) { return true; } return false; } void PumpController::setProxyPassword(QString password) { QNetworkProxy proxy = m_nam.proxy(); proxy.setPassword(password); m_nam.setProxy(proxy); m_qoauth->networkAccessManager()->setProxy(proxy); } void PumpController::updateApiUrls() { m_apiBaseUrl = m_serverScheme + m_serverDomain + "/api/user/" + m_userName; qDebug() << "Base API URL is:" << m_apiBaseUrl; this->m_apiFeedUrl = m_apiBaseUrl + "/feed"; // Outbox } void PumpController::setPostsPerPageMain(int ppp) // TODO: replace with globalObject query { m_postsPerPageMain = ppp; qDebug() << "PumpController: setting postsPerPage (main) to" << m_postsPerPageMain; } void PumpController::setPostsPerPageOther(int ppp) { m_postsPerPageOther = ppp; qDebug() << "PumpController: setting postsPerPage (other) to" << m_postsPerPageOther; } /* * Set new user ID (user@domain.tld) and clear OAuth-related tokens/secrets * */ void PumpController::setNewUserId(QString userId) { m_userId = userId; const QStringList splittedUserId = m_userId.split("@"); m_userName = splittedUserId.first(); // Get username, before @ m_serverDomain = splittedUserId.last(); // Get server domain, after @ qDebug() << "Server to connect:" << m_serverDomain << "; with username:" << m_userName; m_clientId.clear(); m_clientSecret.clear(); m_token.clear(); m_tokenSecret.clear(); m_postsEverSeen.clear(); m_applicationAuthorized = false; emit this->authorizationStatusChanged(m_applicationAuthorized); } /* * Get "pumpserver.org" and "user" from "user@pumpserver.org", * set OAuth token from Account dialog * */ void PumpController::setUserCredentials(QString userId) { m_initialDataTimer->stop(); // Just in case it was running before m_userId = userId; const QStringList splittedUserId = m_userId.split("@"); m_userName = splittedUserId.first(); m_serverDomain = splittedUserId.last(); qDebug() << "New user ID is:" << m_userId; this->updateApiUrls(); emit this->authorizationStatusChanged(m_applicationAuthorized); m_haveProfile = false; m_haveFollowing = false; m_haveFollowers = false; m_havePersonLists = false; m_haveMainTL = false; m_haveDirectTL = false; m_haveActivityTL = false; m_haveFavoritesTL = false; m_haveMainMF = false; m_haveDirectMF = false; m_haveActivityMF = false; m_initialDataStep = 0; m_initialDataAttempts = 0; // This will call getUserProfile(), getContactList(), several getFeed()... if (m_applicationAuthorized) { m_initialDataTimer->start(2000); // Start 2 seconds after setting the ID // (mainly on program startup) emit logMessage(tr("Authorized to use account %1. Getting initial data.") .arg(m_userId)); } else { emit logMessage(tr("There is no authorized account.")); } } QString PumpController::currentUserId() { return m_userId; } QString PumpController::currentUsername() { return m_userName; } QString PumpController::currentServerScheme() { return m_serverScheme; } QString PumpController::currentServerDomain() { return m_serverDomain; } QString PumpController::currentFollowersUrl() { return m_userFollowersUrl; } int PumpController::currentFollowingCount() { return m_userFollowingCount; } int PumpController::currentFollowersCount() { return m_userFollowersCount; } bool PumpController::currentlyAuthorized() { return m_applicationAuthorized; } /* * Get any user's profile (not only our own) * * GET https://pumpserver.example/api/user/username * */ void PumpController::getUserProfile(QString userId) { const QStringList splittedUserId = userId.split("@"); QString url = m_serverScheme + splittedUserId.last() + "/api/user/" + splittedUserId.first(); QNetworkRequest userProfileRequest = this->prepareRequest(url, QOAuth::GET, UserProfileRequest); m_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 = m_apiBaseUrl + "/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); emit currentJobChanged(tr("Updating profile...")); QByteArray data = this->prepareJSON(jsonVariant); m_nam.put(updateProfileRequest, data); qDebug() << "Updating user profile" << fullName << hometown; } /* * Update user's e-mail, used for notifications and such. * This might change in the future. * */ void PumpController::updateUserEmail(QString newEmail, QString password) { QNetworkRequest updateEmailRequest = this->prepareRequest(m_apiBaseUrl, QOAuth::PUT, UpdateEmailRequest); QVariantMap jsonEmailVariant; jsonEmailVariant.insert("email", newEmail); // Changing the e-mail requires providing username (inmutable) and password jsonEmailVariant.insert("nickname", m_userName); jsonEmailVariant.insert("password", password); QByteArray data = this->prepareJSON(jsonEmailVariant); m_nam.put(updateEmailRequest, data); } /* * Add an avatar URL to the queue of pending * */ void PumpController::enqueueAvatarForDownload(QString url) { if (QFile::exists(MiscHelpers::getCachedAvatarFilename(url)) || m_pendingAvatarsList.contains(url)) { qDebug() << "PumpController() Using cached avatar, or it is pending download..."; } else { m_pendingAvatarsList.append(url); this->getAvatar(url); qDebug() << "PumpController() Avatar not cached, downloading" << url; } } /* * Add the URL of an image to the queue of pending-download * * connect() signal/slot if necessary to refresh when done * */ void PumpController::enqueueImageForDownload(QString url) { if (QFile::exists(MiscHelpers::getCachedImageFilename(url)) || m_pendingImagesList.contains(url)) { qDebug() << "PumpController::enqueueImageForDownload(), " "Using cached image, or requested image is pending download..."; } else { m_pendingImagesList.append(url); this->getImage(url); qDebug() << "PumpController::enqueueImageForDownload(), " "image not cached, downloading" << url; } } void PumpController::getAvatar(QString avatarUrl) { if (avatarUrl.isEmpty()) { return; } qDebug() << "Getting avatar" << avatarUrl; QNetworkRequest avatarRequest(QUrl((const QString)avatarUrl)); avatarRequest.setRawHeader("User-Agent", m_userAgentString); avatarRequest.setAttribute(QNetworkRequest::User, QVariant(AvatarRequest)); m_nam.get(avatarRequest); } void PumpController::getImage(QString imageUrl) { if (imageUrl.isEmpty()) { return; } QNetworkRequest imageRequest = this->prepareRequest(imageUrl, QOAuth::GET, ImageRequest); /* * Follow HTTP redirects. Only for Qt 5.6 and newer. * Won't work if redirection is from https:// to http:// URLs. * * TODO -- FIXME: This attribute was obsoleted by * QNetworkRequest::RedirectPolicyAttribute in Qt 5.9 */ imageRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); imageRequest.setAttribute(QNetworkRequest::Attribute(QNetworkRequest::User + 1), QVariant(imageUrl)); // Track original URL m_nam.get(imageRequest); qDebug() << "getImage() imageRequest sent"; } QNetworkReply *PumpController::getMedia(QString mediaUrl) { QNetworkRequest mediaRequest = this->prepareRequest(mediaUrl, QOAuth::GET, MediaRequest); qDebug() << "getMedia() sending mediaRequest for:" << mediaUrl; return m_nam.get(mediaRequest); } void PumpController::notifyAvatarStored(QString avatarUrl, QString avatarFilename) { m_pendingAvatarsList.removeAll(avatarUrl); emit avatarStored(avatarUrl, avatarFilename); } void PumpController::notifyImageStored(QString imageUrl) { m_pendingImagesList.removeAll(imageUrl); emit imageStored(imageUrl); } void PumpController::notifyImageFailed(QString imageUrl) { m_pendingImagesList.removeAll(imageUrl); emit imageFailed(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 = m_apiBaseUrl + "/" + listType; QOAuth::ParamMap paramMap; paramMap.insert("count", "200"); // 200 each time (max allowed by API) paramMap.insert("offset", QString("%1").arg(offset).toLocal8Bit()); QNetworkRequest contactListRequest; if (listType == "following") { contactListRequest = this->prepareRequest(url, QOAuth::GET, FollowingListRequest, paramMap); if (offset == 0) { m_totalReceivedFollowing = 0; m_followingIdList.clear(); this->showStatusMessageAndLogIt(tr("Getting list of 'Following'...")); } } else { contactListRequest = this->prepareRequest(url, QOAuth::GET, FollowersListRequest, paramMap); if (offset == 0) { m_totalReceivedFollowers = 0; } this->showStatusMessageAndLogIt(tr("Getting list of 'Followers'...")); } m_nam.get(contactListRequest); } void PumpController::getSiteUserList() { QString url = m_serverScheme + m_serverDomain + "/api/users"; QOAuth::ParamMap paramMap; paramMap.insert("count", "50"); // 50 users instead of the server default QNetworkRequest siteUsersRequest = this->prepareRequest(url, QOAuth::GET, SiteUserListRequest, paramMap); emit currentJobChanged(tr("Getting site users for %1...", "%1 is a server name").arg(m_serverDomain)); m_nam.get(siteUsersRequest); } /* * Check if a user ID is in the "following" list * */ bool PumpController::userInFollowing(QString contactId) { if (m_followingIdList.contains(contactId)) { return true; } else { return false; } } void PumpController::updateInternalFollowingIdList(QStringList idList) { m_followingIdList.append(idList); } void PumpController::removeFromInternalFollowingList(QString id) { m_followingIdList.removeAll(id); } /* * GET https://pumpserver.example/api/user/username/lists/person * */ void PumpController::getListsList() { qDebug() << "Getting list of lists"; QString url = m_apiBaseUrl + "/lists/person"; QOAuth::ParamMap paramMap; paramMap.insert("count", "200"); // Get up to 200 lists QNetworkRequest listsListRequest = this->prepareRequest(url, QOAuth::GET, ListsListRequest, paramMap); emit currentJobChanged(tr("Getting list of person lists...")); m_nam.get(listsListRequest); } /* * Create a person list * */ void PumpController::createPersonList(QString name, QString description) { qDebug() << "PumpController() creating person list:" << name; QNetworkRequest postRequest = this->prepareRequest(this->m_apiFeedUrl, 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); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; emit currentJobChanged(tr("Creating person list...")); m_nam.post(postRequest, data); } void PumpController::deletePersonList(QString listId) { qDebug() << "PumpController::deletePersonList() deleting list" << listId; QNetworkRequest deleteRequest = this->prepareRequest(this->m_apiFeedUrl, QOAuth::POST, DeletePersonListRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "collection"); jsonVariantObject.insert("id", listId); QVariantMap jsonVariant; jsonVariant.insert("verb", "delete"); jsonVariant.insert("object", jsonVariantObject); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "about to POST:" << data; emit currentJobChanged(tr("Deleting person list...")); m_nam.post(deleteRequest, data); } void PumpController::getPersonList(QString url) { qDebug() << "Getting a person list:" << url; QOAuth::ParamMap paramMap; paramMap.insert("count", "200"); // Get 200 members // FIXME: could be more QNetworkRequest personListRequest = this->prepareRequest(url, QOAuth::GET, PersonListRequest, paramMap); emit currentJobChanged(tr("Getting a person list...")); m_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; QNetworkRequest postRequest = this->prepareRequest(this->m_apiFeedUrl, 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); if (m_silentListsHandling) // In 'private' mode, address to the same as the object { QVariantList jsonAudienceTo; jsonAudienceTo.append(jsonVariantObject); // To: only the specific user jsonVariant.insert("to", jsonAudienceTo); } const QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; emit currentJobChanged(tr("Adding person to list...")); m_nam.post(postRequest, data); } /* * Remove member from a list * */ void PumpController::removePersonFromList(QString listId, QString personId) { qDebug() << "PumpController() removing person from list:" << personId << listId; QNetworkRequest postRequest = this->prepareRequest(this->m_apiFeedUrl, 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); if (m_silentListsHandling) // 'private' mode { QVariantList jsonAudienceTo; jsonAudienceTo.append(jsonVariantObject); jsonVariant.insert("to", jsonAudienceTo); } const QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; emit currentJobChanged(tr("Removing person from list...")); m_nam.post(postRequest, data); } /* * Create a group where users can join and post messages for the other * members, similar to the StatusNet groups. * */ void PumpController::createGroup(QString name, QString summary, QString description) { qDebug() << "PumpController() creating group:" << name; QNetworkRequest postRequest = this->prepareRequest(this->m_apiFeedUrl, QOAuth::POST, CreateGroupRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "group"); if (!name.isEmpty()) { jsonVariantObject.insert("displayName", name); } jsonVariantObject.insert("summary", summary); jsonVariantObject.insert("content", description); QVariantMap jsonVariant; jsonVariant.insert("verb", "create"); jsonVariant.insert("object", jsonVariantObject); // Audience, To:Public // Groups created public ATM -- FIXME QVariantMap jsonVariantPublic; jsonVariantPublic.insert("objectType", "collection"); jsonVariantPublic.insert("id", "http://activityschema.org/collection/public"); QVariantList jsonVariantAudience; jsonVariantAudience.append(jsonVariantPublic); jsonVariant.insert("to", jsonVariantAudience); // To: Public QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; emit currentJobChanged(tr("Creating group...")); m_nam.post(postRequest, data); } /* * Join a group to be able to send messages to it. * Joining based on ID. * */ void PumpController::joinGroup(QString id) { qDebug() << "PumpController() joining group:" << id; QNetworkRequest postRequest = this->prepareRequest(this->m_apiFeedUrl, QOAuth::POST, JoinGroupRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "group"); jsonVariantObject.insert("id", id); QVariantMap jsonVariant; jsonVariant.insert("verb", "join"); jsonVariant.insert("object", jsonVariantObject); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; emit currentJobChanged(tr("Joining group...")); m_nam.post(postRequest, data); } void PumpController::leaveGroup(QString id) { qDebug() << "PumpController() leaving group:" << id; QNetworkRequest postRequest = this->prepareRequest(this->m_apiFeedUrl, QOAuth::POST, LeaveGroupRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "group"); jsonVariantObject.insert("id", id); QVariantMap jsonVariant; jsonVariant.insert("verb", "leave"); jsonVariant.insert("object", jsonVariantObject); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; emit currentJobChanged(tr("Leaving group...")); m_nam.post(postRequest, data); } /* * 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); m_nam.get(likesRequest); } /* * Get comments for one specific post * * GET https://pumpserver.example/api/#objectType#/#id#/replies * or proxyed URL. URL is given by the post itself * */ void PumpController::getPostComments(QString postCommentsUrl, QString postId) { qDebug() << "Getting comments for post" << postCommentsUrl; if (!urlIsInOurHost(postCommentsUrl)) // Post in another server and no proxyURL { emit currentJobChanged(tr("The comments for this post cannot be loaded " "due to missing data on the server.")); return; } 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); commentsRequest.setAttribute(QNetworkRequest::Attribute(QNetworkRequest::User + 1), QVariant(postId)); m_nam.get(commentsRequest); } /* * Get list of people who shared a specific post * */ void PumpController::getPostShares(QString postSharesUrl) { // TODO } /* * Get a feed * * Major timelines: "Timeline", "Messages", "Activity" and "Favorites" * * GET https://pumpserver.example/api/username/inbox/major * * /inbox/direct/major = Direct timeline, posts with the user's address in * the "To:" field, that is, sent explicitly to the user * * /feed/major = Activity timeline, user's own posts * * /favorites = Favorites timeline, posts where user clicked "like" * * Minor feeds: "Meanwhile", "Mentions" and "Actions" * * GET https://pumpserver.example/api/username/inbox/minor * /inbox/direct/minor * /feed/minor * */ void PumpController::getFeed(PumpController::requestTypes feedType, int itemCount, QString url, int feedOffset) { // Name of the feed and API path QStringList feedNameAndPath = this->getFeedNameAndPath(feedType); qDebug() << "PumpController::getFeed() " << feedNameAndPath; emit currentJobChanged(tr("Getting '%1'...", "%1 is the name of a feed") .arg(feedNameAndPath.first())); QOAuth::ParamMap paramMap; paramMap.insert("count", QString("%1").arg(itemCount).toLocal8Bit()); if (feedOffset != 0) { paramMap.insert("offset", QString("%1").arg(feedOffset).toLocal8Bit()); } if (url.isEmpty()) { url = m_apiBaseUrl + feedNameAndPath.last(); } else { if (url.contains("?")) // Only if there are parameters in the URL { // FIXME: This should be made safer QStringList splitUrl = url.split("?"); url = splitUrl.first(); QStringList splitParams = splitUrl.last().split("="); paramMap.insert(splitParams.first().toUtf8(), // 'before' or 'since' splitParams.last().toUtf8()); // An activity ID } } QNetworkRequest feedRequest = this->prepareRequest(url, QOAuth::GET, feedType, paramMap); /* qDebug() << "PumpController::getFeed() ****\nfeedRequest:\n"; qDebug() << feedRequest.rawHeader("Authorization"); qDebug() << "\n*\n\nURL: " << feedRequest.url().toString() << "\n\n*******"; */ m_nam.get(feedRequest); } QStringList PumpController::getFeedNameAndPath(int feedType) { QStringList nameAndPath; switch (feedType) { case MainTimelineRequest: nameAndPath << tr("Timeline") << QStringLiteral("/inbox/major"); break; case DirectTimelineRequest: nameAndPath << tr("Messages") << QStringLiteral("/inbox/direct/major"); break; case ActivityTimelineRequest: nameAndPath << tr("Activity") << QStringLiteral("/feed/major"); break; case FavoritesTimelineRequest: nameAndPath << tr("Favorites") << QStringLiteral("/favorites"); break; case MinorFeedMainRequest: nameAndPath << tr("Meanwhile") << QStringLiteral("/inbox/minor"); break; case MinorFeedDirectRequest: nameAndPath << tr("Mentions") << QStringLiteral("/inbox/direct/minor"); break; case MinorFeedActivityRequest: nameAndPath << tr("Actions") << QStringLiteral("/feed/minor"); break; case UserTimelineRequest: nameAndPath << tr("User timeline") << QString(); //// FIXME? break; default: nameAndPath << QString() << QString(); qDebug() << "PumpController::getFeedNameAndPath() wrong feed type!"; } return nameAndPath; } QString PumpController::getFeedApiUrl(int feedType) { QString fullFeedUrl = m_apiBaseUrl; fullFeedUrl.append(this->getFeedNameAndPath(feedType).last()); return fullFeedUrl; } /* * Prepare a QNetworkRequest with OAuth header, content type and user agent. * */ QNetworkRequest PumpController::prepareRequest(QString url, QOAuth::HttpMethod method, requestTypes requestType, QOAuth::ParamMap paramMap, QString contentTypeString) { QByteArray authHeader = m_qoauth->createParametersString(url, method, m_token, m_tokenSecret, QOAuth::HMAC_SHA1, paramMap, QOAuth::ParseForHeaderArguments); //qDebug() << "QOAuth::error()" << m_qoauth->error() << " (200=OK)"; QNetworkRequest request; // Don't append inline parameters if they're empty, that can mess up things if (!paramMap.isEmpty()) { url.append(m_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() == m_serverDomain) { // Un-percent-encode the URL; needed to avoid "invalid signature" error // when parameters have special chars like % or / url = QByteArray::fromPercentEncoding(url.toLocal8Bit()); request.setUrl(QUrl(url)); request.setRawHeader("Authorization", authHeader); } if (!contentTypeString.isEmpty()) { request.setHeader(QNetworkRequest::ContentTypeHeader, contentTypeString); } request.setRawHeader("User-Agent", m_userAgentString); request.setAttribute(QNetworkRequest::User, QVariant(requestType)); return request; } /* * Generate JSON plaintext from a VarianMap. * */ QByteArray PumpController::prepareJSON(QVariantMap jsonVariantMap) { QJsonDocument jsonDocument; jsonDocument = QJsonDocument::fromVariant(jsonVariantMap); return jsonDocument.toJson(); } /* * Read JSON and generate a QVariantMap * */ QVariantMap PumpController::parseJSON(QByteArray rawData, bool *parsedOk) { QVariantMap jsonData; bool parsed; QJsonDocument jsonDocument; jsonDocument = QJsonDocument::fromJson(rawData); jsonData = jsonDocument.toVariant().toMap(); parsed = !jsonDocument.isNull(); // tmp? FIXME *parsedOk = parsed; return jsonData; } /* * Upload a file to the /uploads feed for the user * * Used to upload pictures, audio, video and misc files * */ QNetworkReply *PumpController::uploadFile(QString filename, QString contentType, requestTypes uploadType) { qDebug() << "PumpController::uploadFile()" << filename << contentType; QString url = m_apiBaseUrl + "/uploads"; QNetworkRequest postRequest = this->prepareRequest(url, QOAuth::POST, uploadType, QOAuth::ParamMap(), contentType); QFile file(filename); file.open(QIODevice::ReadOnly); QByteArray data = file.readAll(); // FIXME: This can use a lot of RAM with big files file.close(); this->showStatusMessageAndLogIt(tr("Uploading %1", "1=filename").arg(filename) + QString(" (%1, %2)") .arg(contentType) .arg(MiscHelpers::fileSizeString(filename))); return m_nam.post(postRequest, data); } bool PumpController::urlIsInOurHost(QString url) { return (QUrl(url).host() == m_serverDomain); } void PumpController::addCommentUrlToSeenList(QString id, QString url) { if (urlIsInOurHost(url)) { m_postsEverSeen.insert(id, url); } } QString PumpController::commentsUrlForPost(QString id) { return m_postsEverSeen.value(id).toString(); } void PumpController::showTransientMessage(QString message) { emit transientStatusBarMessage(message); } void PumpController::showStatusMessageAndLogIt(QString message, QString url) { emit currentJobChanged(message); emit logMessage(message, url); } /* * Show a message in the status bar with a snippet from a post or comment, * or its title, and add it to the log. * */ void PumpController::showObjectSnippetAndLogIt(QString message, QVariantMap jsonMap, QString messageWhenTitled) { QString title = jsonMap.value("object").toMap() .value("displayName").toString().trimmed(); QString snippet; if (title.isEmpty()) { QString content = jsonMap.value("object").toMap() .value("content").toString(); snippet = MiscHelpers::htmlToPlainText(content, 40); // Limit to 40 chars } else { if (!messageWhenTitled.isEmpty()) { message = messageWhenTitled; } snippet = title; } emit currentJobChanged(message.arg("\"" + snippet + "\"")); emit logMessage(message.arg(""" + snippet + """)); } void PumpController::setIgnoreSslErrors(bool state) { m_ignoreSslErrors = state; m_qoauth->setIgnoreSslErrors(state); } void PumpController::setIgnoreSslInImages(bool state) { m_ignoreSslInImages = state; } void PumpController::setNoHttpsMode() { m_serverScheme = "http://"; // Instead of the default https:// this->updateApiUrls(); // Update API base URL with this scheme } void PumpController::setSilentFollows(bool state) { m_silentFollows = state; } void PumpController::setSilentLists(bool state) { m_silentListsHandling = state; } void PumpController::setSilentLikes(bool state) { m_silentLikes = state; } void PumpController::updatePostsEverSeen(QVariantMap postMap) { m_postsEverSeen = postMap; } QVariantMap PumpController::getPostsEverSeen() { return m_postsEverSeen; } /***************************************************************************/ /***************************************************************************/ /***************************************************************************/ /***************************************************************************/ /*********************************** SLOTS *********************************/ /***************************************************************************/ /***************************************************************************/ /***************************************************************************/ /***************************************************************************/ void PumpController::requestFinished(QNetworkReply *reply) { const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) .toInt(); const int requestType = reply->request() .attribute(QNetworkRequest::User).toInt(); const QString requestExtraString = reply->request() .attribute(QNetworkRequest::Attribute(QNetworkRequest::User + 1)) .toString(); const QString replyUrl = reply->url().toString(); const QString replyHost = reply->url().host(); const QString replyErrorString = reply->errorString(); const QString replyServerVersion = reply->rawHeader(QByteArrayLiteral("Server")); const QByteArray replyFirstLineRaw = reply->readLine(); // As of Pump.io 1.x, errors have
              's const QByteArray replyData = replyFirstLineRaw + reply->readAll(); QString replyFirstLine; QString prettyLogMessage; qDebug() << "Request finished. HTTP code:" << httpCode; qDebug() << "Size:" << replyData.length() << "bytes; URL:" << replyUrl; qDebug() << "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 { replyFirstLine = QString(replyFirstLineRaw); int endOfReplyFirstLine = replyFirstLine.indexOf(QStringLiteral("
              "), 0, Qt::CaseInsensitive); if (endOfReplyFirstLine > 0) { replyFirstLine.truncate(endOfReplyFirstLine); // Cut at first
              } if (replyData.startsWith(QByteArrayLiteral("{"))) // Looks like JSON containing an error message { qDebug() << "Parsing JSON error message"; bool jsonErrorParsed = false; QVariantMap jsonError = this->parseJSON(replyData, &jsonErrorParsed); if (jsonErrorParsed) { // Since it's a JSON-encoded error message, use it instead of // the first data line; These can have Unicode symbols QString jsonErrorString = jsonError.value("error").toString() .trimmed(); if (!jsonErrorString.isEmpty()) { replyFirstLine = jsonErrorString; } } } else if (replyData.startsWith(QByteArrayLiteral("<"))) // Looks like HTML { qDebug() << "Parsing HTML error message"; QTextDocument replyHtml; replyHtml.setHtml(replyData); QString htmlErrorString = replyHtml.toPlainText(); int firstBreakPos = htmlErrorString.indexOf("\n"); if (firstBreakPos > 0) { htmlErrorString.truncate(firstBreakPos); } if (!htmlErrorString.isEmpty()) { replyFirstLine = htmlErrorString; } } if (requestType == PublishPostRequest) { emit postPublishingFailed(); } else if (requestType == UpdatePostRequest) { emit postPublishingFailed(); // kinda TMP } else if (requestType == CommentPostRequest) { emit commentPostingFailed(requestExtraString); } else if (requestType == UpdateCommentRequest) { emit commentPostingFailed(requestExtraString); // also, kinda TMP } else if (requestType == UploadMediaForPostRequest) { emit postPublishingFailed(); // FIXME? } else if (requestType == MediaRequest) { emit downloadFailed(replyUrl); } else if (requestType == ImageRequest) { this->notifyImageFailed(replyUrl); } else if (requestType == MainTimelineRequest || requestType == DirectTimelineRequest || requestType == ActivityTimelineRequest || requestType == FavoritesTimelineRequest) { emit currentJobChanged(tr("Error loading timeline!")); // FIXME: gets overwritten immediately emit timelineFailed(requestType); } else if (requestType == UserTimelineRequest) // FIXME, merge with previous? { emit userTimelineFailed(); } else if (requestType == MinorFeedMainRequest || requestType == MinorFeedDirectRequest || requestType == MinorFeedActivityRequest) { emit currentJobChanged(tr("Error loading minor feed!")); // FIXME: gets overwritten immediately emit minorFeedFailed(requestType); } else if (requestType == PostCommentsRequest) { emit commentsNotReceived(requestExtraString); } else if (requestType == CheckContactRequest) { m_webfingerCheckTimer->stop(); // Timeout checking is irrelevant at this point qDebug() << "Contact ID is not valid, or server is down:" << replyUrl; this->showStatusMessageAndLogIt(tr("Unable to verify the address!") + " (" + m_addressPendingToFollow + ")"); emit contactVerified(m_addressPendingToFollow, httpCode, m_webfingerCheckTimedOut, replyServerVersion); m_addressPendingToFollow.clear(); return; // Avoid showing ugly 404 error } } const QString httpErrorString = tr("HTTP error", "For the following HTTP error codes" "you can check " "http://en.wikipedia.org/wiki/List_of_HTTP_status_codes " "in your language") + ": "; QString errorTypeString; switch (httpCode) { //////////////////////////////////////////////// First, handle error codes case 504: errorTypeString = httpErrorString + tr("Gateway Timeout", "HTTP 504 error string") + " (504)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); qDebug() << "HTTP 504: Gateway Timeout."; qDebug() << "Data: " << replyData; return; case 503: errorTypeString = httpErrorString + tr("Service Unavailable", "HTTP 503 error string") + " (503) - " + replyFirstLine; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); // if not just an image, it's important, so popup notification too if (requestType != ImageRequest && requestType != AvatarRequest) { emit showErrorNotification(errorTypeString + "\n" + replyUrl); } qDebug() << "HTTP 503: Service Unavailable."; qDebug() << "Data: " << replyData; return; case 502: errorTypeString = httpErrorString + tr("Bad Gateway", "HTTP 502 error string") + " (502) - " + replyFirstLine; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); qDebug() << "HTTP 502: Bad Gateway."; qDebug() << "Data: " << replyData; return; case 501: errorTypeString = httpErrorString + tr("Not Implemented", "HTTP 501 error string") + " (501) - " + replyFirstLine; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); qDebug() << "HTTP 501: Not Implemented."; qDebug() << "Data: " << replyFirstLineRaw; return; case 500: errorTypeString = httpErrorString + tr("Internal Server Error", "HTTP 500 error string") + " (500) - " + replyFirstLine; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); // if not just an image, it's important, so popup notification too if (requestType != ImageRequest && requestType != AvatarRequest) { emit showErrorNotification(errorTypeString + "\n" + replyUrl); } qDebug() << "HTTP 500: Internal Server Error."; qDebug() << "Data: " << replyData; return; case 410: errorTypeString = httpErrorString + tr("Gone", "HTTP 410 error string") + " (410) " + replyFirstLine; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); qDebug() << "HTTP 410: Gone."; qDebug() << "Data: " << replyFirstLineRaw; return; case 404: errorTypeString = httpErrorString + tr("Not Found", "HTTP 404 error string") + " (404)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); qDebug() << "HTTP 404: Not Found."; qDebug() << "Data: " << replyFirstLineRaw; return; case 403: errorTypeString = httpErrorString + tr("Forbidden", "HTTP 403 error string") + " (403) - " + replyFirstLine; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); qDebug() << "HTTP 403: Forbidden."; qDebug() << "Data: " << replyFirstLineRaw; return; case 401: errorTypeString = httpErrorString + tr("Unauthorized", "HTTP 401 error string") + " (401) - " + replyFirstLine; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); qDebug() << "HTTP 401: Unauthorized."; qDebug() << "Data: " << replyFirstLineRaw; return; case 400: errorTypeString = httpErrorString + tr("Bad Request", "HTTP 400 error string") + " (400) - " + replyFirstLine; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); // if not just an image, it's important, so popup notification too if (requestType != ImageRequest && requestType != AvatarRequest) { emit showErrorNotification(errorTypeString + "\n" + replyUrl); } qDebug() << "HTTP 400: Bad Request."; qDebug() << "Data: " << replyData; return; case 302: errorTypeString = httpErrorString + tr("Moved Temporarily", "HTTP 302 error string") + " (302)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); qDebug() << "HTTP 302: Moved Temporarily."; qDebug() << "Data: " << replyFirstLineRaw; return; case 301: errorTypeString = httpErrorString + tr("Moved Permanently", "HTTP 301 error string") + " (301)"; emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, replyUrl); qDebug() << "HTTP 301: Moved Permanently."; qDebug() << "Data: " << replyFirstLineRaw; return; case 0: // Other kinds of network errors errorTypeString = tr("Error connecting to %1").arg(replyHost); emit currentJobChanged(errorTypeString + ": " + replyErrorString); emit logMessage(errorTypeString + ": " + replyErrorString, replyUrl); qDebug() << "Error connecting to" << replyHost << ": " << replyErrorString; return; // Other HTTP codes default: errorTypeString = tr("Unhandled HTTP error code %1").arg(httpCode); emit currentJobChanged(errorTypeString + ": " + replyUrl); emit logMessage(errorTypeString, 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 // Stuff for JSON parsing bool jsonParsedOK = false; QVariantMap jsonData; QVariantList jsonDataList; // Unless it was an AvatarRequest, ImageRequest or MediaRequest, // it should be JSON, so parse it if (requestType != AvatarRequest && requestType != ImageRequest && requestType != MediaRequest) { jsonData = this->parseJSON(replyData, &jsonParsedOK); 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) { m_clientId = jsonData.value("client_id").toString(); m_clientSecret = jsonData.value("client_secret").toString(); // FIXME: error control, etc. // check if jsonData.keys().contains("client_id") !! QSettings settings; settings.setValue("clientID", m_clientId); settings.setValue("clientSecret", m_clientSecret); settings.sync(); this->getToken(); } break; case UserProfileRequest: qDebug() << "A user profile was requested"; if (jsonParsedOK && jsonData.size() > 0) { QVariantMap profileMap = jsonData.value("profile").toMap(); if (profileMap.value("id").toString() == "acct:" + m_userId) { qDebug() << "Received OWN profile; keys:" << profileMap.keys(); qDebug() << "Links from profile:" << profileMap.value("links").toMap().keys(); qDebug() << "Lists from profile:" << profileMap.value("lists").toMap() .value("totalItems").toInt(); qDebug() << "Lists URL:" << profileMap.value("lists").toMap() .value("url").toString(); m_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.value("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 m_userFollowersUrl = profileMap.value("followers").toMap() .value("url").toString(); m_userFollowingCount = profileMap.value("following").toMap() .value("totalItems").toInt(); m_userFollowersCount = profileMap.value("followers").toMap() .value("totalItems").toInt(); qDebug() << "Following count:" << m_userFollowingCount; qDebug() << "Followers count:" << m_userFollowersCount; this->logMessage(tr("Server version: %1") .arg(replyServerVersion)); this->showStatusMessageAndLogIt(tr("Profile received.") + " " + tr("Followers") + QString(": %1;") .arg(QLocale::system() .toString(m_userFollowersCount)) + " " + tr("Following") + QString(": %1") .arg(QLocale::system() .toString(m_userFollowingCount))); } else { qDebug() << "Expected profile information; got:"; qDebug() << replyData; } } break; case UpdateProfileRequest: this->showStatusMessageAndLogIt(tr("Profile updated.")); this->getUserProfile(m_userId); emit userDidSomething(); break; case UpdateEmailRequest: this->showStatusMessageAndLogIt(tr("E-mail updated: %1") .arg(jsonData.value("email") .toString())); this->getUserProfile(m_userId); // TMP, FIXME... break; //////////////////////////////////// case PublishPostRequest: if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; QString objectId = jsonData.value("object").toMap() .value("id").toString(); qDebug() << "Post ID:" << objectId; QString objectType = jsonData.value("object").toMap() .value("objectType").toString(); if (objectType == "image" || objectType == "audio" || objectType == "video" || objectType == "file") { QString translatedObjectType = ASObject::getTranslatedType(objectType); this->showStatusMessageAndLogIt(tr("%1 published successfully. " "Updating post content...", "%1 is the type of object: " "note, image...") .arg(translatedObjectType)); // Update the object with title and description // (workaround for the non-title-non-description issue) this->updatePost(objectId, objectType, m_currentPostDescription, m_currentPostTitle); } else { // Not a a media post, notify "posted OK" this->showObjectSnippetAndLogIt(tr("Untitled post %1 " "published successfully.", "%1 is a piece of the post"), jsonData, tr("Post %1 published successfully.", "%1 is the title of the post")); emit postPublished(); qDebug() << "Non-media post published correctly"; } } else { qDebug() << "Error parsing received JSON data!"; qDebug() << "Raw data:" << replyData; // JSON directly } break; case PublishAvatarRequest: this->showStatusMessageAndLogIt(tr("Avatar published successfully.")); if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; QString imageUrl = jsonData.value("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: if (jsonParsedOK && jsonData.size() > 0) { this->showObjectSnippetAndLogIt(tr("Untitled post %1 " "updated successfully.", "%1 is a piece of the post"), jsonData, tr("Post %1 updated successfully.", "%1 is the title of the post")); } emit postPublished(); break; case UpdateCommentRequest: if (jsonParsedOK && jsonData.size() > 0) { this->showObjectSnippetAndLogIt(tr("Comment %1 updated successfully.", "%1 is a piece of the comment"), jsonData); } emit commentPosted(requestExtraString); emit userDidSomething(); break; ///////////////////////////////////// If liking a post was requested case LikePostRequest: this->showStatusMessageAndLogIt(tr("Message liked or unliked " "successfully.")); emit likeSet(); emit userDidSomething(); 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: if (jsonParsedOK && jsonData.size() > 0) { this->showObjectSnippetAndLogIt(tr("Comment %1 posted successfully.", "%1 is a piece of the comment"), jsonData); } emit commentPosted(requestExtraString); // This will be caught by Commenter() emit userDidSomething(); 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"; jsonDataList = jsonData["items"].toList(); int commentCount = jsonDataList.size(); qDebug() << "Number of items in comments list:" << commentCount; if (commentCount == 1) { emit currentJobChanged(tr("1 comment received.")); } else { emit currentJobChanged(tr("%1 comments received.") .arg(commentCount)); } emit commentsReceived(jsonDataList, replyUrl); } else { qDebug() << "Error parsing received comment JSON data!"; qDebug() << "Raw data:" << replyData; } break; case SharePostRequest: qDebug() << "Post shared OK"; if (jsonParsedOK && jsonData.size() > 0) { // FIXME: this should be done using ASActivity+ASObject QVariantMap authorMap = jsonData.value("object").toMap().value("author").toMap(); QString authorName = authorMap.value("displayName").toString(); if (authorName.isEmpty()) { authorName = ASPerson::cleanupId(authorMap.value("id").toString()); } this->showStatusMessageAndLogIt(tr("Post by %1 shared successfully.", "1=author of the post we are sharing") .arg(authorName)); emit userDidSomething(); } break; case PostSharesRequest: // TODO break; /////////////////////////////////////////////// If a feed was requested case MainTimelineRequest: // just jump to the next case DirectTimelineRequest: // just jump to next case ActivityTimelineRequest: // just... yeah, jump case FavoritesTimelineRequest: // and jump! case MinorFeedMainRequest: // jump! case MinorFeedDirectRequest: // jump, jump, jump!! case MinorFeedActivityRequest: // everybody jump! case UserTimelineRequest: prettyLogMessage = tr("Received '%1'.", "%1 is the name of a feed") .arg(this->getFeedNameAndPath(requestType).first()); if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; jsonDataList = jsonData["items"].toList(); qDebug() << "Number of items in this feed block:" << jsonDataList.size(); if (jsonDataList.size() > 0) { prettyLogMessage.append(" " + tr("Adding items...")); } // Emit this before the other signals emit currentJobChanged(prettyLogMessage); int totalItems = jsonData.value("totalItems").toInt(); QString previousLink = jsonData.value("links").toMap() .value("prev").toMap() .value("href").toString(); QString nextLink = jsonData.value("links").toMap() .value("next").toMap() .value("href").toString(); if (requestType == MainTimelineRequest) { qDebug() << "It was the main timeline"; m_haveMainTL = true; emit mainTimelineReceived(jsonDataList, previousLink, nextLink, totalItems); } else if (requestType == DirectTimelineRequest) { qDebug() << "It was the direct messages timeline"; m_haveDirectTL = true; emit directTimelineReceived(jsonDataList, previousLink, nextLink, totalItems); } else if (requestType == ActivityTimelineRequest) { qDebug() << "It was the own activity timeline"; m_haveActivityTL = true; emit activityTimelineReceived(jsonDataList, previousLink, nextLink, totalItems); } else if (requestType == FavoritesTimelineRequest) { qDebug() << "It was the favorites timeline"; m_haveFavoritesTL = true; emit favoritesTimelineReceived(jsonDataList, previousLink, nextLink, totalItems); } else if (requestType == UserTimelineRequest) { QString timelineUrl = jsonData.value("url").toString(); qDebug() << "It was a user timeline:" << timelineUrl; emit userTimelineReceived(jsonDataList, previousLink, nextLink, totalItems, timelineUrl); } else if (requestType == MinorFeedMainRequest) { qDebug() << "It was the Meanwhile feed"; m_haveMainMF = true; emit minorFeedMainReceived(jsonDataList, previousLink, nextLink, totalItems); } else if (requestType == MinorFeedDirectRequest) { qDebug() << "It was the Mentions feed"; m_haveDirectMF = true; emit minorFeedDirectReceived(jsonDataList, previousLink, nextLink, totalItems); } else if (requestType == MinorFeedActivityRequest) { qDebug() << "It was the Actions feed"; m_haveActivityMF = true; emit minorFeedActivityReceived(jsonDataList, previousLink, nextLink, totalItems); } } else { qDebug() << "Error parsing received JSON data!"; qDebug() << "Raw data:" << replyData; // JSON directly } break; case DeletePostRequest: this->showStatusMessageAndLogIt(tr("Message deleted successfully.")); emit userDidSomething(); break; case CheckContactRequest: m_webfingerCheckTimer->stop(); // No need to check timeout at this point qDebug() << "Contact ID seems OK:" << replyUrl; emit contactVerified(m_addressPendingToFollow, httpCode, m_webfingerCheckTimedOut, replyServerVersion); m_addressPendingToFollow.clear(); break; case FollowContactRequest: // Just jump to next case UnfollowContactRequest: if (jsonParsedOK && jsonData.size() > 0) { ASPerson *contact = new ASPerson(jsonData.value("object").toMap(), this); if (requestType == FollowContactRequest) { prettyLogMessage = tr("Following %1 (%2) successfully.", "%1 is a person's name, %2 is the ID") .arg(contact->getName()) .arg(contact->getId()); emit contactFollowed(contact); emit followingListChanged(); } else { prettyLogMessage = tr("Stopped following %1 (%2) successfully.", "%1 is a person's name, %2 is the ID") .arg(contact->getName()) .arg(contact->getId()); emit contactUnfollowed(contact); emit followingListChanged(); } this->showStatusMessageAndLogIt(prettyLogMessage); emit userDidSomething(); } 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"); int totalCollectionCount = jsonData.value("totalItems").toInt(); if (contactsVariant.type() == QVariant::List) { qDebug() << "Parsed a List, listing contacts..."; int receivedItemsCount = contactsVariant.toList().size(); QString batchInfoString; if (requestType == FollowingListRequest) { m_userFollowingCount = totalCollectionCount; m_totalReceivedFollowing += receivedItemsCount; emit contactListReceived("following", contactsVariant.toList(), m_totalReceivedFollowing); batchInfoString = QString(" (%1/%2)") .arg(m_totalReceivedFollowing) .arg(m_userFollowingCount); if (m_totalReceivedFollowing >= totalCollectionCount) { m_haveFollowing = true; this->showStatusMessageAndLogIt(tr("List of 'following' " "completely received.") + batchInfoString); emit followingListChanged(); } else { emit currentJobChanged(tr("Partial list of 'following' " "received.") + batchInfoString); qDebug() << "Partial following received:" << batchInfoString; } } else // == FollowersListRequest { m_userFollowersCount = totalCollectionCount; m_totalReceivedFollowers += receivedItemsCount; emit contactListReceived("followers", contactsVariant.toList(), m_totalReceivedFollowers); batchInfoString = QString(" (%1/%2)") .arg(m_totalReceivedFollowers) .arg(m_userFollowersCount); if (m_totalReceivedFollowers >= totalCollectionCount) { m_haveFollowers = true; this->showStatusMessageAndLogIt(tr("List of 'followers' " "completely received.") + batchInfoString); } else { emit currentJobChanged(tr("Partial list of 'followers' " "received.") + batchInfoString); qDebug() << "Partial following received:" << batchInfoString; } } } 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..."; m_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; case SiteUserListRequest: this->showStatusMessageAndLogIt(tr("List of %1 users received.", "%1 is a server name") .arg(m_serverDomain) + " (" + QLocale::system() .toString(jsonData.value("totalItems") .toInt()) + ")"); if (jsonParsedOK && jsonData.size() > 0) { qDebug() << "JSON parsed OK"; emit siteUserListReceived(jsonData.value("items").toList(), jsonData.value("totalItems").toInt()); //qDebug() << "\n\n*** Site User List:\n\n" << replyData; } break; ///////////////////////////////////////////////////////////////////// Lists // Person list created OK case CreatePersonListRequest: this->showStatusMessageAndLogIt(tr("Person list '%1' created successfully.") .arg(jsonData.value("object").toMap() .value("displayName").toString())); this->getListsList(); // And reload the person lists (FIXME!) emit userDidSomething(); break; // Person list deleted OK case DeletePersonListRequest: this->showStatusMessageAndLogIt(tr("Person list deleted successfully.")); this->getListsList(); // And reload the person lists (FIXME!) emit userDidSomething(); 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..." << jsonData.value("displayName").toString(); emit currentJobChanged(tr("Person list received.")); // Using list URL as ID, since the ID isn't provided here... emit personListReceived(personListVariant.toList(), jsonData.value("url").toString()); } 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 = ASPerson::cleanupId(jsonData.value("object").toMap() .value("id").toString()); QString personName = jsonData.value("object").toMap() .value("displayName").toString(); QString personAvatar = jsonData.value("object").toMap() .value("image").toMap() .value("url").toString(); emit personAddedToList(personId, personName, personAvatar); this->showStatusMessageAndLogIt(tr("%1 (%2) added to list successfully.", "1=contact name, 2=contact ID") .arg(personName) .arg(personId)); emit userDidSomething(); } break; // Person removed from list OK case RemoveMemberFromListRequest: if (jsonParsedOK && jsonData.size() > 0) { QString personId = ASPerson::cleanupId(jsonData.value("object").toMap() .value("id").toString()); QString personName = jsonData.value("object").toMap() .value("displayName").toString(); emit personRemovedFromList(personId); this->showStatusMessageAndLogIt(tr("%1 (%2) removed from list successfully.", "1=contact name, 2=contact ID") .arg(personName) .arg(personId)); emit userDidSomething(); } break; //////////////////////////////////////////////////////////////////// Groups case CreateGroupRequest: this->showStatusMessageAndLogIt(tr("Group %1 created successfully.") .arg(jsonData.value("object").toMap() .value("displayName").toString())); emit userDidSomething(); qDebug() << "Group created; ID:" << jsonData.value("object").toMap() .value("id").toString(); break; case JoinGroupRequest: this->showStatusMessageAndLogIt(tr("Group %1 joined successfully.") .arg(jsonData.value("object").toMap() .value("displayName").toString())); emit userDidSomething(); qDebug() << "Group joined; =========================="; qDebug() << "Keys: " << jsonData.keys(); qDebug() << jsonData.value("object").toString(); break; case LeaveGroupRequest: this->showStatusMessageAndLogIt(tr("Left the %1 group successfully.") .arg(jsonData.value("object").toMap() .value("displayName").toString())); emit userDidSomething(); break; case AvatarRequest: qDebug() << "Received AVATAR data, from " << replyUrl; emit avatarPictureReceived(replyData, replyUrl); break; case ImageRequest: qDebug() << "Received IMAGE data, from " << replyUrl; if (replyUrl == requestExtraString || requestExtraString.isEmpty()) { qDebug() << "-- Directly"; emit imageReceived(replyData, replyUrl); } else { qDebug() << "-- Via redirect!\n" << "(Originally from: " << requestExtraString << ")"; emit imageReceived(replyData, requestExtraString); } break; case MediaRequest: qDebug() << "Received MEDIA data, from " << replyUrl; emit downloadCompleted(replyUrl); break; //////////////////////////////////////// If uploading a file was requested case UploadAvatarRequest: // just jump to next case UploadMediaForPostRequest: // 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; QString objectType = jsonData["objectType"].toString(); if (objectType == "image" || objectType == "audio" || objectType == "video" || objectType == "file") { if (requestType == UploadMediaForPostRequest) { prettyLogMessage = tr("File uploaded successfully. " "Posting message..."); emit currentJobChanged(prettyLogMessage); emit logMessage(prettyLogMessage, replyUrl); this->postMediaStepTwo(uploadedFileId); } else if (requestType == UploadAvatarRequest) { this->showStatusMessageAndLogIt(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; } /* * Handle SSL errors * * Default is blocking connections with SSL problems, unless the request * if for an image on a remote host and the corresponding option was set * in the Post settings, or --ignoresslerrors was used * */ void PumpController::sslErrorsHandler(QNetworkReply *reply, QList errorList) { qDebug() << "\n==== SSL errors!! ===="; qDebug() << "At:" << reply->url().toString(); qDebug() << "Error list:" << errorList << "\n\n"; QString errorsString; foreach (QSslError sslError, errorList) { errorsString.append(sslError.errorString() + "; "); } errorsString.remove(-2, 2); // remove "; " at the end this->showStatusMessageAndLogIt(tr("SSL errors in connection to %1!") .arg(reply->url().host()) + QString(" (%1)").arg(errorsString)); bool allowLoadingImage = false; if (m_ignoreSslInImages) { int requestType = reply->request().attribute(QNetworkRequest::User).toInt(); if (requestType == ImageRequest && reply->url().host() != m_serverDomain) { allowLoadingImage = true; this->showStatusMessageAndLogIt(tr("Loading external image from " "%1 regardless of SSL errors, " "as configured...", "%1 is a hostname") .arg(reply->url().host()), reply->url().toString()); } } // Ignore SSL errors and continue, if configured to do so if (m_ignoreSslErrors || allowLoadingImage) { qDebug() << "Ignoring these errors..."; reply->ignoreSslErrors(); } } void PumpController::getToken() { // If we do not have client_id or client_secret, do dynamic client registration if (m_clientId.isEmpty() || m_clientSecret.isEmpty()) { qDebug() << "PumpController::getToken()"; qDebug() << "We do not have client_id/client_secret yet; " "doing Dynamic Client Registration"; this->showStatusMessageAndLogIt(tr("The application is not registered with " "your server yet. Registering...")); // POST to https://yourserver.example/api/client/register QNetworkRequest postRequest(QUrl(m_serverScheme + m_serverDomain + "/api/client/register")); postRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); postRequest.setRawHeader("User-Agent", m_userAgentString); postRequest.setAttribute(QNetworkRequest::User, QVariant(ClientRegistrationRequest)); QByteArray data("{" " \"type\": \"client_associate\", " " \"application_type\": \"native\", " " \"application_name\": \"Dianara\", " " \"logo_uri\": \"http://dianara.nongnu.org/dianara-logo.png\", " " \"client_uri\": \"https://jancoding.wordpress.com/dianara\" " "}"); qDebug() << "About to POST:" << data; // upon receiving data (id+secret), will execute getToken() again m_nam.post(postRequest, data); } else { qDebug() << "Using saved client_id and client_secret:" << m_clientId << m_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 this->showStatusMessageAndLogIt(tr("Getting OAuth 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!"; // Notify the user about missing plugin emit authorizationFailed(tr("OAuth support error"), tr("Your installation of QOAuth, a library " "used by Dianara, doesn't seem to have " "HMAC-SHA1 support.") + "\n" + tr("You probably need to install the OpenSSL " "plugin for QCA: %1, %2 or similar.") .arg("qca2-plugin-openssl") .arg("libqca2-plugin-ossl")); } m_qoauth->setConsumerKey(m_clientId.toLocal8Bit()); m_qoauth->setConsumerSecret(m_clientSecret.toLocal8Bit()); QString requestTokenUrl = m_serverScheme + m_serverDomain + "/oauth/request_token"; qDebug() << "GET: " << requestTokenUrl << "with" << m_qoauth->consumerKey() << m_qoauth->consumerSecret(); QOAuth::ParamMap oAuthParams; oAuthParams.insert("oauth_callback", "oob"); QOAuth::ParamMap reply = m_qoauth->requestToken(requestTokenUrl, QOAuth::GET, QOAuth::HMAC_SHA1, oAuthParams); if (m_qoauth->error() == QOAuth::NoError) { qDebug() << "requestToken OK:" << reply.keys(); m_token = reply.value(QOAuth::tokenParameterName()); m_tokenSecret = reply.value(QOAuth::tokenSecretParameterName()); qDebug() << "Token:" << m_token; qDebug() << "Token Secret:" << m_tokenSecret.left(5) << "********** (hidden)"; QUrl oAuthAuthorizeUrl(m_serverScheme + m_serverDomain + "/oauth/authorize"); QUrlQuery query; query.addQueryItem("oauth_token", m_token); oAuthAuthorizeUrl.setQuery(query); bool oauthUrlOpenedOk = 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, oauthUrlOpenedOk); // Now, user should enter VERIFIER in AccountDialog to authorize the program } else { qDebug() << "QOAuth error" << m_qoauth->error() << "!"; qDebug() << reply.keys(); emit authorizationFailed(tr("Authorization error"), tr("There was an OAuth error while trying " "to get the authorization token.") + "\n\n" + tr("QOAuth error %1").arg(m_qoauth->error())); } } } 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 = m_serverScheme + m_serverDomain + "/oauth/access_token"; QOAuth::ParamMap reply = m_qoauth->accessToken(requestAuthorizationUrl, QOAuth::GET, m_token, m_tokenSecret, QOAuth::HMAC_SHA1, moreParams); if (m_qoauth->error() == QOAuth::NoError) // Woooohooo!! { qDebug() << "Got authorized token; Dianara is authorized to access the account"; m_token = reply.value(QOAuth::tokenParameterName()); m_tokenSecret = reply.value(QOAuth::tokenSecretParameterName()); m_applicationAuthorized = true; this->showStatusMessageAndLogIt(tr("Application authorized successfully.")); QSettings settings; settings.setValue("isApplicationAuthorized", m_applicationAuthorized); settings.setValue("token", m_token); settings.setValue("tokenSecret", m_tokenSecret); settings.sync(); qDebug() << "Token:" << m_token; qDebug() << "TokenSecret:" << m_tokenSecret.left(5) << "***********"; emit authorizationStatusChanged(m_applicationAuthorized); emit authorizationSucceeded(); } else { QString errorMessage = tr("OAuth error while authorizing application.") + QString(" (%1)").arg(m_qoauth->error()); this->showStatusMessageAndLogIt(errorMessage); emit authorizationFailed(tr("Authorization error"), errorMessage); qDebug() << "OAuth error while authorizing application" << m_qoauth->error(); } } /* * Called by a QTimer; * Get initial data (profile, contacts, timelines), one step at a time * */ void PumpController::getInitialData() { qDebug() << "PumpController::getInitialData() step" << m_initialDataStep; m_initialDataTimer->setInterval(3000); // Every 3 sec // If we're still waiting for a proxy password, don't do anything if (this->needsProxyPassword()) { emit currentJobChanged(tr("Waiting for proxy password...")); return; } /* * 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 to specific contacts, and to know the state * of following/not Following the author of a post in a timeline * */ switch (m_initialDataStep) { case 0: if (!m_haveProfile) { this->getUserProfile(m_userId); } break; case 1: // Ensure we have the profile already, before getting contact lists... if (!m_haveProfile) { m_initialDataStep = -1; // Restart initialization! this->showStatusMessageAndLogIt(tr("Still waiting for profile. " "Trying again...")); } break; case 2: if (!m_haveFollowing) { if (m_haveProfile) // Require profile first, since it provides followingCount { this->getContactList("following"); } else { --m_initialDataStep; // go back 1 to retry } } break; case 3: if (!m_haveFollowers) { if (m_haveProfile) // Required for followersCount { this->getContactList("followers"); } else { --m_initialDataStep; // retry } } break; case 4: if (!m_haveMainTL) { this->getFeed(PumpController::MainTimelineRequest, m_postsPerPageMain); } break; case 5: if (!m_haveMainMF) { this->getFeed(PumpController::MinorFeedMainRequest, 50); } break; case 6: if (!m_havePersonLists) { this->getListsList(); } break; case 7: if (!m_haveDirectTL) { this->getFeed(PumpController::DirectTimelineRequest, m_postsPerPageOther); } break; case 8: if (!m_haveDirectMF) { this->getFeed(PumpController::MinorFeedDirectRequest, 20); } break; case 9: if (!m_haveActivityTL) { this->getFeed(PumpController::ActivityTimelineRequest, m_postsPerPageOther); } break; case 10: if (!m_haveActivityMF) { this->getFeed(PumpController::MinorFeedActivityRequest, 20); } break; case 11: if (!m_haveFavoritesTL) { this->getFeed(PumpController::FavoritesTimelineRequest, m_postsPerPageOther); } break; case 12: // If some data is still missing, go back to the beginning of the cycle if (!m_haveProfile || !m_haveFollowing || !m_haveFollowers || !m_havePersonLists || !m_haveMainTL || !m_haveMainMF || !m_haveDirectTL || !m_haveDirectMF || !m_haveActivityTL || !m_haveActivityMF || !m_haveFavoritesTL) { // Unless we've already tried several times, like people with broken // contact lists, for instance, which will never be received if (m_initialDataAttempts < 5) { m_initialDataStep = -1; ++m_initialDataAttempts; QString attempts; if (m_initialDataAttempts > 1) { attempts = tr("%1 attempts").arg(m_initialDataAttempts); } else { attempts = tr("1 attempt"); } this->showStatusMessageAndLogIt(tr("Some initial data was not " "received. Restarting " "initialization...") + " (" + attempts + ")"); } else { this->showStatusMessageAndLogIt(tr("Some initial data was not " "received after several " "attempts. Something might " "be wrong with your server. " "You might still be able to " "use the service normally.")); } } else { this->showStatusMessageAndLogIt(tr("All initial data received. " "Initialization complete.")); // If there are no problems, this will just wait one interval, // so it takes longer for the final (default) step to arrive } break; default: m_initialDataTimer->stop(); if (m_initialDataAttempts < 5) // with 5+ attempts, leave previous message { this->showStatusMessageAndLogIt(tr("Ready.")); } emit initializationCompleted(); qDebug() << "--------------------------------------"; qDebug() << "-- All initial data loaded -----------"; qDebug() << "--------------------------------------"; } emit initializationStepChanged(m_initialDataStep); ++m_initialDataStep; } /* * Send a NOTE to the server * */ void PumpController::postNote(QVariantMap audienceMap, QString postText, QString postTitle) { qDebug() << "PumpController::postNote()"; QNetworkRequest postRequest = this->prepareRequest(m_apiFeedUrl, QOAuth::POST, PublishPostRequest); qDebug() << "Should be posting to:" << audienceMap; QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "note"); if (!postTitle.isEmpty()) { jsonVariantObject.insert("displayName", postTitle); } //jsonVariantObject.insert("summary", "summary test"); jsonVariantObject.insert("content", postText); QVariantMap jsonVariant; jsonVariant.insert("verb", "post"); jsonVariant.insert("object", jsonVariantObject); jsonVariant.insert("to", audienceMap.value("to").toList()); jsonVariant.insert("cc", audienceMap.value("cc").toList()); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; m_nam.post(postRequest, data); } /* * Post media-type of object (image, audio, video, file) * * First, upload the file. * Then we get its ID in a signal, and create the post itself * */ QNetworkReply *PumpController::postMedia(QVariantMap audienceMap, QString postText, QString postTitle, QString mediaFilename, QString mediaType, QString mimeContentType) { qDebug() << "PumpController::postMedia()" << mediaType; qDebug() << "Uploading" << mediaFilename << "with title:" << postTitle; // Store postTitle, postText, audienceMap, and postType, then upload m_currentPostTitle = postTitle; m_currentPostDescription = postText; m_currentPostAudience = audienceMap; m_currentPostType = mediaType; QNetworkReply *networkReply = this->uploadFile(mediaFilename, mimeContentType, UploadMediaForPostRequest); return networkReply; } /* * Post Media, step 2: after getting the ID in the file upload request, * create the post itself * */ void PumpController::postMediaStepTwo(QString id) { qDebug() << "PumpController::postMediaStepTwo()" << m_currentPostType << "ID:" << id; QNetworkRequest postRequest = this->prepareRequest(m_apiFeedUrl, QOAuth::POST, PublishPostRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", m_currentPostType); jsonVariantObject.insert("id", id); QVariantMap jsonVariant; jsonVariant.insert("verb", "post"); jsonVariant.insert("object", jsonVariantObject); jsonVariant.insert("to", m_currentPostAudience.value("to").toList()); jsonVariant.insert("cc", m_currentPostAudience.value("cc").toList()); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; m_nam.post(postRequest, data); } /* * Second step for avatar upload. * * Post the image to Public * */ void PumpController::postAvatarStepTwo(QString id) { qDebug() << "PumpController::postAvatarStepTwo() image ID:" << id; QNetworkRequest postRequest = this->prepareRequest(m_apiFeedUrl, 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 QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; m_nam.post(postRequest, data); } /* * Update post contents (the object) * */ void PumpController::updatePost(QString id, QString type, QString content, QString title) { qDebug() << "PumpController::updatePost(), post ID:" << id; QNetworkRequest postRequest = this->prepareRequest(m_apiFeedUrl, QOAuth::POST, UpdatePostRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("id", id); jsonVariantObject.insert("objectType", type); if (!title.isEmpty()) // FIXME: needs a way to remove titles... { jsonVariantObject.insert("displayName", title); } jsonVariantObject.insert("content", content); QVariantMap jsonVariant; jsonVariant.insert("verb", "update"); jsonVariant.insert("object", jsonVariantObject); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; m_nam.post(postRequest, data); } /* * Like (favorite) a post, by its ID (URL) * */ void PumpController::likePost(QString postId, QString postType, QString authorId, bool like) { qDebug() << "PumpController::likePost() liking post" << postId; QNetworkRequest likeRequest = this->prepareRequest(m_apiFeedUrl, 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); if (m_silentLikes) { QVariantMap authorVariant; authorVariant.insert("objectType", "person"); authorVariant.insert("id", "acct:" + authorId); QVariantList jsonAudienceTo; jsonAudienceTo.append(authorVariant); // To: only the author jsonVariant.insert("to", jsonAudienceTo); } QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "about to POST:" << data; m_nam.post(likeRequest, data); } void PumpController::addComment(QString comment, QString postId, QString postType) { qDebug() << "PumpController::addComment() sending comment to this post:" << postId; QNetworkRequest commentRequest = this->prepareRequest(m_apiFeedUrl, QOAuth::POST, CommentPostRequest); commentRequest.setAttribute(QNetworkRequest::Attribute(QNetworkRequest::User + 1), QVariant(postId)); 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); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "about to POST:" << data; m_nam.post(commentRequest, data); } /* * Update comment contents (object) * * FIXME: This should be merged with :updatePost() * */ void PumpController::updateComment(QString id, QString content, QString inReplyToId) { qDebug() << "PumpController::updateComment(), comment ID:" << id; QNetworkRequest postRequest = this->prepareRequest(m_apiFeedUrl, QOAuth::POST, UpdateCommentRequest); postRequest.setAttribute(QNetworkRequest::Attribute(QNetworkRequest::User + 1), QVariant(inReplyToId)); QVariantMap jsonVariantObject; jsonVariantObject.insert("id", id); jsonVariantObject.insert("objectType", "comment"); jsonVariantObject.insert("content", content); QVariantMap jsonVariant; jsonVariant.insert("verb", "update"); jsonVariant.insert("object", jsonVariantObject); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "About to POST:" << data; m_nam.post(postRequest, data); } void PumpController::sharePost(QString postId, QString postType) { qDebug() << "PumpController::sharePost() sharing post" << postId; QNetworkRequest shareRequest = this->prepareRequest(m_apiFeedUrl, QOAuth::POST, SharePostRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", postType); jsonVariantObject.insert("id", postId); QVariantMap jsonVariant; jsonVariant.insert("verb", "share"); jsonVariant.insert("object", jsonVariantObject); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "about to POST:" << data; m_nam.post(shareRequest, data); } void PumpController::unsharePost(QString postId, QString postType) { qDebug() << "PumpController::unsharePost() unsharing post" << postId; QNetworkRequest unshareRequest = this->prepareRequest(m_apiFeedUrl, QOAuth::POST, UnsharePostRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", postType); jsonVariantObject.insert("id", postId); QVariantMap jsonVariant; jsonVariant.insert("verb", "unshare"); jsonVariant.insert("object", jsonVariantObject); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "about to POST:" << data; m_nam.post(unshareRequest, data); } void PumpController::deletePost(QString postId, QString postType) { qDebug() << "PumpController::deletePost() deleting post" << postId; QNetworkRequest deleteRequest = this->prepareRequest(m_apiFeedUrl, QOAuth::POST, DeletePostRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", postType); jsonVariantObject.insert("id", postId); QVariantMap jsonVariant; jsonVariant.insert("verb", "delete"); jsonVariant.insert("object", jsonVariantObject); QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "about to POST:" << data; m_nam.post(deleteRequest, data); } /* * Check if a user ID exists in the corresponding server, * and if it is a Pump server. * * Used before actually following a contact. * */ void PumpController::followContact(QString address) { // Don't do anything if another check+follow is already in progress if (!m_addressPendingToFollow.isEmpty()) { // FIXME -- Show more clearly, a dialog box or something this->showStatusMessageAndLogIt(tr("Can't follow %1 at this time.", "%1 is a user ID") .arg("'" + address + "'") + " " + tr("Trying to follow %1.", "%1 is a user ID") .arg("'" + m_addressPendingToFollow) + "'"); emit cannotFollowNow(address); return; } const QString host = address.split('@').last(); QString url = "https://" // TMP/FIXME: should retry with HTTP if it fails + host + "/.well-known/webfinger?resource=" + address; QNetworkRequest checkRequest = this->prepareRequest(url, QOAuth::GET, CheckContactRequest); emit currentJobChanged(tr("Checking address %1 before " "following...").arg(address)); m_addressPendingToFollow = address; m_webfingerCheckTimer->start(60000); // Take care of possible timeouts (1 min) m_webfingerCheckTimedOut = false; qDebug() << "About to verify webfinger ID:" << url; m_webfingerCheckReply = m_nam.get(checkRequest); } /* * Add a contact to the /following list with their webfinger address * * Info for the newly added contact will come in the reply * */ void PumpController::followVerifiedContact(QString address) { qDebug() << "PumpController::followVerifiedContact()" << address; QNetworkRequest followRequest = this->prepareRequest(m_apiFeedUrl, QOAuth::POST, FollowContactRequest); QVariantMap jsonVariantObject; jsonVariantObject.insert("objectType", "person"); jsonVariantObject.insert("id", "acct:" + address); QVariantMap jsonVariant; jsonVariant.insert("verb", "follow"); jsonVariant.insert("object", jsonVariantObject); if (m_silentFollows) // In 'private' mode, address to the same as the object { QVariantList jsonAudienceTo; jsonAudienceTo.append(jsonVariantObject); // To: only the specific user jsonVariant.insert("to", jsonAudienceTo); } QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "about to POST:" << data; m_nam.post(followRequest, data); } /* * Remove a contact from the /following list with their webfinger address * */ void PumpController::unfollowContact(QString address) { qDebug() << "PumpController::unfollowContact()" << address; QNetworkRequest unfollowRequest = this->prepareRequest(m_apiFeedUrl, 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); if (m_silentFollows) { QVariantList jsonAudienceTo; jsonAudienceTo.append(jsonVariantObject); jsonVariant.insert("to", jsonAudienceTo); } QByteArray data = this->prepareJSON(jsonVariant); qDebug() << "about to POST:" << data; m_nam.post(unfollowRequest, data); } /* * If the QTimer times out, abort the connection manually, * since Qt doesn't handle this * */ void PumpController::onValidationTimeout() { qDebug() << "Aborting user ID check due to timeout!"; m_webfingerCheckReply->abort(); m_webfingerCheckReply->deleteLater(); m_webfingerCheckTimedOut = true; } dianara-v1.4.1/src/filterchecker.h0000664000175000017500000000303113210105406015145 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 FILTERCHECKER_H #define FILTERCHECKER_H #include #include #include #include "asactivity.h" class FilterChecker : public QObject { Q_OBJECT public: explicit FilterChecker(QObject *parent = 0); ~FilterChecker(); void setFilters(QVariantList newFiltersList); int validateActivity(ASActivity *activity); enum FilterTypes { FilterOut, Highlight, NoFiltering = 999 }; signals: public slots: private: QList m_filteredContent; QList m_filteredAuthor; QList m_filteredGenerator; QList m_filteredDescription; }; #endif // FILTERCHECKER_H dianara-v1.4.1/src/hclabel.cpp0000664000175000017500000001105513202666600014276 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "hclabel.h" HClabel::HClabel(QString initialText, QWidget *parent) : QLabel(parent) { this->setWordWrap(true); this->setAutoFillBackground(true); this->setTextFormat(Qt::RichText); this->setBaseText(initialText); this->setHighlighted(false); // Default this->setExpanded(false); m_toggleTimer = new QTimer(this); m_toggleTimer->setSingleShot(true); connect(m_toggleTimer, &QTimer::timeout, this, &HClabel::toggleContent); qDebug() << "HClabel created"; } HClabel::~HClabel() { qDebug() << "HClabel destroyed"; } void HClabel::setHighlighted(bool highlighted) { if (highlighted) { // Constant highlighting this->setStyleSheet("QLabel " "{ color: palette(highlighted-text); " " background-color: qlineargradient(spread:pad, " " x1:0, y1:0, x2:0, y2:1, " " stop:0 rgba(0, 0, 0, 0), " " stop:0.3 palette(highlight), " " stop:0.7 palette(highlight), " " stop:1 rgba(0, 0, 0, 0)); " " border-radius: 4px " "}" "QLabel:hover " "{ color: palette(highlighted-text); " " background-color: palette(highlight);" " border-radius: 4px " "}"); } else { // Highlight only on mouse hover this->setStyleSheet("QLabel " "{ background-color: transparent }" "QLabel:hover " "{ color: palette(highlighted-text); " " background-color: palette(highlight);" " border-radius: 4px " "}"); } } void HClabel::setExpanded(bool isExpanded) { m_expanded = isExpanded; } void HClabel::setBaseText(QString text) { m_baseText = text; if (m_extendedText.isEmpty() || !m_expanded) { this->setText(m_baseText); } else { this->setText(m_baseText + "
              " + m_extendedText + "
              "); } } void HClabel::setExtendedText(QString text) { m_baseText = this->text(); // Save it m_extendedText = text.trimmed(); } ////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// SLOTS ////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void HClabel::toggleContent() { if (m_expanded) { this->setText(m_baseText); } else { if (!m_extendedText.isEmpty()) { this->setText(m_baseText + "
              " + m_extendedText + "
              "); } } this->setExpanded(!m_expanded); } ////////////////////////////////////////////////////////////////////////////// /////////////////////////////// PROTECTED //////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void HClabel::mousePressEvent(QMouseEvent *event) { m_toggleTimer->start(200); // Don't emit the signal for now... we'll see if we need it //emit clicked(); event->ignore(); // Let the click pass through to the parent } dianara-v1.4.1/src/avatarbutton.cpp0000664000175000017500000002523413206623175015426 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "avatarbutton.h" AvatarButton::AvatarButton(ASPerson *person, PumpController *pumpController, GlobalObject *globalObject, QSize avatarSize, QWidget *parent) : QToolButton(parent) { m_pumpController = pumpController; m_globalObject = globalObject; this->setPopupMode(QToolButton::InstantPopup); this->setStyleSheet("QToolButton { border: none; " " border-radius: 8px; " " padding: 2px }" "QToolButton:hover { border: none; " " background-color: " " palette(highlight) }"); this->setIconSize(avatarSize); this->setMinimumSize(avatarSize); m_iconWidth = avatarSize.width(); this->setToolTip(person->getTooltipInfo()); // Get local file name for avatar, which is stored in base64 hash form QString avatarFile = MiscHelpers::getCachedAvatarFilename(person->getAvatarUrl()); if (QFile::exists(avatarFile)) { this->updateAvatarIcon(avatarFile); } else { this->setGenericAvatarIcon(); m_pumpController->enqueueAvatarForDownload(person->getAvatarUrl()); connect(m_pumpController, &PumpController::avatarStored, this, &AvatarButton::redrawAvatar); } m_authorId = person->getId(); m_authorName = person->getNameWithFallback(); m_authorUrl = person->getUrl(); m_authorAvatarUrl = person->getAvatarUrl(); m_authorOutbox = person->getOutboxLink(); m_authorFollowed = false; // Real status will be read when creating the menu createAvatarMenu(); if (!m_authorId.isEmpty()) // Don't add the menu for invalid users { this->setMenu(m_avatarMenu); } qDebug() << "AvatarButton created"; } AvatarButton::~AvatarButton() { qDebug() << "AvatarButton destroyed"; } void AvatarButton::setGenericAvatarIcon() { this->setIcon(QIcon::fromTheme("user-identity", QIcon(":/images/no-avatar.png")) .pixmap(this->iconSize()) .scaledToWidth(m_iconWidth, Qt::SmoothTransformation)); } void AvatarButton::updateAvatarIcon(QString filename) { const QPixmap avatarPixmap = QPixmap(filename) .scaledToWidth(m_iconWidth, Qt::SmoothTransformation); if (!avatarPixmap.isNull()) { this->setIcon(QIcon(avatarPixmap)); } else { qDebug() << "AvatarButton() avatar pixmap is null, using generic"; this->setGenericAvatarIcon(); } } /* * Create the menu shown when clicking the avatar * */ void AvatarButton::createAvatarMenu() { const bool userIsAuthor = (m_authorId == m_pumpController->currentUserId()); m_avatarMenu = new QMenu(this); m_avatarMenu->setSeparatorsCollapsible(false); m_avatarMenuIdAction = new QAction(QIcon::fromTheme("user-identity", QIcon(":/images/no-avatar.png")), m_authorId, this); m_avatarMenuIdAction->setSeparator(true); // Make it nicer and not clickable m_avatarMenu->addAction(m_avatarMenuIdAction); QString openProfileString; if (userIsAuthor) { openProfileString = tr("Open your profile in web browser"); } else { openProfileString = tr("Open %1's profile in web browser") .arg(m_authorName); } m_avatarMenuProfileAction = new QAction(QIcon::fromTheme("internet-web-browser", QIcon(":/images/no-avatar.png")), openProfileString, this); connect(m_avatarMenuProfileAction, &QAction::triggered, this, &AvatarButton::openAuthorProfileInBrowser); if (m_authorUrl.isEmpty()) // Disable if there isn't actually an URL { m_avatarMenuProfileAction->setDisabled(true); } m_avatarMenu->addAction(m_avatarMenuProfileAction); m_avatarMenuFollowAction = new QAction(this); // Connections and label are set in setFollowUnfollow() m_avatarMenuMessageAction = new QAction(QIcon::fromTheme("document-edit", QIcon(":/images/button-edit.png")), tr("Send message to %1") .arg(m_authorName), this); connect(m_avatarMenuMessageAction, &QAction::triggered, this, &AvatarButton::sendMessageToUser); m_avatarMenuBrowseAction = new QAction(QIcon::fromTheme("edit-find", QIcon(":/images/menu-find.png")), tr("Browse messages"), this); connect(m_avatarMenuBrowseAction, &QAction::triggered, this, &AvatarButton::browseUserMessages); // Disable 'browse' option if not available (empty or in another server; Pump issue) if (!m_pumpController->urlIsInOurHost(m_authorOutbox)) { m_avatarMenuBrowseAction->setDisabled(true); } // Only add "follow/unfollow", "send message" and "browse messages" if (!userIsAuthor) // options if we're not the author { m_avatarMenu->addAction(m_avatarMenuFollowAction); this->syncFollowState(true); m_avatarMenu->addAction(m_avatarMenuMessageAction); m_avatarMenu->addAction(m_avatarMenuBrowseAction); } // // More options can be added from outside, via addActionToMenu() // } /* * See if we're currently following this user, according to the contact list * */ void AvatarButton::syncFollowState(bool firstTime) { bool authorFollowedBefore = m_authorFollowed; m_authorFollowed = m_pumpController->userInFollowing(m_authorId); if (m_authorFollowed != authorFollowedBefore || firstTime) { this->setFollowUnfollow(); } } /* * 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 AvatarButton::setFollowUnfollow() { if (m_authorFollowed) { m_avatarMenuFollowAction->setIcon(QIcon::fromTheme("list-remove-user", QIcon(":/images/list-remove.png"))); m_avatarMenuFollowAction->setText(tr("Stop following")); connect(m_avatarMenuFollowAction, &QAction::triggered, this, &AvatarButton::unfollowUser); disconnect(m_avatarMenuFollowAction, &QAction::triggered, this, &AvatarButton::followUser); //qDebug() << "post author followed, connecting to UNFOLLOW()" << this->authorId; } else { m_avatarMenuFollowAction->setIcon(QIcon::fromTheme("list-add-user", QIcon(":/images/list-add.png"))); m_avatarMenuFollowAction->setText(tr("Follow")); connect(m_avatarMenuFollowAction, &QAction::triggered, this, &AvatarButton::followUser); disconnect(m_avatarMenuFollowAction, &QAction::triggered, this, &AvatarButton::unfollowUser); //qDebug() << "post author not followed, connecting to FOLLOW()" << this->authorId; } } void AvatarButton::addSeparatorToMenu() { m_avatarMenu->addSeparator(); } void AvatarButton::addActionToMenu(QAction *action) { m_avatarMenu->addAction(action); } //////////////////////////////////////////////////////////////////////////// ////////////////////////////////// SLOTS /////////////////////////////////// //////////////////////////////////////////////////////////////////////////// void AvatarButton::openAuthorProfileInBrowser() { MiscHelpers::openUrl(m_authorUrl, this); } void AvatarButton::followUser() { m_pumpController->followContact(m_authorId); // Actual menu will be updated when appropriate SIGNAL is received } void AvatarButton::unfollowUser() { const QString nameWithId = ASPerson::makeNameIdString(m_authorName, m_authorId); int confirmation = QMessageBox::question(this, tr("Stop following?"), tr("Are you sure you want to " "stop following %1?") .arg(nameWithId), tr("&Yes, stop following"), tr("&No"), QString(), 1, 1); if (confirmation == 0) { m_pumpController->unfollowContact(m_authorId); // Menu option will be updated when appropriate SIGNAL is received } } void AvatarButton::sendMessageToUser() { m_globalObject->createMessageForContact(m_authorId, m_authorName, m_authorUrl); } void AvatarButton::browseUserMessages() { m_globalObject->browseUserMessages(m_authorId, m_authorName, this->icon(), m_authorOutbox + "/major"); // TMP! not future-proof } /* * Redraw avatar after receiving it * */ void AvatarButton::redrawAvatar(QString avatarUrl, QString avatarFilename) { if (avatarUrl == m_authorAvatarUrl) { this->updateAvatarIcon(avatarFilename); disconnect(m_pumpController, &PumpController::avatarStored, this, &AvatarButton::redrawAvatar); } } dianara-v1.4.1/src/asperson.h0000664000175000017500000000370413206623177014213 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 ASPERSON_H #define ASPERSON_H #include #include #include class ASPerson : public QObject { Q_OBJECT public: explicit ASPerson(QVariantMap personMap, QObject *parent = 0); ~ASPerson(); void updateDataFromPerson(ASPerson *person); QString getId(); static QString cleanupId(QString id); QString getName(); QString getNameWithFallback(); static QString makeNameIdString(QString userName, QString userId); QString getHometown(); QString getBio(); QString getAvatarUrl(); QString getUrl(); QString getTooltipInfo(); QString getOutboxLink(); bool isFollowed(); //int getFollowingCount(); //int getFollowersCount(); QString getCreatedAt(); QString getupdatedAt(); signals: public slots: private: QString m_id; QString m_name; QString m_hometown; QString m_bio; QString m_avatar; QString m_url; QString m_outboxLink; bool m_followed; //int m_followingCount; //int m_followersCount; QString m_createdAt; QString m_updatedAt; }; #endif // ASPERSON_H dianara-v1.4.1/src/listsmanager.h0000664000175000017500000000535213204341753015046 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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); QTreeWidgetItem *createPersonItem(QString id, QString name, QString avatarFile); signals: public slots: void setPersonsInList(QVariantList personList, QString listUrl); void createList(); void deleteList(); void enableDisableCreateButton(QString listName); void enableDisableDeleteButtons(); void showAddPersonDialog(); void addPerson(QIcon icon, QString contactString, QString contactName, QString contactId, QString contactUrl); void removePerson(); void addPersonItemToList(QString personId, QString personName, QString avatarUrl); void removePersonItemFromList(QString personId); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_buttonsLayout; QTreeWidget *m_listsTreeWidget; QPushButton *m_deleteListButton; QPushButton *m_addPersonButton; QPushButton *m_removePersonButton; PeopleWidget *m_peopleWidget; QGroupBox *m_newListGroupbox; QHBoxLayout *m_groupboxMainLayout; QVBoxLayout *m_groupboxLeftLayout; QLineEdit *m_newListNameLineEdit; QTextEdit *m_newListDescTextEdit; QPushButton *m_createListButton; QStringList m_personListsUrlList; PumpController *m_pumpController; }; #endif // LISTSMANAGER_H dianara-v1.4.1/src/main.cpp0000644000175000017500000002337213221336426013634 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 needed in some cases #include #include "mainwindow.h" void customMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { Q_UNUSED(type) Q_UNUSED(context) Q_UNUSED(msg) // Do nothing return; } int main(int argc, char *argv[]) { QApplication dianaraApp(argc, argv); dianaraApp.setApplicationName("Dianara"); dianaraApp.setApplicationVersion("1.4.1"); dianaraApp.setOrganizationName("JanCoding"); dianaraApp.setOrganizationDomain("jancoding.wordpress.com"); std::cout << QString("Dianara v%1 - JanKusanagi 2012-2017\n" "https://jancoding.wordpress.com/dianara\n\n") .arg(dianaraApp.applicationVersion()) .toStdString(); std::cout << QString("- Built with Qt v%1").arg(QT_VERSION_STR) .toStdString(); /* * REPRODUCIBLEBUILD is defined via .pro file when SOURCE_DATE_EPOCH is * defined in the build environment. This is used to avoid hardcoding * timestamps and this way make the builds reproducible. * */ #ifndef REPRODUCIBLEBUILD std::cout << QString(" on %1, %2") .arg(__DATE__) .arg(__TIME__).toStdString(); #endif std::cout << "\n"; std::cout << QString("- Running with Qt v%1\n\n").arg(qVersion()) .toStdString(); std::cout.flush(); // To make the mswin version of Dianara distributable as a standalone package #ifdef Q_OS_WIN dianaraApp.addLibraryPath("./plugins/"); #endif QStringList cmdLine = qApp->arguments(); cmdLine.removeFirst(); // Remove the program executable name/path bool debugMode = false; bool nextParameterIsConfig = false; bool ignoreSslErrors = false; bool nohttps = false; // Parse command line parameters if (!cmdLine.isEmpty()) { foreach (QString argument, cmdLine) { // Help if (argument.startsWith("--help", Qt::CaseInsensitive) || argument.startsWith("-h", Qt::CaseInsensitive)) { std::cout << "\nHelp:\n"; std::cout << " " << argv[0] << " [options]\n\n"; std::cout << "Options:\n"; std::cout << " -c --config [name] Use a different " "configuration file and data folder\n" " (parameter is a " "simple name, not a file path)\n"; std::cout << " -d --debug Show debug messages " "in the terminal\n"; std::cout << " --linkcolor=[color] Set color for " "links, such as 'green' or '#00B500'\n" "\n"; std::cout << " --nohttps Use this if your " "server does not support HTTPS\n"; std::cout << " --ignoresslerrors Ignore SSL errors " "(dangerous, use with care!)\n" "\n"; std::cout << " -v --version Show version " "information and exit\n"; std::cout << " -h --help Show this help and " "exit\n"; std::cout << "\n"; return 0; // Exit to shell } // Version info if (argument.startsWith("--version", Qt::CaseInsensitive) || argument.startsWith("-v", Qt::CaseInsensitive)) { // Version information already shown std::cout << "\n" "You can get the latest stable version from " "http://dianara.nongnu.org and\n" "the latest development version from " "https://gitlab.com/dianara/dianara-dev" "\n\n"; return 0; } // Use different config file, by setting a different applicationName if (argument.startsWith("--config", Qt::CaseInsensitive) || argument.startsWith("-c", Qt::CaseInsensitive)) { nextParameterIsConfig = true; cmdLine.removeAll(argument); } else if (nextParameterIsConfig) { const QString configName = "Dianara_" + argument.toLower(); dianaraApp.setApplicationName(configName); nextParameterIsConfig = false; std::cout << "Using alternate config file: " << configName.toStdString() << "\n"; } // Debug mode if (argument.startsWith("--debug", Qt::CaseInsensitive) || argument.startsWith("-d", Qt::CaseInsensitive)) { debugMode = true; cmdLine.removeAll(argument); std::cout << "Debug messages enabled\n"; } // Option to set link color (useful in GTK environments) if (argument.startsWith("--linkcolor=", Qt::CaseInsensitive)) { const QString linkColorName = argument.split("=").last(); const QColor linkColor = QColor(linkColorName); if (linkColor.isValid()) { QPalette newPalette = dianaraApp.palette(); newPalette.setColor(QPalette::Link, linkColor); dianaraApp.setPalette(newPalette); std::cout << "Forcing link color to: " << linkColorName.toStdString() << "\n"; } else { std::cout << "Invalid link color specified!\n"; } } // Option to use non-https servers if (argument.startsWith("--nohttps", Qt::CaseInsensitive)) { nohttps = true; cmdLine.removeAll(argument); std::cout << "*** Using No-HTTPS mode\n"; } // Option to ignore SSL errors if (argument.startsWith("--ignoresslerrors", Qt::CaseInsensitive)) { ignoreSslErrors = true; cmdLine.removeAll(argument); std::cout << "\n" "*************************************\n" "*** WARNING: Ignoring SSL errors. ***\n" "*** This is insecure! ***\n" "*************************************\n" "\n"; } } // End foreach } // Register custom message handler, to hide debug messages unless specified if (!debugMode) { #ifdef Q_OS_WIN FreeConsole(); #endif std::cout << "To see debug messages while running, use --debug\n"; qInstallMessageHandler(customMessageHandler); } // Load translation files // Get language from LANG environment variable or system's locale QString languageString = qgetenv("LANG"); if (languageString.isEmpty()) { languageString = QLocale::system().name(); } QString languageFile; bool languageLoaded; QTranslator translatorQt; languageFile = QString("qt_%1").arg(languageString); std::cout << "\n" << "Using Qt translation " << QLibraryInfo::location(QLibraryInfo::TranslationsPath).toStdString() << "/" << languageFile.toStdString() << "... "; languageLoaded = translatorQt.load(languageFile, QLibraryInfo::location(QLibraryInfo::TranslationsPath)); if (languageLoaded) { std::cout << "OK"; dianaraApp.installTranslator(&translatorQt); } else { std::cout << "Unavailable"; } QTranslator translatorDianara; languageFile = QString(":/translations/dianara_%1").arg(languageString); std::cout << "\n" << "Using program translation " << languageFile.toStdString() << "... "; languageLoaded = translatorDianara.load(languageFile); if (languageLoaded) { std::cout << "OK"; dianaraApp.installTranslator(&translatorDianara); } else { std::cout << "Unavailable"; } std::cout << "\n\n"; std::cout.flush(); MainWindow dianaraWindow; if (ignoreSslErrors) { dianaraWindow.enableIgnoringSslErrors(); } if (nohttps) { dianaraWindow.enableNoHttpsMode(); } dianaraWindow.toggleMainWindow(true); // show(), firstTime=true return dianaraApp.exec(); } dianara-v1.4.1/src/emailchanger.h0000664000175000017500000000374313207030347014773 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 EMAILCHANGER_H #define EMAILCHANGER_H #include #include #include #include #include #include #include #include #include #include "pumpcontroller.h" class EmailChanger : public QWidget { Q_OBJECT public: explicit EmailChanger(QString explanation, PumpController *pumpController, QWidget *parent = 0); ~EmailChanger(); void setCurrentEmail(QString email); signals: public slots: void validateFields(); void changeEmail(); void cancelDialog(); protected: virtual void closeEvent(QCloseEvent *event); private: QVBoxLayout *m_mainLayout; QFormLayout *m_middleLayout; QHBoxLayout *m_bottomLayout; QLabel *m_infoLabel; QLineEdit *m_mailLineEdit; QLineEdit *m_mailRepeatLineEdit; QLineEdit *m_passwordLineEdit; QLabel *m_errorsLabel; QPushButton *m_changeButton; QPushButton *m_cancelButton; QAction *m_cancelAction; QString m_currentEmail; PumpController *m_pumpController; }; #endif // EMAILCHANGER_H dianara-v1.4.1/src/post.cpp0000644000175000017500000022475513211037300013671 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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" #include "mainwindow.h" Post::Post(ASActivity *activity, bool highlightedByFilter, bool isStandalone, PumpController *pumpController, GlobalObject *globalObject, QWidget *parent) : QFrame(parent) { this->pController = pumpController; this->globalObj = globalObject; this->standalone = isStandalone; this->seeFullImageString = tr("Click the image to see it in full size"); this->downloadAttachmentString = tr("Click to download the attachment"); this->postImageIsAnimated = false; // Initialize this->postImageFailed = false; this->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); this->setMinimumSize(30, 30); // Ensure something's visible at all times activity->setParent(this); // reparent the passed activity leftColumnFrame = new QFrame(this); ///////////////////////////////////////////////// Highlighting this->highlightType = NoHighlight; QStringList highlightColors = globalObj->getColorsList(); QString highlightPostColor; // Post is adressed to us if (activity->getRecipientsIdList().contains(pController->currentUserId())) { highlightType = MessageForUserHighlight; highlightPostColor = highlightColors.at(0); // Color #1 in config } else if (activity->author()->getId() == pController->currentUserId() // Post or activity is ours || activity->object()->author()->getId() == pController->currentUserId()) { highlightType = OwnMessageHighlight; highlightPostColor = highlightColors.at(3); // Color #4 in the config } else if (highlightedByFilter) // Highlight by filtering rules { highlightType = FilterRulesHighlight; highlightPostColor = highlightColors.at(4); // Color #5 } if (highlightType != NoHighlight && !standalone // Don't use highlighting when opening as standalone post && !pController->currentUserId().isEmpty()) { if (QColor::isValidColor(highlightPostColor)) { // CSS for horizontal gradient from configured color to transparent QString css = QString("QFrame#LeftFrame " "{ background-color: " " qlineargradient(spread:pad, " " x1:0, y1:0, x2:1, y2:0, " " stop:0 %1, stop:1 rgba(0, 0, 0, 0)); " "}") .arg(highlightPostColor); leftColumnFrame->setObjectName("LeftFrame"); leftColumnFrame->setStyleSheet(css); } else { this->leftColumnFrame->setFrameStyle(QFrame::Panel); } } this->unreadPostColor = highlightColors.at(5); // Color #6 leftColumnLayout = new QVBoxLayout(); this->activityId = activity->getId(); // FIXME: will be empty in Favorites TL this->postId = activity->object()->getId(); this->postType = activity->object()->getType(); this->postUrl = activity->object()->getUrl(); this->postLikesCount = 0; // Store relevant likes/comments/shares URLs this->postLikesUrl = activity->object()->getLikesUrl(); this->postCommentsUrl = activity->object()->getCommentsUrl(); this->postSharesUrl = activity->object()->getSharesUrl(); // Add comments URL to "ever seen" list, only needed if post is on another server if (activity->object()->hasProxiedUrls()) { this->pController->addCommentUrlToSeenList(this->postId, this->postCommentsUrl); } ASPerson *authorPerson; // Having ID means the object has proper author data if (!activity->object()->author()->getId().isEmpty()) { authorPerson = activity->object()->author(); } else // No ID means post author data is the activity's author data { // This is a workaround, because pump.io doesn't give object author // if the activity is by the same user authorPerson = activity->author(); } this->postAuthorId = authorPerson->getId(); this->postAuthorName = authorPerson->getNameWithFallback(); this->postGeneratorString = activity->getGenerator(); // If it's a standalone post, set window title and restore size if (standalone) { QString windowTitle = tr("Post", "Noun, not verb") + ": " + activity->object()->getTranslatedType(postType) + " - Dianara"; this->setWindowIcon(QIcon::fromTheme("mail-message", QIcon(":/images/button-edit.png"))); this->setWindowTitle(windowTitle); this->setWindowFlags(Qt::Window); this->setWindowModality(Qt::NonModal); // Restore size this->setMinimumSize(420, 360); QSettings settings; this->resize(settings.value("Post/postSize", QSize(600, 440)).toSize()); connect(globalObj, SIGNAL(programShuttingDown()), this, SLOT(close())); } postIsUnread = false; postIsDeleted = false; QFont detailsFont; detailsFont.setPointSize(detailsFont.pointSize() - 1); // FIXME: check size first // Is the post a reshare? Indicate who shared it with a wide label at the top postIsSharedLabel = new QLabel(this); // With parent to avoid leaks // Needs to be initialized even if // unused, for setFuzzyTimestamps() postIsSharedLabel->hide(); if (activity->isShared()) { postIsSharedLabel->show(); this->postSharedById = activity->getSharedById(); postShareTime = activity->getUpdatedAt(); QString sharedByName = activity->getSharedByName(); QString sharedByAvatar = MiscHelpers::getCachedAvatarFilename(activity->getSharedByAvatar()); postShareInfoString = QString::fromUtf8("\342\231\272     "); // Recycling symbol QFont shareInfoFont; // If 'extended share info' is enabled, show avatar and use bigger font if (globalObj->getPostExtendedShares()) { // Share info font is standard size postShareInfoString.append(" " "    "); // FIXME/TODO: show client used? } else { // Small font, like in other details shareInfoFont = detailsFont; } postShareInfoString.append(tr("Via %1").arg("author()->getUrl() + "\">" + sharedByName + " - ")); // Fuzzy time is added here, in setFuzzyTimestamps(), along with To/Cc info this->postSharedToCCString.clear(); if (!activity->getToString().isEmpty()) { postSharedToCCString.append(" - " + tr("To") + ": " + activity->getToString()); } if (!activity->getCCString().isEmpty()) { postSharedToCCString.append(" - " + tr("Cc") + ": " + activity->getCCString()); } postIsSharedLabel->setText(postShareInfoString); postIsSharedLabel->setWordWrap(true); postIsSharedLabel->setOpenExternalLinks(true); postIsSharedLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); postIsSharedLabel->setFont(shareInfoFont); postIsSharedLabel->setForegroundRole(QPalette::Text); postIsSharedLabel->setBackgroundRole(QPalette::Base); postIsSharedLabel->setAutoFillBackground(true); postIsSharedLabel->setFrameStyle(QFrame::Panel | QFrame::Raised); connect(postIsSharedLabel, SIGNAL(linkHovered(QString)), this, SLOT(showHighlightedUrl(QString))); QString shareTooltip = "" "" ""; shareTooltip.append("" "" "
              " "" "" + sharedByName + "
              "); shareTooltip.append("" + this->postSharedById + "" "
              "); QString exactShareTime = Timestamp::localTimeDate(postShareTime); shareTooltip.append("
              " + tr("Shared on %1").arg(exactShareTime)); if (!this->postGeneratorString.isEmpty()) { shareTooltip.append("
              " + tr("Using %1", "1=Program used for posting or sharing") .arg(this->postGeneratorString)); this->postGeneratorString.clear(); // So it's not used in the post timestamp tooltip! } postIsSharedLabel->setToolTip(shareTooltip); } /////////////////////////////////////////////////// Left column, post Meta info // Different frame if post is ours if (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 QSize avatarSize = globalObj->getPostAvatarSize(); if (!activity->object()->getDeletedTime().isEmpty()) { // If the post was initially deleted, use a small avatar avatarSize = QSize(32,32); } postAuthorAvatarButton = new AvatarButton(authorPerson, this->pController, this->globalObj, avatarSize, this); ///////////// Add extra options to the avatar menu postAuthorAvatarButton->addSeparatorToMenu(); // Open post in browser openPostInBrowserAction = new QAction(QIcon::fromTheme("internet-web-browser", QIcon(":/images/button-download.png")), tr("Open post in web browser"), this); connect(openPostInBrowserAction, SIGNAL(triggered()), this, SLOT(openPostInBrowser())); postAuthorAvatarButton->addActionToMenu(openPostInBrowserAction); // Copy URL copyPostUrlAction = new QAction(QIcon::fromTheme("edit-copy", QIcon(":/images/button-download.png")), tr("Copy post link to clipboard"), this); connect(copyPostUrlAction, SIGNAL(triggered()), this, SLOT(copyPostUrlToClipboard())); postAuthorAvatarButton->addActionToMenu(copyPostUrlAction); // Disable Open and Copy URL if there's no URL if (this->postUrl.isEmpty()) { openPostInBrowserAction->setDisabled(true); copyPostUrlAction->setDisabled(true); } // ----- postAuthorAvatarButton->addSeparatorToMenu(); // Normalize text colors normalizeTextAction = new QAction(QIcon::fromTheme("format-text-color"), tr("Normalize text colors"), this); connect(normalizeTextAction, SIGNAL(triggered()), this, SLOT(normalizeTextFormat())); postAuthorAvatarButton->addActionToMenu(normalizeTextAction); if (standalone) // own window, add close option to menu { // ----- postAuthorAvatarButton->addSeparatorToMenu(); /* * 1.3.5: This will probably not be needed anymore for any environment * since Posts are now windows, not dialogs, but still... */ this->closeAction = new QAction(QIcon::fromTheme("window-close", QIcon(":/images/button-close.png")), tr("&Close"), this); connect(closeAction, SIGNAL(triggered()), this, SLOT(close())); postAuthorAvatarButton->addActionToMenu(closeAction); } // End avatar menu leftColumnLayout->addWidget(postAuthorAvatarButton, 0, Qt::AlignLeft); QFont authorFont; authorFont.setBold(true); if (postIsOwn) // Another visual hint when the post is our own { authorFont.setItalic(true); } // Author name postAuthorNameLabel = new QLabel(postAuthorName, this); postAuthorNameLabel->setTextFormat(Qt::PlainText); postAuthorNameLabel->setWordWrap(true); ///////////////// FIXME: make optional postAuthorNameLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); postAuthorNameLabel->setFont(authorFont); // Ensure a certain width for the metadata column, by setting a minimum width for this label postAuthorNameLabel->setMinimumWidth(90); // FIXME: use font metrics for some chars instead leftColumnLayout->addWidget(postAuthorNameLabel); leftColumnLayout->addSpacing(1); // Post creation time postCreatedAtString = activity->object()->getCreatedAt(); // Timestamp format is "Combined date and time in UTC", like // "2012-02-07T01:32:02Z" as per ISO8601 http://en.wikipedia.org/wiki/ISO_8601 postCreatedAtLabel = new HClabel(QString(), this); postCreatedAtLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); postCreatedAtLabel->setFont(detailsFont); postCreatedAtLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); leftColumnLayout->addWidget(postCreatedAtLabel); // Show generator app, if enabled to display directly if (!this->postGeneratorString.isEmpty() && globalObj->getPostShowExtraInfo()) { //QString::fromUtf8("\342\232\231 ") // Gear symbol this->postGeneratorLabel = new QLabel(QString::fromUtf8("\342\214\250 ") // Keyboard symbol + this->postGeneratorString, this); postGeneratorLabel->setFont(detailsFont); postGeneratorLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum); leftColumnLayout->addWidget(postGeneratorLabel); } leftColumnLayout->addSpacing(4); // Location information, if any this->postLocationLabel = new HClabel(QString(), this); postLocationLabel->setFont(detailsFont); postLocationLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); leftColumnLayout->addWidget(postLocationLabel); leftColumnLayout->addSpacing(2); postLocationLabel->hide(); if (!activity->isShared()) // Because when shared, To/Cc are shown with share info { if (!activity->getToString().isEmpty()) { this->postToLabel = new QLabel(tr("To") + ": " + activity->getToString() + "  ", // Small hack to try to ensure full visibility this); postToLabel->setWordWrap(true); postToLabel->setOpenExternalLinks(true); postToLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); postToLabel->setFont(detailsFont); leftColumnLayout->addWidget(postToLabel); connect(postToLabel, SIGNAL(linkHovered(QString)), this, SLOT(showHighlightedUrl(QString))); } if (!activity->getCCString().isEmpty()) { this->postCCLabel = new QLabel(tr("Cc") + ": " + activity->getCCString() + "  ", this); postCCLabel->setWordWrap(true); postCCLabel->setOpenExternalLinks(true); postCCLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); postCCLabel->setFont(detailsFont); leftColumnLayout->addWidget(postCCLabel); connect(postCCLabel, SIGNAL(linkHovered(QString)), this, SLOT(showHighlightedUrl(QString))); } } leftColumnLayout->addSpacing(4); // Set these labels 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 HClabel(QString(), this); postLikesCountLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); postLikesCountLabel->setFont(detailsFont); postLikesCountLabel->setOpenExternalLinks(true); postLikesCountLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); connect(postLikesCountLabel, SIGNAL(linkHovered(QString)), this, SLOT(showHighlightedUrl(QString))); leftColumnLayout->addWidget(postLikesCountLabel); postLikesCountLabel->hide(); postCommentsCountLabel = new QLabel(this); postCommentsCountLabel->setWordWrap(true); postCommentsCountLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); postCommentsCountLabel->setFont(detailsFont); leftColumnLayout->addWidget(postCommentsCountLabel); postCommentsCountLabel->hide(); postSharesCountLabel = new HClabel(QString(), this); postSharesCountLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); postSharesCountLabel->setFont(detailsFont); postSharesCountLabel->setOpenExternalLinks(true); postSharesCountLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); connect(postSharesCountLabel, SIGNAL(linkHovered(QString)), this, SLOT(showHighlightedUrl(QString))); leftColumnLayout->addWidget(postSharesCountLabel); postSharesCountLabel->hide(); // Try to use all remaining space, aligning // all previous widgets nicely at the top // leftColumnLayout->setAlignment(Qt::AlignTop) caused glitches leftColumnLayout->addStretch(1); QFont buttonsFont; buttonsFont.setPointSize(buttonsFont.pointSize() - 1); // Check if the object is in reply to something (a shared comment, for instance) this->postParentMap = activity->object()->getInReplyTo(); if (!postParentMap.isEmpty()) { openParentPostButton = new QPushButton(QIcon::fromTheme("go-up-search", QIcon(":/images/button-parent.png")), tr("Parent", "As in 'Open the parent post'. " "Try to use the shortest word!"), this); //openParentPostButton->setFlat(true); openParentPostButton->setFont(buttonsFont); openParentPostButton->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum); openParentPostButton->setToolTip("" + tr("Open the parent post, to which " "this one replies")); connect(openParentPostButton, SIGNAL(clicked()), this, SLOT(openParentPost())); leftColumnLayout->addWidget(openParentPostButton); } leftColumnFrame->setLayout(leftColumnLayout); ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////// Right column, content rightColumnFrame = new QFrame(this); rightColumnFrame->setObjectName("RightFrame"); // For css changes later rightColumnLayout = new QVBoxLayout(); rightColumnLayout->setAlignment(Qt::AlignTop); ///////////////////////////////////// Title this->postTitleLabel = new QLabel(this); postTitleLabel->setWordWrap(true); QFont postTitleFont; postTitleFont.fromString(globalObj->getPostTitleFont()); postTitleLabel->setFont(postTitleFont); rightColumnLayout->addWidget(postTitleLabel, 0, Qt::AlignTop); ///////////////////////////////////// Summary QString postSummary = activity->object()->getSummary(); if (!postSummary.isEmpty()) { this->postSummaryLabel = new QLabel(this); postSummaryLabel->setWordWrap(true); postSummaryLabel->setFont(detailsFont); postSummaryLabel->setText(postSummary); rightColumnLayout->addWidget(postSummaryLabel, 0); } ///////////////////////////////////// Post text postText = new QTextBrowser(this); postText->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); postText->setMinimumSize(10, 10); postText->setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); postText->setOpenLinks(false); // don't open links, manage in openClickedUrl() postText->setReadOnly(true); // it's default with QTextBrowser, but still... QFont postContentsFont; postContentsFont.fromString(globalObj->getPostContentsFont()); postText->setFont(postContentsFont); // To help screen readers, enable keyboard interaction here // This is disabled for now, since it doesn't really help; hopefully with Qt5 // things will be better. //postText->setTextInteractionFlags(Qt::TextBrowserInteraction // | Qt::TextSelectableByKeyboard); connect(postText, SIGNAL(anchorClicked(QUrl)), this, SLOT(openClickedUrl(QUrl))); connect(postText, SIGNAL(highlighted(QString)), this, SLOT(showHighlightedUrl(QString))); rightColumnLayout->addWidget(postText, 1); // No alignment; makes size wrong when standalone // Buttons // Add like, comment, share and, if post is ours, edit and delete buttons buttonsLayout = new QHBoxLayout(); buttonsLayout->setContentsMargins(0, 0, 0, 0); buttonsLayout->setMargin(0); buttonsLayout->setSpacing(0); // Like button likeButton = new QPushButton(QIcon::fromTheme("emblem-favorite", QIcon(":/images/button-like.png")), "*like*", this); likeButton->setCheckable(true); this->fixLikeButton(activity->object()->isLiked()); likeButton->setFlat(true); likeButton->setFont(buttonsFont); connect(likeButton, SIGNAL(clicked(bool)), this, SLOT(likePost(bool))); buttonsLayout->addWidget(likeButton, 0, Qt::AlignLeft); // Only add Comment and Share buttons if it's NOT a comment if (postType != "comment") // (can happen in the Favorites timeline or via shares) { // Comment (reply) button commentButton = new QPushButton(QIcon::fromTheme("mail-reply-sender", QIcon(":/images/button-comment.png")), tr("Comment", "verb, for the comment button"), this); commentButton->setToolTip(tr("Reply to 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())); buttonsLayout->addWidget(commentButton, 0, Qt::AlignLeft); // Share button shareButton = new QPushButton(QIcon::fromTheme("mail-forward", QIcon(":/images/button-share.png")), "*share*", this); // Note: Gwenview includes 'document-share' icon shareButton->setFlat(true); shareButton->setFont(buttonsFont); if (postSharedById.isEmpty() || (pController->currentUserId() != this->postSharedById)) { shareButton->setText(tr("Share")); shareButton->setToolTip("" + tr("Share this post with your contacts")); 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.2.x, since it doesn't really work } 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", QIcon(":/images/button-edit.png")), tr("Edit"), this); editButton->setToolTip("" + tr("Modify 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", QIcon(":/images/button-delete.png")), tr("Delete"), this); deleteButton->setToolTip("" + tr("Erase this post")); deleteButton->setFlat(true); deleteButton->setFont(buttonsFont); connect(deleteButton, SIGNAL(clicked()), this, SLOT(deletePost())); buttonsLayout->addWidget(deleteButton, 0, Qt::AlignRight); } /******************************************************************/ this->pendingImagesList.clear(); // Will add the own "post image", and -based ones // Get URL of post image, if it's "image" type of post if (this->postType == "image") { postSmallImageUrl = activity->object()->getSmallImageUrl(); postFullImageUrl = activity->object()->getImageUrl(); postAttachmentPureUrl = activity->object()->getAttachmentPureUrl(); if (globalObj->getPostFullImages()) { postImageUrl = postFullImageUrl; } else { postImageUrl = postSmallImageUrl; } pendingImagesList.append(postImageUrl); // TMP/FIXME: optionally autodownload the full size version // when using thumbnails in the timeline? // Get stored size this->postImageSize = QSize(activity->object()->getImageWidth(), activity->object()->getImageHeight()); } QString attachmentUrl; // Get URL of post audio file, if it's "audio" type of post if (this->postType == "audio") { postAudioUrl = activity->object()->getAudioUrl(); attachmentUrl = postAudioUrl; postAttachmentPureUrl = activity->object()->getAttachmentPureUrl(); qDebug() << "we got AUDIO:" << postAudioUrl; } // Get URL of post video file, if it's "video" type of post if (this->postType == "video") { postVideoUrl = activity->object()->getVideoUrl(); attachmentUrl = postVideoUrl; postAttachmentPureUrl = activity->object()->getAttachmentPureUrl(); qDebug() << "we got VIDEO:" << postVideoUrl; } // Get URL of post general file, if it's "file" type of post if (this->postType == "file") { postFileUrl = activity->object()->getFileUrl(); attachmentUrl = postFileUrl; postAttachmentPureUrl = activity->object()->getAttachmentPureUrl(); postFileMimeType = activity->object()->getMimeType(); qDebug() << "we got FILE:" << postFileUrl << "; type:" << postFileMimeType; } // Button to join/leave the group, if the post is the creation of a group if (postType == "group") // FIXME: for now, only JOIN, as we can't know if we're members yet { this->joinLeaveButton = new QPushButton(QIcon::fromTheme("user-group-new", QIcon(":/images/list-add.png")), tr("Join Group"), this); connect(joinLeaveButton, SIGNAL(clicked()), this, SLOT(joinGroup())); rightColumnLayout->addWidget(joinLeaveButton, 0, Qt::AlignCenter); this->groupInfoLabel = new QLabel(tr("%1 members in the group") .arg(activity->object()->getMemberCount()), this); #ifdef GROUPSUPPORT groupInfoLabel->setText(groupInfoLabel->text() + "
              object()->getId() + "\">-GROUP ID-"); #endif groupInfoLabel->setWordWrap(true); groupInfoLabel->setFont(detailsFont); groupInfoLabel->setAlignment(Qt::AlignCenter); rightColumnLayout->addWidget(groupInfoLabel); } // Widget to download the attached media, if any if (!attachmentUrl.isEmpty()) { QString suggestedFilename = MiscHelpers::getSuggestedFilename(this->postAuthorId, this->postType, this->postTitle, this->postAttachmentPureUrl, this->postFileMimeType); this->downloadWidget = new DownloadWidget(attachmentUrl, suggestedFilename, this->pController, this); rightColumnLayout->addWidget(downloadWidget, 0); } // FIXME: button creation should be moved here rightColumnLayout->addLayout(buttonsLayout, 0); // Show which filtering rules caused this post to be highlighted QVariantMap filterMatches = activity->getFilterMatches(); if (filterMatches.value("filterAction").toInt() == FilterChecker::Highlight) { this->filterMatchesWidget = new FilterMatchesWidget(filterMatches, this); rightColumnLayout->addWidget(filterMatchesWidget); // FIXME -- TODO: make this widget optional + way to hide it later } /////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// Comments block this->commenter = new CommenterBlock(this->pController, this->globalObj, this->postId, this->postAuthorId, this->standalone, this); connect(commenter, SIGNAL(commentSent(QString)), this, SLOT(sendComment(QString))); connect(commenter, SIGNAL(commentUpdated(QString,QString)), this, SLOT(updateComment(QString,QString))); connect(commenter, SIGNAL(allCommentsRequested()), this, SLOT(getAllComments())); rightColumnLayout->addWidget(commenter, 0); rightColumnLayout->addStretch(0); // Push other stuff to the top if there's extra space rightColumnFrame->setLayout(rightColumnLayout); if (standalone) { // Set link to "check for comments" this->commenter->updateShowAllLink(); this->updateBestCommentsUrl(); // If we still don't have a valid URL from our host, proxyURL or not.. if (!this->canGetAllComments()) { this->commenter->disableShowAllLink(); } } mainLayout = new QHBoxLayout(); mainLayout->setContentsMargins(0, 0, 0, 0); // Minimal margins mainLayout->setSpacing(1); mainLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop); mainLayout->addWidget(leftColumnFrame, 3); // stretch 3/17 mainLayout->addWidget(rightColumnFrame, 14); // stretch 14/17 if (activity->isShared()) { if (globalObj->getPostExtendedShares()) { // Add a vertical hint on left side, a line and a recycling symbol this->shareHintLabel = new QLabel(QString::fromUtf8("\342\231\272"), this); shareHintLabel->setToolTip(this->postIsSharedLabel->toolTip()); shareHintLabel->setAlignment(Qt::AlignBottom); shareHintLabel->setFrameStyle(QFrame::VLine); mainLayout->insertWidget(0, shareHintLabel); } outerLayout = new QVBoxLayout(); outerLayout->setContentsMargins(0, 0, 0, 0); outerLayout->setSpacing(0); outerLayout->setAlignment(Qt::AlignTop); outerLayout->addWidget(postIsSharedLabel, 0); // Ensure it's small outerLayout->addLayout(mainLayout, 10); this->setLayout(outerLayout); } else { this->setLayout(mainLayout); } // Set read status this->setPostUnreadStatus(); // Set timestamps; Doing this here, after CommenterBlock has been initialized too this->setFuzzyTimestamps(); this->resizeTimer = new QTimer(this); resizeTimer->setSingleShot(true); connect(resizeTimer, SIGNAL(timeout()), this, SLOT(delayedResize())); this->updateDataFromActivity(activity); qDebug() << "Post created" << this->postId; } Post::~Post() { qDebug() << "Post destroyed" << this->postId; } /* * Fill in or replace data such as audience, generator, * or object data from activity data * */ void Post::updateDataFromActivity(ASActivity *activity) { qDebug() << "Updating Post() data from activity..."; // set To/CC..... TODO this->updateDataFromObject(activity->object()); } /* * Fill in or replace data such as timestamps, title, contents, * number of likes and shares, etc, from object data * */ void Post::updateDataFromObject(ASObject *object) { qDebug() << "Updating Post() data from object..."; //////////////////////////////////////////////////////////////// Metadata // Post update time, if any this->postUpdatedAtString = object->getUpdatedAt(); if (this->postUpdatedAtString == this->postCreatedAtString || !object->getDeletedTime().isEmpty()) // Edited time useless when deleted { this->postUpdatedAtString.clear(); } QString postCreatedAtExtendedText = tr("Posted on %1", "1=Date") .arg(Timestamp::localTimeDate(postCreatedAtString)); QString dateGeneratorTooltip = tr("Type", "As in: type of object") + ": " + object->getTranslatedType(postType) + "\n" + postCreatedAtExtendedText; if (!this->postGeneratorString.isEmpty()) { dateGeneratorTooltip.append("\n" + tr("Using %1", "1=Program used for " "posting or sharing") .arg(this->postGeneratorString)); } if (!postUpdatedAtString.isEmpty()) { // Using \n instead of
              because I don't want this tooltip to be HTML (wrapped) QString modifiedOn = tr("Modified on %1") .arg(Timestamp::localTimeDate(postUpdatedAtString)); postCreatedAtExtendedText.append("
              " + modifiedOn); dateGeneratorTooltip.append("\n\n" + modifiedOn); } postCreatedAtLabel->setExtendedText(postCreatedAtExtendedText); postCreatedAtLabel->setToolTip(dateGeneratorTooltip); this->setFuzzyTimestamps(); // Save post title for openClickedUrl(), editPost() etc this->postTitle = object->getTitle(); if (!this->postTitle.isEmpty()) { postTitleLabel->setText(postTitle); postTitleLabel->show(); } else { postTitleLabel->hide(); } // Raw HTML of main content this->postOriginalText = object->getContent(); // Save it for later... QStringList postTextImageList = MiscHelpers::htmlWithReplacedImages(postOriginalText, this->postText->width()); postTextImageList.removeFirst(); // First is the HTML itself qDebug() << "Post has" << postTextImageList.size() << "images included..."; pendingImagesList.append(postTextImageList); this->getPendingImages(); // Location information QString location = object->getLocationName(); if (!location.isEmpty()) { this->postLocationLabel->setBaseText(tr("In") + ": " + location + ""); postLocationLabel->setExtendedText(object->getLocationTooltip()); postLocationLabel->setToolTip("" + QString::fromUtf8("\342\214\226 ") // Position indicator symbol + "" + object->getLocationTooltip()); postLocationLabel->show(); } else { postLocationLabel->hide(); } ///////////////////////////////// Likes - Comments - Shares information /* FIXME: this needs some serious work. * * Separation between what's initially set and what can be * updated later from minor feed information * */ // Set the initial likes, comments and shares (4 most recent) int likesCount = object->getLikesCount(); if (likesCount > 0) { this->postLikesCount = likesCount; this->setLikesLabel(this->postLikesCount); this->setLikes(object->getLastLikesList(), this->postLikesCount); } int commentsCount = object->getCommentsCount(); if (commentsCount > 0 && !standalone) // Don't set initial comments in standalone mode { // It's unnecessary and messes up comment area size this->setCommentsLabel(commentsCount); this->commenter->setComments(object->getLastCommentsList(), commentsCount); } int sharesCount = object->getSharesCount(); this->setSharesLabel(sharesCount); this->setShares(object->getLastSharesList(), sharesCount); // If there's a "deleted" key, show minimalistic version of the post indicating it QString postDeletedTime = object->getDeletedOnString(); if (!postDeletedTime.isEmpty()) { this->setPostDeleted(postDeletedTime); // If the setting to show deleted posts if off, hide if (!globalObj->getShowDeleted()) { // FIXME: this also hides posts that were not deleted when added to the timeline this->hide(); } } this->setPostContents(); } void Post::updateCommentFromObject(ASObject *object) { this->commenter->updateCommentFromObject(object); } void Post::setCommentDeletedFromObject(ASObject *object) { this->commenter->setCommentDeletedFromObject(object); } /* * Set the post contents, and trigger a vertical resize * */ void Post::setPostContents() { //qDebug() << "Post::setPostContents()" << this->postId; QString postMediaHtml; // Embedded image int imageWidth; if (!this->postImageUrl.isEmpty()) { QString imageCachedFilename = MiscHelpers::getCachedImageFilename(postImageUrl); if (QFile::exists(imageCachedFilename)) { // If the image is wider than the post space, make it smaller imageWidth = qMin(MiscHelpers::getImageWidth(imageCachedFilename), this->postWidth); this->postImageIsAnimated = MiscHelpers::isImageAnimated(imageCachedFilename); QString belowMessage; if (postImageIsAnimated) { belowMessage = "" + QString::fromUtf8("\342\226\266") // Play-like symbol + " " + tr("Image is animated. Click on it to play.") + " " + QString::fromUtf8("\342\232\231") // Gear symbol + ""; } else { belowMessage = this->seeFullImageString; } QString resolution = tr("Size", "Image size (resolution)") + ": " + MiscHelpers::resolutionString(postImageSize.width(), postImageSize.height()); postMediaHtml = MiscHelpers::mediaHtmlBase(this->postType, imageCachedFilename, resolution, belowMessage, imageWidth); } else // use placeholder image while it loads, or if it failed... { QString imageLoadingString; QString placeholderFilename; if (this->postImageFailed) { imageLoadingString = tr("Couldn't load image!"); placeholderFilename = "image-missing.png"; } else { imageLoadingString = tr("Loading image..."); placeholderFilename = "image-loading.png"; } postMediaHtml = "
              " "
              " "" "

              " + imageLoadingString + "" "
              " "

              "; } } // Embedded audio if (!this->postAudioUrl.isEmpty()) { postMediaHtml = MiscHelpers::mediaHtmlBase(this->postType, ":/images/attached-audio.png", this->downloadAttachmentString, tr("Attached Audio")); } // Embedded video if (!this->postVideoUrl.isEmpty()) { postMediaHtml = MiscHelpers::mediaHtmlBase(this->postType, ":/images/attached-video.png", this->downloadAttachmentString, tr("Attached Video")); } // Attached file if (!this->postFileUrl.isEmpty()) { QByteArray iconDataUri; QMimeDatabase mimeDb; QMimeType mimeType = mimeDb.mimeTypeForName(this->postFileMimeType); const QString mimeIcon = mimeType.iconName(); if (QIcon::hasThemeIcon(mimeIcon)) { QPixmap mimePixmap = QIcon::fromTheme(mimeIcon).pixmap(128, 128); QBuffer mimeBuffer; mimeBuffer.open(QIODevice::WriteOnly); mimePixmap.save(&mimeBuffer, "PNG"); // Compose a "Data URI" with the icon-as-png data iconDataUri = "data:image/png;base64," + mimeBuffer.buffer().toBase64(); mimeBuffer.close(); } else { iconDataUri = ":/images/attached-file.png"; } postMediaHtml = MiscHelpers::mediaHtmlBase(this->postType, iconDataUri, this->downloadAttachmentString, tr("Attached File") + QString(": %1 (%2)") .arg(mimeType.comment(), postFileMimeType)); } // Text contents QStringList postTextImageList = MiscHelpers::htmlWithReplacedImages(postOriginalText, this->postWidth); QString postTextContents = postTextImageList.takeAt(0); // Add the text content of the post postText->setHtml(postMediaHtml + postTextContents); this->setPostHeight(); } void Post::onResizeOrShow() { this->postWidth = qMax(postText->width() - 80, // FIXME: use viewport? 50); // minimum width of 50 this->setPostContents(); this->commenter->adjustCommentsHeight(); this->commenter->adjustCommentArea(); } /* * Set the height of the post, based on the contents * */ void Post::setPostHeight() { // Minimum height depends on configured avatar size int minHeight = qMax(this->globalObj->getPostAvatarSize().height(), 64); // Absolute minimum of 64, even if avatar is 32x32 // Unless the post is deleted, in which case it can be smaller if (postIsDeleted) { minHeight = 20; } int height = qMax(postText->document()->size().toSize().height() + 12, // +12px error margin minHeight); // Don't allow a post to be too tall, either height = qMin(height, this->globalObj->getTimelineHeight()); if (standalone) { postText->setMinimumHeight(minHeight); } else // Only force a specific height if the post is NOT standalone { postText->setMinimumHeight(height); postText->setMaximumHeight(height); } } /* * Download post image, and img-tag-based images parsed from the contents * */ void Post::getPendingImages() { if (!pendingImagesList.isEmpty()) { foreach (QString imageUrl, pendingImagesList) { pController->enqueueImageForDownload(imageUrl); } connect(pController, SIGNAL(imageStored(QString)), this, SLOT(redrawImages(QString))); connect(pController, SIGNAL(imageFailed(QString)), this, SLOT(onImageFailed(QString))); } } /* * Return the likes/favorites URL for this post * */ QString Post::likesUrl() { return this->postLikesUrl; } /* * Update the tooltip/extended info in "%NUMBER likes" with the names * of the people who liked the post * */ void Post::setLikes(QVariantList likesList, int likesCount) { if (likesList.size() > 0) { this->postLikesMap.clear(); this->postLikesMap = ASObject::simplePersonMapFromList(likesList); this->refreshLikesInfo(likesList.size(), likesCount); } // 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->postLikesCount = likesCount; // use the passed likesCount parameter } else { this->postLikesCount = likesList.size(); // show size of actual names list } this->setLikesLabel(this->postLikesCount); } void Post::appendLike(QString actorId, QString actorName, QString actorUrl) { if (!postLikesMap.contains(actorId)) { ASObject::addOnePersonToSimpleMap(actorId, actorName, actorUrl, &this->postLikesMap); ++this->postLikesCount; this->refreshLikesInfo(this->postLikesCount, this->postLikesCount); // FIXME? this->setLikesLabel(this->postLikesCount); } } void Post::removeLike(QString actorId) { if (postLikesMap.contains(actorId)) { this->postLikesMap.remove(actorId); --this->postLikesCount; this->refreshLikesInfo(this->postLikesCount, this->postLikesCount); // FIXME? this->setLikesLabel(this->postLikesCount); } } void Post::refreshLikesInfo(int likesListSize, int likesCount) { QString peopleString = ASObject::personStringFromSimpleMap(this->postLikesMap, likesCount); QString likesString; if (likesListSize == 1) { likesString = tr("%1 likes this", "One person").arg(peopleString); } else { likesString = tr("%1 like this", "More than one person").arg(peopleString); } this->postLikesCountLabel->setExtendedText(likesString); this->postLikesCountLabel->setToolTip("" // Turn the tooltip into rich text + likesString); } /* * Update the "NUMBER likes" label in left side * */ void Post::setLikesLabel(int likesCount) { if (likesCount != 0) { if (likesCount == 1) { postLikesCountLabel->setBaseText(QString::fromUtf8("\342\231\245 ") // Heart symbol + tr("1 like")); } else { postLikesCountLabel->setBaseText(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) { int commentCount = commentsList.size(); this->commenter->setComments(commentsList, commentCount); this->commenter->adjustCommentArea(); // update number of comments in left side counter this->setCommentsLabel(commentCount); } void Post::appendComment(ASObject *comment) { this->commenter->appendComment(comment, true); // Just this one this->commenter->adjustCommentsHeight(); this->commenter->adjustCommentArea(); this->setCommentsLabel(this->commenter->getCommentCount()); } /* * Update the "NUMBER comments" label in left side * */ void Post::setCommentsLabel(int commentsCount) { if (commentsCount != 0) { QString countString = QString::fromUtf8("\342\234\215 "); // writing hand if (commentsCount == 1) { countString.append(tr("1 comment")); } else { countString.append(tr("%1 comments").arg(commentsCount)); } postCommentsCountLabel->setText(countString); postCommentsCountLabel->show(); } else { postCommentsCountLabel->clear(); postCommentsCountLabel->hide(); } } void Post::updateBestCommentsUrl() { // If comments URL is in another server and not proxyed... if (!pController->urlIsInOurHost(this->postCommentsUrl)) { // Try to find the post in the 'ever seen' list QString newCommentsUrl = pController->commentsUrlForPost(this->postId); if (!newCommentsUrl.isEmpty()) { qDebug() << "Found matching post; Updating comments URL..."; this->postCommentsUrl = newCommentsUrl; } } } /* * 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 = ASObject::personStringFromList(sharesList, sharesCount); 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); } this->postSharesCountLabel->setExtendedText(sharesString); this->postSharesCountLabel->setToolTip("" // So the tooltip is rich text, wordwrapped + 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) { postSharesCountLabel->setBaseText(QString::fromUtf8("\342\231\273 ") // Recycle symbol + tr("Shared once")); } else { postSharesCountLabel->setBaseText(QString::fromUtf8("\342\231\273 ") // Recycle symbol + tr("Shared %1 times").arg(resharesCount)); } postSharesCountLabel->show(); } else { postSharesCountLabel->clear(); postSharesCountLabel->hide(); } } /* * Set or unset the visual hint indicating if the post is unread * */ void Post::setPostUnreadStatus() { if (!QColor::isValidColor(unreadPostColor)) { unreadPostColor = "palette(highlight)"; } // CSS for horizontal gradient from configured color to transparent QString css = QString("QFrame#RightFrame " "{ background-color: " " qlineargradient(spread:pad, " " x1:0, y1:0, x2:1, y2:0, " " stop:0 rgba(0, 0, 0, 0), " " stop:1 %1); " "}").arg(unreadPostColor); if (this->postIsUnread) { rightColumnFrame->setStyleSheet(css); postCreatedAtLabel->setHighlighted(true); } else { rightColumnFrame->setStyleSheet(QString()); postCreatedAtLabel->setHighlighted(false); } // Avoid flickering effect later this->postCreatedAtLabel->repaint(); } void Post::setPostAsNew() { this->postIsUnread = true; setPostUnreadStatus(); } void Post::setPostAsRead(bool informTimeline) { if (postIsUnread) { this->postIsUnread = false; if (informTimeline) { // Inform the Timeline() if (this->highlightType == NoHighlight) { emit postRead(false); } else { emit postRead(true); // Say it's marked as read, and was highlighted } } setPostUnreadStatus(); } } void Post::setPostDeleted(QString postDeletedTime) { if (this->postIsDeleted) { // Already set as deleted, ignore return; } /// FIXME, this needs some work if (!this->postOriginalText.isEmpty()) { this->postOriginalText.prepend("
              "); // -------- } this->postOriginalText.prepend("
              " + postDeletedTime + "
              "); // kinda tmp... qDebug() << "This post was deleted on" << postDeletedTime; this->postIsDeleted = true; this->setDisabled(true); this->setPostContents(); } int Post::getHighlightType() { return this->highlightType; } QString Post::getActivityId() { return this->activityId; } QString Post::getObjectId() { return this->postId; } void Post::setFuzzyTimestamps() { QString fuzzyTimestamps = Timestamp::fuzzyTime(postCreatedAtString); if (!postUpdatedAtString.isEmpty()) { fuzzyTimestamps.append("
              " + tr("Edited: %1") .arg(Timestamp::fuzzyTime(postUpdatedAtString))); } this->postCreatedAtLabel->setBaseText(fuzzyTimestamps); // Update "share time" on share info too! if (!postShareInfoString.isEmpty()) { QString shareLabelString; shareLabelString = this->postShareInfoString; shareLabelString.append(Timestamp::fuzzyTime(postShareTime)); shareLabelString.append(this->postSharedToCCString); shareLabelString.append(QString::fromUtf8("     " "\342\231\272")); // Recycle symbol, again this->postIsSharedLabel->setText(shareLabelString); } commenter->updateFuzzyTimestamps(); } void Post::syncAvatarFollowState() { this->postAuthorAvatarButton->syncFollowState(); // Ask CommenterBlock to update its avatars too this->commenter->updateAvatarFollowStates(); } bool Post::isBeingCommented() { if (this->commenter->isFullMode()) { return true; } return false; } bool Post::isNew() { return this->postIsUnread; } //////////////////////////////////////////////////////////////////////////// ////////////////////////////////// SLOTS /////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /* * Like (favorite) a post * */ void Post::likePost(bool like) { qDebug() << "Post::likePost()" << (like ? "like" : "unlike"); this->pController->likePost(this->postId, this->postType, this->postAuthorId, like); this->fixLikeButton(like); connect(pController, SIGNAL(likeSet()), this, SLOT(getAllLikes())); // Clicking 'Like' doesn't generate a mousePressEvent, so mark as read here this->setPostAsRead(); } /* * Set the right labels and tooltips to the like button, depending on its state * */ void Post::fixLikeButton(bool isLiked) { if (isLiked) { likeButton->setToolTip("" + tr("You like this")); likeButton->setText(tr("Unlike")); } else { likeButton->setToolTip("" + tr("Like this post")); likeButton->setText(tr("Like")); } likeButton->setChecked(isLiked); } 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 * * If some text was selected, insert it as quoted text * */ void Post::commentOnPost() { qDebug() << "Commenting on post" << this->postTitle << this->postId; QString initialText; QString selectedText = this->postText->textCursor().selectedText(); if (!selectedText.isEmpty()) { // Selected text has literal < and >, so convert to HTML entities selectedText.replace("<", "<"); selectedText.replace(">", ">"); selectedText.prepend("[...] "); selectedText.append(" [...]"); initialText = MiscHelpers::quotedText(this->postAuthorName, selectedText); } this->commenter->setFullMode(initialText); // Show "Check for comments" link, if it wasn't present, in some form, already if (this->canGetAllComments()) // As long as we can actually get them { this->commenter->updateShowAllLink(); } emit commentingOnPost(this->commenter); // Clicking 'Comment' doesn't generate a mousePressEvent, so mark as read here this->setPostAsRead(); } /* * The actual sending of the comment to the Pump controller * */ void Post::sendComment(QString commentText) { qDebug() << "About to publish this comment:" << commentText; this->pController->addComment(MiscHelpers::cleanupHtml(commentText), this->postId, this->postType); } void Post::updateComment(QString commentId, QString commentText) { this->pController->updateComment(commentId, MiscHelpers::cleanupHtml(commentText), this->postId); } void Post::requestCommenterComments() { if (this->canGetAllComments()) { this->commenter->requestAllComments(); } } void Post::getAllComments() { this->updateBestCommentsUrl(); /* // Keeping this for the future * // to be used to copy over comments directly (TBD) // Try to find this same post in one of the major timelines qDebug() << "Looking for this post in the major timelines, for comments"; MainWindow *mainWindow = (MainWindow *)this->globalObj->parent(); // Kinda risky bool postFoundOk = false; Post *alternatePost = mainWindow->findPostInTimelines(this->postId, &postFoundOk); if (postFoundOk) { qDebug() << "Found the post; Updating comments Url..."; this->postCommentsUrl = alternatePost->commentsUrl(); } */ this->pController->getPostComments(this->postCommentsUrl, this->postId); } /* * 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("?").first(); if (this->commentsUrl() == originatingPostCleanUrl) { this->setComments(commentsList); } } bool Post::canGetAllComments() { return pController->urlIsInOurHost(this->postCommentsUrl); } /* * Share a post. * * TODO: Turn this into a custom dialog allowing to select * custom recipients for the 'share' activity. * */ void Post::sharePost() { // Clicking 'Share' doesn't generate a mousePressEvent, so mark as read here this->setPostAsRead(); QString shareConfirmString; if (!this->postIsOwn) { shareConfirmString = tr("Do you want to share %1's post?") .arg(this->postAuthorName); } else { shareConfirmString = tr("Are you sure you want to share " "your own post?"); } int confirmation = QMessageBox::question(this, tr("Share post?"), shareConfirmString + "\n\n\n", tr("&Yes, share it"), tr("&No"), QString(), 1, 1); if (confirmation == 0) { qDebug() << "Sharing this post:" << this->postId; this->pController->sharePost(this->postId, this->postType); } else { qDebug() << "Confirmation cancelled, not sharing"; } } void Post::unsharePost() { int confirmation = QMessageBox::question(this, tr("Unshare post?"), tr("Do you want to unshare %1's post?") .arg(this->postAuthorName), tr("&Yes, unshare it"), tr("&No"), QString(), 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 cancelled, will not unshare"; } } /* * Set the Publisher in editing mode with this post's contents * */ void Post::editPost() { this->globalObj->editPost(this->postId, this->postType, this->postTitle, this->postOriginalText); if (standalone) { this->close(); } } /* * 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"), QString(), 1, 1); if (confirmation == 0) { qDebug() << "Deleting post"; this->pController->deletePost(this->postId, this->postType); QString timeNow = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); this->setPostDeleted(ASObject::makeDeletedOnString(timeNow)); } else { qDebug() << "Confirmation cancelled, not deleting the post"; } } void Post::joinGroup() { qDebug() << "Joining group " << this->postId << "via Post() button"; this->pController->joinGroup(this->postId); this->joinLeaveButton->setDisabled(true); // FIXME; make it turn into "Leave", etc. } 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..."; QString suggestedFilename = MiscHelpers::getSuggestedFilename(this->postAuthorId, this->postType, this->postTitle, this->postAttachmentPureUrl); ImageViewer *viewer = new ImageViewer(this->postFullImageUrl, this->postImageSize, this->postTitle, suggestedFilename, this->postImageIsAnimated, nullptr); // Independent window connect(pController, SIGNAL(imageStored(QString)), viewer, SLOT(reloadImage(QString))); connect(pController, SIGNAL(imageFailed(QString)), viewer, SLOT(onImageFailed(QString))); this->pController->enqueueImageForDownload(this->postFullImageUrl); viewer->show(); } else if (url.scheme() == "attachment") { this->downloadWidget->downloadAttachment(); } else { qDebug() << "Opening this link in browser"; MiscHelpers::openUrl(url, this); } } void Post::showHighlightedUrl(QString url) { if (!url.isEmpty()) { if (url.startsWith(QStringLiteral("image:/"))) // Own image:/ URL { QString statusMessage = this->seeFullImageString + " (" + MiscHelpers::resolutionString(postImageSize.width(), postImageSize.height()) + ")"; this->pController->showTransientMessage(statusMessage); } else if (url.startsWith(QStringLiteral("attachment:/"))) { this->pController->showTransientMessage(this->downloadAttachmentString); } else // Normal URL { this->pController->showTransientMessage(url); qDebug() << "Highlighted url in post:" << url; } } else { this->pController->showTransientMessage(QString()); } } void Post::openPostInBrowser() { MiscHelpers::openUrl(this->postUrl, this); } void Post::copyPostUrlToClipboard() { QApplication::clipboard()->setText(this->postUrl); } void Post::openParentPost() { qDebug() << "Opening parent post..." << this->postParentMap.value("id").toString(); // Create a fake activity for the parent post QVariantMap fakeActivityMap; fakeActivityMap.insert("object", this->postParentMap); fakeActivityMap.insert("actor", this->postParentMap.value("author").toMap()); ASActivity *originalPostActivity = new ASActivity(fakeActivityMap, pController->currentUserId(), this); Post *parentPost = new Post(originalPostActivity, false, // Not highlighted true, // Post is standalone pController, globalObj, nullptr); // Independent window parentPost->show(); connect(pController, SIGNAL(commentsReceived(QVariantList,QString)), parentPost, SLOT(setAllComments(QVariantList,QString))); parentPost->requestCommenterComments(); } /* * Normalize text colors; Used from menu when a post * has white text with white background, or similar * */ void Post::normalizeTextFormat() { postText->selectAll(); // Set default text color for all text postText->setTextColor(qApp->palette().windowText().color()); postText->setTextBackgroundColor(QColor(Qt::transparent)); // Take care of background colors QTextBlockFormat blockFormat = postText->textCursor().blockFormat(); blockFormat.setBackground(QBrush()); postText->textCursor().setBlockFormat(blockFormat); // 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); } void Post::delayedResize() { this->onResizeOrShow(); } /* * Redraw post contents after receiving downloaded images * */ void Post::redrawImages(QString imageUrl) { if (pendingImagesList.contains(imageUrl)) { this->pendingImagesList.removeAll(imageUrl); // If there are no more images for this post, disconnect if (pendingImagesList.isEmpty()) { disconnect(pController, SIGNAL(imageStored(QString)), this, SLOT(redrawImages(QString))); disconnect(pController, SIGNAL(imageFailed(QString)), this, SLOT(onImageFailed(QString))); // Trigger resizeEvent(), with setPostContents(), etc. triggerResize(); } } } void Post::onImageFailed(QString imageUrl) { if (imageUrl == this->postImageUrl) { this->postImageFailed = true; } this->redrawImages(imageUrl); } //////////////////////////////////////////////////////////////////////////// //////////////////////////////// PROTECTED ///////////////////////////////// //////////////////////////////////////////////////////////////////////////// void Post::resizeEvent(QResizeEvent *event) { this->resizeTimer->stop(); this->resizeTimer->start(200); // Schedule content resize event->accept(); } /* * On mouse click in any part of the post, set it as read * */ void Post::mousePressEvent(QMouseEvent *event) { setPostAsRead(); event->accept(); } void Post::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { event->accept(); if (standalone) { this->close(); } } else { event->ignore(); } } /* * Ensure we change back the highlighted URL message when the mouse leaves the post * */ void Post::leaveEvent(QEvent *event) { this->pController->showTransientMessage(QString()); event->accept(); } /* * closeEvent, needed when posts are opened in separate window * */ void Post::closeEvent(QCloseEvent *event) { qDebug() << "Post::closeEvent()"; if (standalone) { QSettings settings; if (settings.isWritable()) { settings.setValue("Post/postSize", this->size()); } } if (this->commenter->isFullMode() && !this->globalObj->isProgramShuttingDown()) { // Ask composer to cancel post, which asks the user (unless empty) this->commenter->getComposer()->cancelPost(); /* * TODO / FIXME: avoid asking for confirmation only when the program * is shutting down due to environment's request */ } event->ignore(); // Event will be ignored anyway // Check again; if it's still full, it means user cancelled if (!this->commenter->isFullMode() // If not, user accepted, so kill the post || this->globalObj->isProgramShuttingDown()) // Kill it either way if shutting down { this->hide(); this->deleteLater(); } } /* * Needed when a post is shown as a standalone window, * to ensure images are properly resized * */ void Post::showEvent(QShowEvent *event) { if (standalone) { this->onResizeOrShow(); } event->accept(); } dianara-v1.4.1/src/imageviewer.h0000644000175000017500000000706113211400267014650 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 #include #include #include #include #include #include #include #include #include "mischelpers.h" #include "ivgraphicsview.h" class ImageViewer : public QWidget { Q_OBJECT public: explicit ImageViewer(QString url, QSize imageSize, QString title, QString suggestedFilename, bool isAnimated, QWidget *parent = 0); ~ImageViewer(); void createContextMenu(); void loadImage(QString url, QSize expectedSize); void drawImage(); void toggleZoomButtons(); void updateButtons(); signals: public slots: void reloadImage(QString url); void onImageFailed(QString url); void saveImage(); void restartAnimation(); void zoomToFit(); void zoomToFull(); void setFullMode(); void zoomIn(); void zoomOut(); void checkAutoFit(); void rotateLeft(); void rotateRight(); void onFitFullToggled(); protected: virtual void closeEvent(QCloseEvent *event); virtual void showEvent(QShowEvent *event); virtual void hideEvent(QHideEvent *event); virtual void resizeEvent(QResizeEvent *event); virtual void contextMenuEvent(QContextMenuEvent *event); private: QVBoxLayout *m_mainLayout; QLabel *m_loadingLabel; QGraphicsPixmapItem *m_graphicsPixmapItem; QGraphicsScene *m_graphicsScene; IvGraphicsView *m_ivGraphicsView; QHBoxLayout *m_buttonsLayout; QPushButton *m_saveButton; QPushButton *m_restartButton; QHBoxLayout *m_fitFullLayout; QPushButton *m_fitButton; QLabel *m_zoomLabel; QPushButton *m_fullButton; QPushButton *m_zoomInButton; QPushButton *m_zoomOutButton; QPushButton *m_rotateLeftButton; QPushButton *m_rotateRightButton; QLabel *m_infoLabel; QPushButton *m_closeButton; QMenu *m_contextMenu; QAction *m_saveAction; QAction *m_toggleFitFullAction; QAction *m_rotateLeftAction; QAction *m_rotateRightAction; QAction *m_closeAction; QTimer *m_autoFitTimer; QString m_imageUrl; QPixmap m_originalPixmap; QString m_originalFileUri; bool m_imageIsAnimated; QLabel *m_movieLabel; QMovie *m_movie; QGraphicsProxyWidget *m_labelProxyWidget; QString m_suggestedFilename; bool m_fitToWindow; double m_zoomLevel; double m_rotationAngle; }; #endif // IMAGEVIEWER_H dianara-v1.4.1/src/profileeditor.cpp0000664000175000017500000003367713207046167015576 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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(tr("Profile Editor") + " - Dianara"); this->setWindowIcon(QIcon::fromTheme("user-properties", QIcon(":/images/no-avatar.png"))); this->setWindowFlags(Qt::Dialog); this->setWindowModality(Qt::ApplicationModal); this->setMinimumSize(500, 400); QSettings settings; this->resize(settings.value("ProfileEditor/profileWindowSize", QSize(580, 480)).toSize()); m_pumpController = pumpController; connect(m_pumpController, &PumpController::avatarStored, this, &ProfileEditor::redrawAvatar); QFont infoFont; infoFont.setPointSize(infoFont.pointSize() - 1); m_webfingerLabel = new QLabel(this); m_webfingerLabel->setFont(infoFont); m_webfingerLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); m_webfingerLabel->setToolTip("" + tr("This is your Pump address")); const QString emailExplanation = tr("This is the e-mail address associated " "with your account, for things such as " "notifications and password recovery"); m_emailChanger = new EmailChanger(emailExplanation, m_pumpController, this); m_emailLabel = new QLabel(this); m_emailLabel->setTextFormat(Qt::PlainText); m_emailLabel->setFont(infoFont); m_emailLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); m_emailLabel->setToolTip("" // Make the tooltip HTML so it gets wordwrap + emailExplanation); m_changeEmailButton = new QPushButton(QIcon::fromTheme("view-pim-mail", QIcon(":/images/button-post.png")), tr("Change &E-mail..."), this); connect(m_changeEmailButton, &QAbstractButton::clicked, m_emailChanger, &QWidget::show); m_avatarLabel = new QLabel(this); m_avatarLabel->setPixmap(QIcon::fromTheme("user-properties", QIcon(":/images/no-avatar.png")) .pixmap(96, 96)); m_changeAvatarButton = new QPushButton(QIcon::fromTheme("folder-image-people"), tr("Change &Avatar..."), this); connect(m_changeAvatarButton, &QAbstractButton::clicked, this, &ProfileEditor::findAvatarFile); m_avatarHasChanged = false; m_fullNameLineEdit = new QLineEdit(this); m_fullNameLineEdit->setToolTip("" + tr("This is your visible name")); connect(m_fullNameLineEdit, &QLineEdit::textEdited, this, &ProfileEditor::enableSaveButton); m_hometownLineEdit = new QLineEdit(this); connect(m_hometownLineEdit, &QLineEdit::textEdited, this, &ProfileEditor::enableSaveButton); m_bioTextEdit = new QTextEdit(this); m_bioTextEdit->setTabChangesFocus(true); m_bioTextEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); connect(m_bioTextEdit, &QTextEdit::textChanged, this, &ProfileEditor::enableSaveButton); m_newAvatarInfoLabel = new QLabel("\n" + tr("Changing your avatar will create a " "post in your timeline with it.\n" "If you delete that post your avatar " "will be deleted too.") + "\n", this); m_newAvatarInfoLabel->setWordWrap(true); m_newAvatarInfoLabel->setAlignment(Qt::AlignCenter); m_newAvatarInfoLabel->setFrameStyle(QFrame::Box | QFrame::Raised); m_newAvatarInfoLabel->setAutoFillBackground(true); m_newAvatarInfoLabel->setForegroundRole(QPalette::HighlightedText); m_newAvatarInfoLabel->setBackgroundRole(QPalette::Highlight); m_newAvatarInfoLabel->hide(); m_saveButton = new QPushButton(QIcon::fromTheme("document-save", QIcon(":/images/button-save.png")), tr("&Save Profile"), this); connect(m_saveButton, &QAbstractButton::clicked, this, &ProfileEditor::saveProfile); m_cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("&Cancel"), this); connect(m_cancelButton, &QAbstractButton::clicked, this, &QWidget::close); // ESC to cancel, too m_cancelAction = new QAction(this); m_cancelAction->setShortcut(QKeySequence(Qt::Key_Escape)); connect(m_cancelAction, &QAction::triggered, this, &QWidget::close); this->addAction(m_cancelAction); // Layout m_emailLayout = new QHBoxLayout(); m_emailLayout->addWidget(m_emailLabel); m_emailLayout->addStretch(1); m_emailLayout->addWidget(m_changeEmailButton); m_avatarLayout = new QHBoxLayout(); m_avatarLayout->addWidget(m_avatarLabel); m_avatarLayout->addStretch(1); m_avatarLayout->addWidget(m_changeAvatarButton); m_topLayout = new QFormLayout(); m_topLayout->setContentsMargins(0, 0, 0, 0); m_topLayout->addRow(tr("Webfinger ID"), m_webfingerLabel); m_topLayout->addRow(tr("E-mail"), m_emailLayout); m_topLayout->addRow(tr("Avatar"), m_avatarLayout); m_topLayout->addRow(tr("Full &Name"), m_fullNameLineEdit); m_topLayout->addRow(tr("&Hometown"), m_hometownLineEdit); m_topLayout->addRow(tr("&Bio"), m_bioTextEdit); // FIXME: the FormLayout doesn't let the bioTextEdit row grow taller than its sizeHint() m_bottomLayout = new QHBoxLayout(); m_bottomLayout->setAlignment(Qt::AlignRight | Qt::AlignBottom); m_bottomLayout->setContentsMargins(0, 0, 0, 0); m_bottomLayout->addWidget(m_saveButton); m_bottomLayout->addWidget(m_cancelButton); m_mainLayout = new QVBoxLayout(); m_mainLayout->addLayout(m_topLayout, 9); m_mainLayout->addWidget(m_newAvatarInfoLabel); m_mainLayout->addLayout(m_bottomLayout, 1); this->setLayout(m_mainLayout); // Disable some stuff until profile is received this->toggleWidgetsEnabled(false); m_saveButton->setEnabled(false); 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) { m_webfingerLabel->setText(m_pumpController->currentUserId()); if (eMail.isEmpty()) { m_emailLabel->setText("<" + tr("Not set", "In reference to the e-mail " "not being set for the account") + ">"); } else { m_emailLabel->setText(eMail); m_emailChanger->setCurrentEmail(eMail); } m_currentAvatarUrl = avatarUrl; QString avatarFilename = MiscHelpers::getCachedAvatarFilename(m_currentAvatarUrl); if (QFile::exists(avatarFilename)) { m_oldAvatarFilename = avatarFilename; // In case we need to restore, to cancel this->setAvatar(avatarFilename); } // If the avatar has not been downloaded yet, it will come later with a SIGNAL() m_fullNameLineEdit->setText(fullName); m_hometownLineEdit->setText(hometown); m_bioTextEdit->setText(bio); // Enable all widgets, since the profile is valid now this->toggleWidgetsEnabled(true); m_saveButton->setDisabled(true); // But disable Save button until necessary } void ProfileEditor::setAvatar(QString filename) { m_avatarLabel->setPixmap(QPixmap(filename) .scaled(96, 96, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } /* * Enable or disable some widgets depending on whether the profile has been received * */ void ProfileEditor::toggleWidgetsEnabled(bool state) { m_changeEmailButton->setEnabled(state); m_changeAvatarButton->setEnabled(state); m_fullNameLineEdit->setEnabled(state); m_hometownLineEdit->setEnabled(state); m_bioTextEdit->setEnabled(state); } /****************************************************************************/ /******************************** SLOTS *************************************/ /****************************************************************************/ void ProfileEditor::redrawAvatar(QString avatarUrl, QString avatarFilename) { if (avatarUrl == m_currentAvatarUrl) { m_oldAvatarFilename = avatarFilename; this->setAvatar(avatarFilename); } } void ProfileEditor::findAvatarFile() { m_newAvatarFilename = QFileDialog::getOpenFileName(this, tr("Select avatar image"), QDir::homePath(), tr("Image files") + " (*.png *.jpg *.jpe " "*.jpeg *.gif);;" + tr("All files") + " (*)"); if (!m_newAvatarFilename.isEmpty()) { qDebug() << "Selected" << m_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 -- TODO QPixmap avatarPixmap = QPixmap(m_newAvatarFilename); m_newAvatarContentType = MiscHelpers::getFileMimeType(m_newAvatarFilename); if (!avatarPixmap.isNull() && !m_newAvatarContentType.isEmpty()) { this->setAvatar(m_newAvatarFilename); m_avatarHasChanged = true; m_newAvatarInfoLabel->show(); this->enableSaveButton(); // Enable, since something has been changed } else { QMessageBox::warning(this, tr("Invalid image"), tr("The selected image is not valid.")); qDebug() << "Invalid avatar file selected"; } } } void ProfileEditor::saveProfile() { if (m_avatarHasChanged) { connect(m_pumpController, &PumpController::avatarUploaded, this, &ProfileEditor::sendProfileData); m_pumpController->uploadFile(m_newAvatarFilename, m_newAvatarContentType, PumpController::UploadAvatarRequest); } else { this->sendProfileData(); // Without a new image ID } } void ProfileEditor::sendProfileData(QString newImageUrl) { if (m_avatarHasChanged) { disconnect(m_pumpController, &PumpController::avatarUploaded, this, &ProfileEditor::sendProfileData); m_avatarHasChanged = false; // For next time the dialog is shown m_newAvatarInfoLabel->hide(); } QString newFullName = m_fullNameLineEdit->text().trimmed(); if (newFullName.isEmpty()) { // To avoid having empty names, use the username part from the ID newFullName = m_pumpController->currentUsername(); } m_pumpController->updateUserProfile(newImageUrl, newFullName, m_hometownLineEdit->text().trimmed(), m_bioTextEdit->toPlainText().trimmed()); this->close(); } /* * Enable the 'Save' button when something actually changes * */ void ProfileEditor::enableSaveButton() { m_saveButton->setEnabled(true); } /****************************************************************************/ /****************************** PROTECTED ***********************************/ /****************************************************************************/ void ProfileEditor::closeEvent(QCloseEvent *event) { m_saveButton->setDisabled(true); // Disabled for next time // If this is still true, it means avatar was changed but editor was cancelled if (m_avatarHasChanged) { m_avatarHasChanged = false; this->setAvatar(m_oldAvatarFilename); m_newAvatarInfoLabel->hide(); } QSettings settings; if (settings.isWritable()) { settings.setValue("ProfileEditor/profileWindowSize", this->size()); } this->hide(); event->ignore(); } dianara-v1.4.1/src/downloadwidget.cpp0000644000175000017500000002402213203413674015715 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "downloadwidget.h" DownloadWidget::DownloadWidget(QString fileUrl, QString suggestedFilename, PumpController *pumpController, QWidget *parent) : QFrame(parent) { m_pumpController = pumpController; m_fileUrl = fileUrl; m_suggestedFilename = suggestedFilename; this->setFrameStyle(QFrame::StyledPanel | QFrame::Raised); m_downloading = false; QFont infoFont; infoFont.setPointSize(infoFont.pointSize() - 1); m_infoLabel = new QLabel(this); m_infoLabel->setAlignment(Qt::AlignCenter); m_infoLabel->setFont(infoFont); m_infoLabel->hide(); m_openButton = new QPushButton(QIcon::fromTheme("document-open", QIcon(":/images/button-open.png")), tr("Open", "Verb, as in: Open the downloaded file"), this); m_openButton->setFlat(true); m_openButton->hide(); connect(m_openButton, &QAbstractButton::clicked, this, &DownloadWidget::openAttachment); m_downloadButton = new QPushButton(QIcon::fromTheme("download", QIcon(":/images/button-download.png")), tr("Download"), this); m_downloadButton->setToolTip("" + tr("Save the attached file to your folders")); m_downloadButton->setFlat(true); connect(m_downloadButton, &QAbstractButton::clicked, this, &DownloadWidget::downloadAttachment); m_progressBar = new QProgressBar(this); m_progressBar->setValue(0); m_progressBar->hide(); // Initially hidden m_cancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel", QIcon(":/images/button-cancel.png")), tr("Cancel"), this); m_cancelButton->hide(); connect(m_cancelButton, &QAbstractButton::clicked, this, &DownloadWidget::cancelDownload); m_layout = new QHBoxLayout(); m_layout->addWidget(m_infoLabel); m_layout->addWidget(m_openButton); m_layout->addWidget(m_downloadButton); m_layout->addWidget(m_progressBar); m_layout->addWidget(m_cancelButton); this->setLayout(m_layout); qDebug() << "DownloadWidget created"; } DownloadWidget::~DownloadWidget() { qDebug() << "DownloadWidget destroyed"; } void DownloadWidget::resetWidget() { m_downloadButton->show(); m_progressBar->hide(); m_openButton->setToolTip(QString()); m_openButton->hide(); m_cancelButton->hide(); m_networkReply->disconnect(); m_networkReply->deleteLater(); disconnect(m_pumpController, &PumpController::downloadCompleted, this, &DownloadWidget::completeDownload); disconnect(m_pumpController, &PumpController::downloadFailed, this, &DownloadWidget::onDownloadFailed); m_downloadedFile.close(); m_downloading = false; } //////////////////////////////////////////////////////////////////////////// ////////////////////////////////// SLOTS /////////////////////////////////// //////////////////////////////////////////////////////////////////////////// void DownloadWidget::downloadAttachment() { if (m_downloading) { qDebug() << "Already downloading!!"; return; } QString filename; filename = QFileDialog::getSaveFileName(this, tr("Save File As..."), QDir::homePath() + "/" + m_suggestedFilename, tr("All files") + " (*)"); if (!filename.isEmpty()) { m_downloading = true; m_infoLabel->setText(QStringLiteral("...")); m_infoLabel->setToolTip(QString()); m_infoLabel->show(); m_downloadButton->hide(); m_openButton->hide(); m_progressBar->show(); m_progressBar->setValue(0); m_progressBar->setToolTip(QString()); m_cancelButton->show(); m_downloadedFile.setFileName(filename); m_downloadedFile.open(QIODevice::WriteOnly); m_networkReply = this->m_pumpController->getMedia(m_fileUrl); connect(m_networkReply, &QIODevice::readyRead, this, &DownloadWidget::storeFileData); connect(m_networkReply, &QNetworkReply::downloadProgress, this, &DownloadWidget::updateProgressBar); connect(m_pumpController, &PumpController::downloadCompleted, this, &DownloadWidget::completeDownload); connect(m_pumpController, &PumpController::downloadFailed, this, &DownloadWidget::onDownloadFailed); } } void DownloadWidget::openAttachment() { /* * TODO: Open button could be visible from the start, and actually * download the file to temporary storage before opening it. * This would hide the Download button. * When download completes, a new "Save" button would appear, to * let the user save the temporary file to regular storage. */ if (m_downloadedFile.exists()) { QDesktopServices::openUrl(QUrl::fromLocalFile(m_downloadedFile.fileName())); } else { m_infoLabel->setText(tr("File not found!")); m_infoLabel->setToolTip(m_downloadedFile.fileName()); m_openButton->hide(); m_downloadButton->show(); // Chance to download again. } } void DownloadWidget::cancelDownload() { int confirmation = QMessageBox::question(this, tr("Abort download?"), tr("Do you want to stop " "downloading the attached " "file?"), tr("&Yes, stop"), tr("&No, continue"), QString(), 1, 1); if (confirmation == 0) { m_networkReply->abort(); m_infoLabel->setText(tr("Download aborted")); resetWidget(); } else { qDebug() << "Confirmation cancelled, NOT stopping download"; } } void DownloadWidget::completeDownload(QString url) { if (url == m_fileUrl) // Ensure completed download is this download { m_infoLabel->setText(tr("Download completed")); resetWidget(); // This also closes the file // FIXME: pass this through GlobalObject m_pumpController->showStatusMessageAndLogIt(tr("Attachment downloaded " "successfully to %1", "%1 = filename") .arg(m_downloadedFile.fileName())); m_openButton->setToolTip("" + tr("Open the downloaded attachment with " "your system's default program for " "this type of file.") + "

              " + m_downloadedFile.fileName()); m_openButton->show(); m_downloadButton->hide(); } } void DownloadWidget::onDownloadFailed(QString url) { if (url == m_fileUrl) // Ensure failed download is this download { m_infoLabel->setText(tr("Download failed")); m_infoLabel->setToolTip(url); qDebug() << "Download FAILED!" << url; resetWidget(); // FIXME: pass this through GlobalObject m_pumpController->showStatusMessageAndLogIt(tr("Downloading attachment " "failed: %1", "%1 = filename") .arg(this->m_downloadedFile.fileName()), url); } } void DownloadWidget::storeFileData() { QByteArray data = m_networkReply->readAll(); int httpStatus = m_networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute) .toInt(); if (httpStatus == 200) { m_downloadedFile.write(data); qDebug() << "Storing data into:" << m_downloadedFile.fileName(); qDebug() << data.length() << "bytes"; } else { qDebug() << "Not storing data into:" << m_downloadedFile.fileName(); qDebug() << "Because HTTP status code is:" << httpStatus; } } void DownloadWidget::updateProgressBar(qint64 received, qint64 total) { m_progressBar->setRange(0, total); m_progressBar->setValue(received); m_infoLabel->setText(tr("Downloading %1 KiB...") .arg(QLocale::system().toString(total / 1024))); QString downloadedTooltip = tr("%1 KiB downloaded") .arg(QLocale::system().toString(received / 1024)); m_infoLabel->setToolTip(downloadedTooltip); m_progressBar->setToolTip(downloadedTooltip); } dianara-v1.4.1/src/dbusinterface.cpp0000664000175000017500000000334613032056650015525 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 "dbusinterface.h" DBusInterface::DBusInterface(QObject *parent) : QObject(parent) { qDebug() << "DBusInterface created"; } DBusInterface::~DBusInterface() { qDebug() << "DBusInterface destroyed"; } /****************************************************************************/ /******************************** SLOTS *************************************/ /****************************************************************************/ void DBusInterface::toggle() { qDebug() << "DBusInterface::toggle()"; QMetaObject::invokeMethod(parent(), "toggleMainWindow"); } void DBusInterface::post(QString title, QString content) { qDebug() << "DBusInterface::post(); " << "Title:" << title << "; " << "Text:" << content; QMetaObject::invokeMethod(parent(), "startPost", Q_ARG(QString, title), Q_ARG(QString, content)); } dianara-v1.4.1/src/dbusinterface.h0000664000175000017500000000251013032056646015167 0ustar janjan/* * This file is part of Dianara * Copyright 2012-2017 JanKusanagi JRR * * 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 DBUSINTERFACE_H #define DBUSINTERFACE_H #include #include #include class DBusInterface : public QObject { Q_OBJECT // Optional, but makes the D-Bus exported methods look nicer Q_CLASSINFO("D-Bus Interface", "org.nongnu.dianara") public: explicit DBusInterface(QObject *parent = 0); ~DBusInterface(); signals: public slots: void toggle(); void post(QString title, QString content); }; #endif // DBUSINTERFACE_H dianara-v1.4.1/translations/0000755000175000017500000000000013221211040014107 5ustar janjandianara-v1.4.1/translations/dianara_es.ts0000644000175000017500000067341213212034133016570 0ustar janjan ASActivity Public Público %1 by %2 1=kind of object: note, comment, etc; 2=author's name %1 de %2 ASObject Note Noun, an object type Nota Article Noun, an object type Artículo Image Noun, an object type Imagen Audio Noun, an object type Audio Video Noun, an object type Vídeo File Noun, an object type Archivo Comment Noun, as in object type: a comment Comentario Group Noun, an object type Grupo Collection Noun, an object type Colección Other As in: other type of post Otro No detailed location No hay ubicación detallada Deleted on %1 Eliminado el %1 and one other y uno más and %1 others y %1 más ASPerson Hometown Ciudad AccountDialog Your Pump.io address: Tu dirección pump.io: Get &Verifier Code Obtener código de &verificación Verifier code: Código de verificación: Enter or paste the verifier code provided by your Pump server here Introduce o pega aquí el codigo de verificación proporcionado por tu servidor Pump &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 If you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. 1=link to website Si aún no tienes una cuenta, puedes registrar una en %1. Este enlace te llevará a un servidor público aleatorio. If you need help: %1 Si necesitas ayuda: %1 Pump.io User Guide Guía de usuario de Pump.io Your address, like username@pumpserver.org Tu dirección, como usuario@servidorpump.org 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. &Authorize Application &Autorizar la aplicación &Cancel &Cancelar Your account is properly configured. Tu cuenta está configurada correctamente. Press Unlock if you wish to configure a different account. Pulsa Desbloquear si quieres configurar una cuenta diferente. &Unlock &Desbloquear 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 Unable to open web browser! ¡No se ha podido abrir el navegador web! AudienceSelector 'To' List Lista 'Para' 'Cc' List Lista 'Cc' &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. ON THE LEFT should change to ON THE RIGHT in RTL languages 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 Public Público Followers Seguidores Lists Listas People... Personas... Selected People Gente seleccionada AvatarButton Open %1's profile in web browser Abrir el perfil de %1 en el navegador web Open your profile in web browser Abrir tu perfil en el navegador web Send message to %1 Enviar mensaje a %1 Browse messages Ver mensajes Stop following Dejar de seguir Follow 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 BannerNotification Timelines were not automatically updated to avoid interruptions. No se han actualizado las líneas temporales automáticamente para evitar interrupciones. This happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you read Esto pasa cuando llega el momento de autoactualizar las líneas temporales, pero no estás al principio de la primera página, para evitar interrupciones mientras lees Update now Actualizar ahora Hide this message Ocultar este mensaje ColorPicker Change... Cambiar... Choose a color Elige un color Comment Posted on %1 Publicado el %1 Modified on %1 Modificado el %1 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 Edit Editar Modify this comment Modificar este comentario Delete Eliminar Erase this comment Borrar 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í, eliminarlo &No &No CommenterBlock You can press Control+Enter to send the comment with the keyboard Puedes pulsar Control+Enter para enviar el comentario con el teclado Reload comments Actualizar comentarios Comment Infinitive verb Comentar Cancel Cancelar Press ESC to cancel the comment if there is no text Pulsa ESC para cancelar el comentario si no hay texto Check for comments Comprobar comentarios Show all %1 comments Mostrar los %1 comentarios Comments are not available Los comentarios no están disponibles Error: Already composing Error: Ya se está redactando You can't edit a comment at this time, because another comment is already being composed. No puedes editar un comentario en este momento, porque ya se está redactando otro comentario. Editing comment Editando comentario Loading comments... Cargando comentarios... Posting comment failed. Try again. Ha fallado la publicación del comentario. Prueba de nuevo. An error occurred Ha ocurrido un error Sending comment... Enviando comentario... Updating comment... Actualizando 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 Cabecera List Lista Table Tabla 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 You can attach only one file. Solo puedes adjuntar un archivo. You cannot drop folders here, only a single file. No puedes soltar carpetas aquí, solo un único archivo. Insert as image? ¿Insertar como imagen? The link you are pasting seems to point to an image. Parece que el enlace que estás pegando apunta a una imagen. Insert as visible image Insertar como imagen visible Insert as link Insertar como enlace Table Size Tamaño de la tabla How many rows (height)? ¿Cuántas filas (altura)? How many columns (width)? ¿Cuántas columnas (ancho)? Type or paste a web address here. You could also select some text first, to turn it into a link. Escribe o pega una dirección web aquí. También puedes seleccionar texto antes, para convertirlo en un enlace. Invalid link Enlace no válido The text you entered does not look like a link. El texto que has introducido no parece un enlace. It should start with one of these types: It = the link, from previous sentence Debería comenzar con uno de estos tipos: &Use it anyway &Usarlo igualmente &Enter it again &Introducirlo de nuevo &Cancel link &Cancelar el enlace Type or paste the image address here. The link must point to the image file directly. Escribe o pega la dirección de la imagen aquí. El enlace ha de apuntar al archivo de imagen directamente. Yes, but saving a &draft Sí, pero guardando un &borrador 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 imágenes 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í 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. ConfigDialog minutes minutos Top Parte superior Bottom Parte inferior Program Configuration Configuración del programa Timeline &update interval Intervalo de &actualización de la línea temporal &Tabs position Posición de las &pestañas &Movable tabs Pes&tañas movibles Minor Feeds Líneas temporales menores 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 de manera &predefinida Pro&xy Settings Ajustes de pro&xy Network configuration Configuración de red Set Up F&ilters Configurar f&iltros Filtering rules Normas de filtrado Highlighted activities, except mine Actividades destacadas, excepto las mías Any highlighted activity Cualquier actividad destacada Always Siempre Never Nunca characters This is a suffix, after a number caracteres Snippet limit Límite de los fragmentos Post Titles Títulos de los mensajes Post Contents Contenido de los mensajes Comments Comentarios You are among the recipients of the activity, such as a comment addressed to you. Estás entre los destinatarios de la actividad, como por ejemplo un comentario dirigido a ti. Used also when highlighting posts addressed to you in the timelines. Usado también cuando se destacan mensajes dirigidos a ti en las líneas temporales. The activity is in reply to something done by you, such as a comment posted in reply to one of your notes. La actividad es en respuesta a alguna cosa hecha por ti, como un comentario publicado en respuesta a una de tus notas. You are the object of the activity, such as someone adding you to a list. Eres el objeto de la actividad, como por ejemplo cuando alguien te añade a una lista. The activity is related to one of your objects, such as someone liking one of your posts. La actividad está relacionada con uno de tus objetos, como por ejemplo cuando a alguien le gusta uno de tus mensajes. Used also when highlighting your own posts in the timelines. Usado también cuando se destacan tus propios mensajes en las líneas temporales. Item highlighted due to filtering rules. Elemento destacado por normas de filtrado. Item is new. El elemento es nuevo. Show snippets in minor feeds Mostrar fragmentos en las líneas temporales menores Hide duplicated posts Ocultar mensajes duplicados No No Before avatar Antes del avatar Before avatar, subtle Antes del avatar, sutil After avatar Después del avatar After avatar, subtle Después del avatar, sutil Snippet limit when highlighted Límite de los fragmentos destacados Show activity icons Mostrar iconos de actividad Avatar size Tamaño de los avatares Show extended share information Mostrar información adicional en los compartidos Show extra information Mostrar información extra Highlight post author's comments Destacar comentarios del autor del mensaje Highlight your own comments Destacar tus propios comentarios Ignore SSL errors in images Ignorar errores de SSL en las imágenes Show character counter Mostrar contador de caracteres Don't inform followers when following someone No informar a los seguidores al seguir a alguien Don't inform followers when handling lists No informar a los seguidores al manipular listas As system notifications Como notificaciones del sistema Using own notifications Usando notificaciones propias Don't show notifications No mostrar notificaciones seconds Next to a duration, in seconds segundos Notification Style Estilo de las notificaciones Duration Duración Persistent Notifications Notificaciones persistentes Also highlight taskbar entry Destacar también en la barra de tareas Notify when receiving: Notificar cuando se reciban: New posts Mensajes nuevos Highlighted posts Mensajes destacados New activities in minor feed Nuevas actividades en la línea temporal menor Highlighted activities in minor feed Actividades destacadas en la línea temporal menor Important errors Errores importantes Default el icono Predefinido System iconset, if available Icono del sistema, si está disponible Show your current avatar Mostrar tu avatar actual Custom icon Icono personalizado System Tray Icon &Type &Tipo de icono en la bandeja del sistema Hide window on startup Ocultar ventana al iniciar General Options Opciones generales Fonts Tipos de letra Colors Colores Timelines Líneas temporales Posts Mensajes Composer Editor Privacy Privacidad Notifications Notificaciones System Tray Bandeja del sistema Minor feed avatar sizes Tamaño de avatares en líneas temporales menores Avatar size in comments Tamaño de los avatares en comentarios S&elect... S&eleccionar... Custom &Icon &Icono personalizado Select custom icon Selecciona icono personalizado Dianara stores data in this folder: Dianara guarda datos en esta carpeta: Left side tabs on left side/west; RTL not affected Lado izquierdo Right side tabs on right side/east; RTL not affected Lado derecho Show information for deleted posts Mostrar información de los mensajes eliminados Jump to new posts line on update Saltar a la línea de nuevos mensajes al actualizar Only for images inserted from web sites. Sólo para imágenes insertadas desde sitios web. Use with care. Usar con cuidado. Show full size images Mostrar imágenes a tamaño completo Use attachment filename as initial post title Usar nombre del adjunto como título inicial del mensaje Inform only the author when liking things Informar sólo al autor de los "Me gusta" &Save Configuration &Guardar configuración &Cancel &Cancelar This is a system notification Esto es una notificación del sistema System notifications are not available! ¡Las notificaciones del sistema no están disponibles! Own notifications will be used. Se usarán las notificaciones propias. This is a basic notification Esto es una notificación básica 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. ContactCard Hometown Ciudad Joined: %1 Se unió: %1 Updated: %1 Actualizado: %1 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 Send Message Enviar mensaje Browse Messages Ver mensajes 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 Type a partial name or ID to find a contact... Escribe una parte de un nombre o ID para encontrar un contacto... F&ull List Lista c&ompleta ContactManager 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: &Follow &Seguir Reload Followers Actualizar Seguidores Reload Following Actualizar Siguiendo Export Followers Exportar Seguidores Export Following Exportar Siguiendo Reload Lists Actualizar listas &Neighbors V&ecinos Cannot export to this file: No se puede exportar a este archivo: Please enter another file name, or choose a different folder. Por favor, introduce otro nombre de archivo, o elige otra carpeta. The server seems to be a Pump server, but the account does not exist. Parece que el servidor es un servidor Pump, pero la cuenta no existe. %1 doesn't seem to be a Pump server. %1 is a hostname No parece que %1 sea un servidor Pump. Following this account at this time will probably not work. Es probable que seguir esta cuenta en este momento no funcione. The %1 server seems unavailable. %1 is a hostname El servidor %1 no parece disponible. Unknown Refers to server version Desconocida Error Error The user address %1 does not exist, or the %2 server is down. La dirección de usuario %1 no existe, o el servidor %2 no está funcionando. Server version Versión del servidor Check the address, and keep in mind that usernames are case-sensitive. Comprueba la dirección, y ten en cuenta que los nombres de usuario diferencian entre mayúsculas y minúsculas. Do you want to try following this address anyway? ¿Quieres intentar seguir esta dirección igualmente? (not recommended) (no recomendado) Yes, follow anyway Si, seguirla igualmente No, cancel No, cancelar About to follow %1... A punto de seguir a %1... Follo&wers Se&guidores Followin&g Siguien&do &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 DownloadWidget Open Verb, as in: Open the downloaded file Abrir Download Descargar Save the attached file to your folders Guardar el archivo adjunto en tus carpetas Cancel Cancelar Save File As... Guardar archivo como... All files Todos los archivos File not found! ¡No se ha encontrado el archivo! Abort download? ¿Interrumpir la descarga? Do you want to stop downloading the attached file? ¿Quieres detener la descarga del archivo adjunto? &Yes, stop &Sí, detener &No, continue &No, continuar Download aborted Descarga interrumpida Download completed Descarga completada Attachment downloaded successfully to %1 %1 = filename Adjunto descargado correctamente como %1 Open the downloaded attachment with your system's default program for this type of file. Abrir el adjunto descargado con el programa predefinido de tu sistema para este tipo de archivo. Download failed Ha fallado la descarga Downloading attachment failed: %1 %1 = filename Ha fallado la descarga del adjunto: %1 Downloading %1 KiB... Descargando %1 KiB... %1 KiB downloaded hmmm FIXME %1 KiB descargados DraftsManager Draft Manager Gestor de borradores Load Cargar Save Guardar Manage drafts... Gestionar borradores... &Delete selected draft &Eliminar borrador seleccionado &Close &Cerrar Untitled draft Borrador sin título Delete draft? ¿Eliminar borrador? Are you sure you want to delete this draft? ¿Estás seguro de que quieres eliminar este borrador? &Yes, delete it &Sí, eliminarlo &No &No EmailChanger Change E-mail Address Cambiar dirección de e-mail Change Cambiar &Cancel &Cancelar E-mail Address: Dirección de e-mail: Again: Otra vez: Your Password: Tu contraseña: E-mail addresses don't match! ¡Las direcciones de e-mail no coinciden! Password is empty! ¡La contraseña está vacía! FDNotifications Show Mostrar FilterEditor Filter Editor Editor de filtros Application Aplicación %1 if %2 contains: %3 This explains a filter rule, like: Hide if Author ID contains JohnDoe %1 si %2 contiene: %3 Here you can set some rules for hiding or highlighting stuff. 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. You could also highlight messages that contain your name. Aquí puedes establecer algunas normas para ocultar o destacar cosas. 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. También podrías destacar mensajes que contienen tu nombre. Hide Ocultar Highlight Destacar Post Contents Contenido de la publicación Author ID ID del autor Activity Description Descripción de la actividad Keywords... Palabras clave... &Add Filter &Añadir filtro Filters in use Filtros en uso &Remove Selected Filter &Quitar filtro seleccionado &Save Filters &Guardar filtros &Cancel &Cancelar if si contains contiene &New Filter &Nuevo filtro C&urrent Filters Filtros &actuales FilterMatchesWidget Content The contents of the post matched Contenido Author Autor App Application, short if possible Aplicación Description Descripción FirstRunWizard Welcome Wizard Asistente de bienvenida Welcome to Dianara! ¡Bienvenido a Dianara! This wizard will help you get started. Este asistente te ayudará a empezar. You can access this window again at any time from the Help menu. Puedes volver a acceder a esta ventana en cualquier momento desde el menú de Ayuda. The first step is setting up your account, by using the following button: El primer paso es configurar tu cuenta, usando el siguiente botón: Configure your &account Configur&a tu cuenta Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already. Una vez que hayas configurado tu cuenta, es recomendable que edites tu perfil y añadas un avatar y algo más de información, si no lo has hecho ya. &Edit your profile &Edita tu perfil By default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes. De manera predefinida, Dianara publicará sólo para tus seguidores, pero es recomendable que publiques para Público, al menos a veces. Post to &Public by default Publicar para &Público de manera predefinida Open general program &help window Abrir la ventana de ay&uda general del programa &Show this again next time Dianara starts &Mostrar de nuevo la próxima vez que se inicie Dianara &Close &Cerrar FontPicker Change... Cambiar... Choose a font Elige un tipo de letra HelpWidget Basic Help Ayuda básica Getting started Empezando The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. La primera vez que inicies Dianara, deberías ver la ventana de Configuración de la cuenta. Allí, introduce tu dirección de Pump.io como nombre@servidor y pulsa el botón Obtener código de verificación. Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. A continuación, tu navegador web habitual debería cargar la página de autorización en tu servidor Pump.io. Allí, tendrás que copiar el código 'VERIFIER' completo, y pegarlo en el segundo campo de Dianara. Entonces pulsa 'Autorizar la aplicación', y una vez que se confirme, pulsa 'Guardar datos'. At this point, your profile, contact lists and timelines will be loaded. En este momento, se cargará tu perfil, las listas de contactos y las líneas temporales. You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. Deberías echar un vistazo a la ventana 'Configuración del programa', en el menú Configuración - Configurar Dianara. Allí encontrarás varias opciones interesantes. Settings Configuración You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. Puedes ajustar varias cosas a tu gusto en la configuración, como el intervalo de tiempo entre actualizaciones de la línea temporal, cuantos mensajes por página quieres, colores para destacar mensajes, notificaciones o qué aspecto tendrá el icono de la bandeja del sistema. Timelines Líneas temporales Contents Índice Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. Recuerda que hay muchos lugares en Dianara donde puedes obtener más información manteniendo el ratón sobre algún texto o botón, y esperando a que aparezca la descripción emergente (tooltip). Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. Aquí, también puedes activar la opción para publicar siempre tus mensajes como públicos de forma predefinida. Siempre puedes cambiar esto en el momento de publicar. The main timeline, where you'll see all the stuff posted or shared by the people you follow. La línea temporal principal, donde verás todo lo que ha publicado o compartido la gente a la que sigues. Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. Línea temporal de mensajes, donde verás mensajes enviados a ti específicamente. Estos mensajes pueden haber sido enviados también a otras personas. Activity timeline, where you'll see your own posts, or posts shared by you. Línea temporal de actividad, donde verás tus propios mensajes, o mensajes que has compartido. Favorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. Línea temporal de favoritos, donde verás los mensajes que te han gustado. Esto puede usarse como un sistema de marcadores. These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. Estas actividades pueden tener un botón "+". Púlsalo para abrir el mensaje al que hacen referencia. Además, como en muchos otros lugares, puedes mantener el ratón encima para ver información relevante en la descripción emergente (tooltip). Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. Dentro de la pestaña 'Vecinos' verás algunos recursos para encontrar gente, y tendrás la opción de ver los últimos usuarios registrados en tu servidor, directamente. Dianara offers a D-Bus interface that allows some control from other applications. Dianara ofrece una interfaz D-Bus que permite un cierto control desde otras aplicaciones. The interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. La interfaz se encuentra en %1, y puedes acceder a ella con herramientas como %2 o %3. Ofrece métodos como %4 y %5. Posting Publicando If you're new to Pump.io, take a look at this guide: Si es la primera vez que usas Pump.io, echa un vistazo a esta guía: New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. Los mensajes nuevos aparecen destacados en un color diferente. Puedes marcarlos como leídos haciendo clic en cualquier parte vacía del mensaje. You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. Puedes publicar notas haciendo clic en el campo de texto de la parte superior de la ventana o pulsando Control+N. Añadir un título al mensaje es opcional, pero muy recomendable, ya que ayudará a identificar mejor las referencias a tu mensaje en la línea temporal menor, notificaciones por e-mail, etc. It is possible to attach images, audio, video, and general files, like PDF documents, to your post. Es posible adjuntar imágenes, audio, video y archivos generales, como documentos PDF, a tu mensaje. You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. Puedes usar el botón Formato para añadir formato al texto, como negrita o cursiva. Algunas de estas opciones requieren que se seleccione el texto antes de usarlas. If you add a specific person to the 'To' list, they will receive your message in their direct messages tab. Si añades una persona específica a la lista 'Para', recibirá tu mensaje en su pestaña de mensajes directos. You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. Puedes hacer mensajes privados añadiendo gente específica a estas listas, y desmarcando las opciones Público y Seguidores. Managing contacts Gestionando los contactos You can see the lists of people you follow, and who follow you from the Contacts tab. Puedes ver las listas de gente a la que sigues, y que te sigue, en la pestaña Contactos. There, you can also manage person lists, used mainly to send posts to specific groups of people. Allí puedes gestionar también las listas de personas, que se utilizan principalmente para enviar mensajes a grupos de gente específicos. You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. Puedes hacer clic en cualquier avatar en los mensajes, los comentarios, y la columna "Mientras tanto", y verás un menú con varias opciones, una de las cuales es seguir o dejar de seguir a esta persona. Keyboard controls Control por teclado Pump.io User Guide Guía de usuario de Pump.io There are seven timelines: Hay siete líneas temporales: The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). La sexta y séptima líneas temporales también son líneas temporales menores, parecidas al "Mientras tanto", pero contienen únicamente actividades dirigidas a ti (Menciones) y actividades hechas por ti (Acciones). You can select who will see your post by using the To and Cc buttons. Puedes seleccionar quien verá tu mensaje usando los botones 'Para' y 'Cc'. You can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. También puedes teclear '@' y los primeros caracteres del nombre de un contacto para mostrar un menú emergente con opciones que coincidan. You can also send a direct message (initially private) to that contact from this menu. También puedes enviar un mensaje directo (inicialmente privado) a este contacto desde este menú. You can find a list with some Pump.io users and other information here: Puedes encontrar una lista con algunos usuarios de Pump.io y otros datos aquí: Users by language Usuarios por idioma Followers of Pump.io Community account Seguidores de la cuenta Pump.io Community The most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. Las acciones más comunes que se encuentran en los menús tienen atajos de teclado escritos al lado, como F5 o Control+N. Besides that, you can use: Aparte de eso, puedes usar: Control+Up/Down/PgUp/PgDown/Home/End to move around the timeline. Control+Arriba/Abajo/RePag/AvPag/Inicio/Fin para moverte por la línea temporal. Control+Left/Right to jump one page in the timeline. Control+Izquierda/Derecha para pasar una página en la línea temporal. Control+G to go to any page in the timeline directly. Control+G para ir a cualquier página de la línea temporal directamente. Control+1/2/3 to switch between the minor feeds. Control+1/2/3 para cambiar entre las líneas temporales menores. Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. Control+Enter para publicar, cuando hayas acabado de redactar una nota o un comentario. Si la nota está vacía, la puedes cancelar pulsando ESC. Command line options Opciones de línea de comandos The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. LEFT SIDE should change to RIGHT SIDE on RTL languages La quinta línea temporal es la línea temporal menor, también conocida como el "Mientras tanto". Es visible en el lado izquierdo, aunque se puede esconder. Aquí verás actividades secundarias hechas por todo el mundo, como acciones de comentar, marcar mensajes con "Me gusta" o seguir a gente. Choose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. Elige una con las teclas de cursor y pulsa Enter para completar el nombre. Esto añadirá a esta persona a la lista de destinatarios. There is a text field at the top, where you can directly enter addresses of new contacts to follow them. Hay un campo de texto en la parte superior, donde puedes introducir directamente direcciones de contactos nuevos para seguirles. While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. Mientras redactas una nota, pulsa Enter para pasar del título al cuerpo del mensaje. Además, pulsando la flecha arriba cuando te encuentres al principio del mensaje, vuelve al título. Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. Control+Enter para acabar de crear una lista de destinatarios para un mensaje, en las listas 'Para' o 'Cc'. You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. Puedes usar el parámetro --config para ejecutar el programa con una configuración diferente. Esto puede ser útil para usar dos o más cuentas diferentes. Incluso puedes ejecutar dos instancias de Dianara a la vez. Use the --debug parameter to have extra information in your terminal window, about what the program is doing. Usa el parámetro --debug para tener información adicional en la ventana de la terminal, sobre lo que está haciendo el programa. If your server does not support HTTPS, you can use the --nohttps parameter. Si tu servidor no tiene soporte HTTPS, puedes utilizar el parámetro --nohttps. If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. Si usas una configuración alternativa, con algo como '--config otraconf', entonces la interfaz estará en org.nongnu.dianara_otraconf. &Close &Cerrar ImageViewer Image Imagen Untitled Sin título &Save As... &Guardar como... &Restart Restart animation &Reiniciar Fit As in: fit image to window Ajustar Full As in: see image in full size Completo Rotate image to the left RTL: This actually means LEFT, anticlockwise Girar la imagen a la izquierda Rotate image to the right RTL: This actually means RIGHT, clockwise Girar la imagen a la derecha &Close &Cerrar Save Image... Guardar imagen... Close Viewer Cerrar visor Downloading full image... Descargando imagen completa... Error downloading image! Error descargando la imagen! Try again later. Vuelve a intentarlo más tarde. 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 &Quitar 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í 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í WARNING: Delete list? ADVERTENCIA: ¿Eliminar lista? &Yes, delete it &Sí, eliminarla &No &No LogViewer Log Registro Clear &Log Borrar el &registro &Close &Cerrar MainWindow &Messages &Mensajes &Contacts &Contactos &Quit &Salir &Session &Sesión 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. Dianara started. Dianara iniciado. Minor activities done by everyone, such as replying to posts Actividades secundarias hechas por todo el mundo, tales como respuestas a mensajes Minor activities addressed to you Actividades secundarias dirigidas a ti Minor activities done by you Actividades secundarias hechas por ti The people you follow, the ones who follow you, and your person lists La gente a la que sigues, la que te sigue, y tus listas de personas Running with Qt v%1. Funcionando con Qt v%1. Auto-update &Timelines Auto-actualizar líneas &temporales Mark All as Read Marcar todo como leído &Post a Note &Publicar una nota &View &Ver Locked Panels and Toolbars Paneles y barras de herramientas bloqueados Side Panel Panel lateral &Toolbar &Barra de herramientas Full &Screen &Pantalla completa &Log &Registro S&ettings &Configuración Edit &Profile Editar &perfil &Account &Cuenta &Configure Dianara &Configurar Dianara &Help Ay&uda Basic &Help &Ayuda básica Visit &Website Visitar sitio &web Report a &Bug Informar de un &fallo Pump.io User &Guide &Guía de usuario de Pump.io List of Some Pump.io &Users Lista de algunos &usuarios de Pump.io Pump.io &Network Status Website Web del estado de la &red Pump.io About &Dianara Acerca de &Dianara Toolbar Barra de herramientas Open the log viewer Abrir el visor del registro Auto-updating enabled Auto-actualizaciones activadas Auto-updating disabled Auto-actualizaciones desactivadas Proxy password required Contraseña de proxy necesaria You have configured a proxy server with authentication, but the password is not set. Has configurado un servidor proxy con autenticación, pero la contraseña no está definida. Enter the password for your proxy server: Introduce la contraseña para tu servidor proxy: Your biography is empty Tu biografía está vacía Click to edit your profile Haz clic para editar tu perfil Starting automatic update of timelines, once every %1 minutes. Iniciando actualización automática de líneas temporales, una vez cada %1 minutos. Stopping automatic update of timelines. Deteniendo actualización automática de líneas temporales. Received %1 older posts in '%2'. %1 is a number, %2 = name of a timeline Se han recibido %1 mensajes anteriores en '%2'. 1 highlighted singular, refers to a post 1 destacado %1 highlighted plural, refers to posts %1 destacados Direct messages Mensajes directos By filters Por filtros 1 more pending to receive. singular, one post 1 más pendiente de recibir. %1 more pending to receive. plural, several posts %1 más pendientes de recibir. Also: Además: Last update: %1 Última actualización: %1 1 more pending to receive. singular, 1 activity 1 más pendiente de recibir. %1 more pending to receive. plural, several activities %1 más pendientes de recibir. '%1' updated. %1 is the name of a feed '%1' actualizada. 1 filtered out. singular, refers to a post 1 filtrado. %1 filtered out. plural, refers to posts %1 filtrados. 1 deleted. singular, refers to a post 1 eliminado. %1 deleted. plural, refers to posts %1 eliminados. No new posts. No hay mensajes nuevos. There is 1 new activity. Hay 1 actividad nueva. There are %1 new activities. Hay %1 actividades nuevas. 1 highlighted. singular, refers to an activity 1 destacada. %1 highlighted. plural, refers to activities %1 destacadas. 1 filtered out. singular, refers to one activity 1 filtrada. %1 filtered out. plural, several activities %1 filtradas. No new activities. No hay actividades nuevas. Error storing image! ¡Error almacenando la imagen! %1 bytes %1 bytes Link to: %1 Enlace a: %1 Marking everything as read... Marcando todo como leído... With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. Con Dianara puedes ver tus líneas temporales, crear nuevos mensajes, subir fotos y multimedia, interactuar con los mensajes, gestionar tus contactos y seguir gente nueva. English translation by JanKusanagi. TRANSLATORS: Change this with your language and name. If there was another translator before you, add your name after theirs ;) Traducción al castellano por JanKusanagi. Dianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. Dianara es software libre, licenciado bajo la licencia GNU GPL, y utiliza algunos iconos Oxygen con licencia LGPL. &Hide Window &Ocultar ventana &Show Window &Mostrar ventana Closing due to environment shutting down... meh... Cerrando debido a que el entorno se está apagando... Quit? ¿Salir? You are composing a note or a comment. Estás redactando una nota o un comentario. Do you really want to close Dianara? ¿Realmente quieres cerrar Dianara? &Yes, close the program &Sí, cerrar el programa &No &No Shutting down Dianara... Cerrando Dianara... System tray icon is not available. El icono de la bandeja del sistema no está disponible. Dianara cannot be hidden in the system tray. Dianara no se puede ocultar en la bandeja del sistema. Do you want to close the program completely? ¿Quieres cerrar el programa completamente? There is 1 new post. Hay 1 mensaje nuevo. There are %1 new posts. Hay %1 mensajes nuevos. Timeline updated at %1. Línea temporal actualizada a las %1. Total posts: %1 Total de mensajes: %1 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. 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! The main timeline La línea temporal principal Press F1 for help Pulsa F1 para ver la ayuda Click here to configure your account Haz clic aquí para configurar tu cuenta Update %1 Actualizar %1 Show Welcome Wizard Mostrar el asistente de bienvenida 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 &Filters and Highlighting hmmm &Filtros y destacados Some Pump.io &Tips Algunos &consejos sobre Pump.io Favor&ites &Favoritos Received %1 older activities in '%2'. %1 is a number, %2 = name of feed Se han recibido %1 actividades anteriores en '%2'. Minor feed updated at %1. Línea temporal menor actualizada a las %1. About Dianara Acerca de Dianara MinorFeed Older Activities Actividades anteriores Get previous minor activities Obtener actividades secundarias anteriores There are no activities to show yet. Aún no hay actividades para mostrar. Get %1 newer As in: Get 3 newer (activities) Recibir %1 más nuevas MinorFeedItem Using %1 Application used to generate this activity Usando %1 To: %1 1=people to whom this activity was sent Para: %1 Cc: %1 1=people to whom this activity was sent as CC Cc: %1 Open referenced post Abrir el mensaje referenciado MiscHelpers bytes bytes Error: Unable to launch browser Error: No se ha podido abrir el navegador The default system web browser could not be executed. No se ha podido ejecutar el navegador web predefinido del sistema. You might need to install the XDG utilities. Puede que necesites instalar las utilidades XDG. PageSelector Jump to page Saltar a página Page number: Número de página: &First As in: first page &Primera &Last As in: last page Últim&a Newer As in: newer pages Más nuevas Older As in: older pages Más antiguas &Go &Ir &Cancel &Cancelar 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 Like Me gusta Shared on %1 Compartido el %1 &Close &Cerrar In En To Para Using %1 1=Program used for posting or sharing Usando %1 Parent As in 'Open the parent post'. Try to use the shortest word! Padre Open the parent post, to which this one replies Abrir el mensaje padre, al que éste responde Modify this post Modificar este mensaje Join Group Unirse al grupo %1 members in the group %1 miembros en el grupo 1 like Le gusta a 1 1 comment 1 comentario Shared %1 times Compartido %1 veces Share Compartir Click to download the attachment Haz clic para descargar el archivo adjunto Post Noun, not verb Mensaje Type As in: type of object Tipo Modified on %1 Modificado el %1 Edit Editar Image is animated. Click on it to play. La imagen es animada. Haz clic para reproducirla. Loading image... Cargando imagen... %1 likes Les gusta a %1 %1 comments %1 comentarios Delete Eliminar Via %1 Meh... A través de %1 Edited: %1 Editado: %1 Posted on %1 1=Date Publicado el %1 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 Open post in web browser Abrir el mensaje en el navegador web Cc Cc Copy post link to clipboard Copiar el enlace del mensaje al portapapeles Normalize text colors Normalizar colores del texto Comment verb, for the comment button Comentar Reply to this post. Responder a este mensaje. Share this post with your contacts Compartir este mensaje con tus contactos Erase this post Borrar este mensaje Size Image size (resolution) Tamaño Couldn't load image! ¡No se ha podido cargar la imagen! Attached Audio Audio adjunto Attached Video Vídeo adjunto Attached File Archivo adjunto %1 likes this One person Le gusta a %1 %1 like this More than one person Les gusta a %1 %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 Are you sure you want to share your own post? ¿Estás seguro de que quieres compartir tu propio mensaje? 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? Are you sure you want to delete this post? ¿Estás seguro de que quieres eliminar este mensaje? &Yes, delete it &Sí, eliminarlo 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 &E-mail... Cambiar &e-mail... Change &Avatar... Cambiar &avatar... This is your visible name Este es tu nombre visible Changing your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. Cambiar tu avatar creará una publicación en tu línea temporal con él. Si borras esta publicación, tu avatar se borrará también. &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. ProxyDialog Proxy Configuration Configuración de proxy Do not use a proxy No utilizar un proxy Your proxy username Tu nombre de usuario para el proxy Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. Nota: La contraseña no se guarda de forma segura. Si lo deseas, puedes dejar el campo vacío, y se te pedirá la contraseña al iniciar. &Save &Guardar &Cancel &Cancelar Proxy &Type &Tipo de proxy &Hostname &Servidor &Port &Puerto Use &Authentication Usar &autenticación &User &Usuario Pass&word Con&traseña Publisher Public Público Followers Seguidores Picture Foto Audio Audio Video Vídeo Ad&d... &Añadir... Upload media, like pictures or videos Subir multimedia, como fotos o vídeos Select Picture... Seleccionar foto... Title Título 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... Drafts Borradores Lists Listas Select who will get a copy of this post Selecciona quien recibirá una copia de este mensaje Other as in other kinds of files Otros Cancel Cancelar Cancel the post Cancelar el mensaje Picture not set Foto no seleccionada Select Audio File... Seleccionar archivo de audio... Find the audio file in your folders Encontrar el archivo de audio en tus carpetas Audio file not set Archivo de audio no seleccionado Select Video... Seleccionar video... Find the video in your folders Encontrar el vídeo en tus carpetas Video not set Vídeo no seleccionado Select File... Seleccionar archivo... Find the file in your folders Encontrar el archivo en tus carpetas File not set Archivo no seleccionado 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 You can't create a message for %1 at this time, because a post is already being composed. No puedes crear un mensaje para %1 en este momento, porque ya se está redactando un mensaje. Draft loaded. Se ha cargado un borrador. ERROR: Already composing ERROR: Ya se está redactando You can't load a draft at this time, because a post is already being composed. No puedes cargar un borrador en este momento, porque ya se está redactando un mensaje. Draft saved. Borrador guardado. Posting failed. Try again. Ha fallado la publicación. Prueba de nuevo. Warning: You have no followers yet Advertencia: Aún no tienes seguidores You're trying to post to your followers only, but you don't have any followers yet. Estás intentando publicar sólo para tus seguidores, pero no tienes ningún seguidor todavía. If you post like this, no one will be able to see your message. Si publicas de esta manera, nadie podrá ver tu mensaje. &Cancel, go back to the post &Cancelar, volver al mensaje Updating... Actualizando... Post is empty. El mensaje está vacío. File not selected. No se ha seleccionado un archivo. Select one image Selecciona una imagen Image files Archivos de imagen Select one file Selecciona un archivo Invalid file Archivo no válido The file type cannot be detected. El tipo de archivo no se puede detectar. All files Todos los archivos Since you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. Como lo que estás subiendo es una imagen, podrías reducirla un poco o guardarla en un formato más comprimido, como JPG. Dianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. Actualmente, Dianara limita las subidas de archivos a 10 MiB por mensaje, para evitar posibles problemas de almacenamiento, o de red, en los servidores. This is a temporary measure, since the servers cannot set their own limits yet. Esto es una medida temporal, ya que los servidores no pueden establecer sus propios límites todavía. File not found. No se ha encontrado el archivo. Error Error The selected file cannot be accessed: No se puede acceder al archivo seleccionado: You might not have the necessary permissions. Es posible que no tengas los permisos necesarios. Resolution Image resolution (size) Resolución File is too big El archivo es demasiado grande Add a brief title for the post here (recommended) Añade un título breve para el mensaje aquí (recomendado) Do you want to make the post public instead of followers-only? ¿Quieres hacer el mensaje público en lugar de sólo para seguidores? &Yes, make it public &Sí, hacerlo público Sorry for the inconvenience. Lamentamos las molestias. It is owned by %1. %1 = a username Es propiedad de %1. Type Tipo Size Tamaño %1 KiB of %2 KiB uploaded %1 KiB de %2 KiB subidos Invalid image Imagen no válida Setting a title helps make the Meanwhile feed more informative Añadir un título ayuda a hacer el contenido del "Mientras tanto" más informativo Remove Quitar? Eliminar? Quitar Cancel the attachment, and go back to a regular note Cancelar el adjunto y volver a una nota normal Cc... Cc... Post verb Publicar Note started from another application. Nota empezada desde otra aplicación. Ignoring new note request from another application. Ignorando petición de nueva nota desde otra aplicación. Editing post. Editando mensaje. &No, post to my followers only &No, publicar sólo para mis seguidores 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. Select one audio file Selecciona un archivo de audio Audio files Archivos de audio Invalid audio file Archivo de audio no válido The audio format cannot be detected. El formato de audio no se puede detectar. Select one video file Selecciona un archivo de vídeo Video files Archivos de vídeo Invalid video file Archivo de vídeo no válido The video format cannot be detected. El formato de vídeo no se puede detectar. Posting... Publicando... Hit Control+Enter to post with the keyboard Pulsa Control+Enter para publicar con el teclado PumpController Getting likes... meh... Recibiendo los "me gusta"... Getting comments... Recibiendo comentarios... Error connecting to %1 Error conectando a %1 Unhandled HTTP error code %1 Código de error HTTP no gestionado: %1 Following %1 (%2) successfully. %1 is a person's name, %2 is the ID Siguiendo a %1 (%2) correctamente. Stopped following %1 (%2) successfully. %1 is a person's name, %2 is the ID Se ha dejado de seguir a %1 (%2) 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. List of %1 users received. %1 is a server name Lista de usuarios de %1 recibida. Person list '%1' created successfully. Lista de personas '%1' creada correctamente. Person list received. Lista de personas recibida. File uploaded successfully. Posting message... Archivo subido correctamente. Publicando mensaje... Authorized to use account %1. Getting initial data. Autorizado para usar la cuenta %1. Recibiendo datos iniciales. There is no authorized account. No hay ninguna cuenta autorizada. Updating profile... Actualizando perfil... Getting list of 'Following'... Recibiendo lista de 'Siguiendo'... Getting list of 'Followers'... Recibiendo lista de 'Seguidores'... Getting site users for %1... %1 is a server name Recibiendo usuarios del servidor %1... 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... Creating group... Creando grupo... Joining group... Uniéndose al grupo... Leaving group... Saliendo del grupo... Timeline Línea temporal Messages Mensajes User timeline Línea temporal de usuario Uploading %1 1=filename Subiendo %1 Error loading timeline! ¡Error cargando la línea temporal! Error loading minor feed! ¡Error cargando la línea temporal menor! Unable to verify the address! ¡No se ha podido verificar la dirección! HTTP error For the following HTTP error codesyou can check http://en.wikipedia.org/wiki/List_of_HTTP_status_codes in your language Error HTTP Gateway Timeout HTTP 504 error string Tiempo de espera de la pasarela agotado Service Unavailable HTTP 503 error string Servicio no disponible Bad Gateway HTTP 502 error string Pasarela incorrecta Not Implemented HTTP 501 error string No implementado 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 Server version: %1 Versión del servidor: %1 Profile received. Perfil recibido. Followers Seguidores Following Siguiendo Profile updated. Perfil actualizado. E-mail updated: %1 E-mail actualizado: %1 %1 published successfully. Updating post content... %1 is the type of object: note, image... Se ha publicado '%1' correctamente. Actualizando contenido del mensaje... Untitled post %1 published successfully. %1 is a piece of the post Mensaje sin título %1 publicado correctamente. Post %1 published successfully. %1 is the title of the post Mensaje %1 publicado correctamente. Avatar published successfully. Avatar publicado correctamente. Untitled post %1 updated successfully. %1 is a piece of the post Mensaje sin título %1 actualizado correctamente. Post %1 updated successfully. %1 is the title of the post Mensaje %1 actualizado correctamente. Comment %1 updated successfully. %1 is a piece of the comment Comentario %1 actualizado correctamente. Comment %1 posted successfully. %1 is a piece of the comment Comentario %1 publicado correctamente. %1 attempts %1 intentos 1 attempt 1 intento Some initial data was not received. Restarting initialization... Algunos datos iniciales no se han recibido. Reiniciando la inicialización... Can't follow %1 at this time. %1 is a user ID No se puede seguir a %1 en este momento. Trying to follow %1. %1 is a user ID Intentando seguir a %1. Checking address %1 before following... Comprobando dirección %1 antes de seguirla... Message liked or unliked successfully. Mensaje marcado o desmarcado "Me gusta" correctamente. Likes received. meh... Se han recibido los "me gusta". 1 comment received. 1 comentario recibido. %1 comments received. %1 comentarios recibidos. Post by %1 shared successfully. 1=author of the post we are sharing Mensaje de %1 compartido correctamente. Received '%1'. %1 is the name of a feed Se ha recibido '%1'. Adding items... Añadiendo elementos... SSL errors in connection to %1! ¡Errores de SSL en la conexión a %1! Loading external image from %1 regardless of SSL errors, as configured... %1 is a hostname Cargando imagen externa de %1 a pesar de los errores SSL, como se ha configurado... OAuth error while authorizing application. Error de OAuth mientras se autorizaba a la aplicación. Message deleted successfully. Mensaje eliminado correctamente. The comments for this post cannot be loaded due to missing data on the server. Los comentarios de este mensaje no se pueden cargar debido a que faltan datos en el servidor. Getting '%1'... %1 is the name of a feed Recibiendo '%1'... Activity Actividad Favorites Favoritos Meanwhile Mientras tanto Mentions Menciones Actions Acciones List of 'lists' received. Lista de 'listas' recibida. Person list deleted successfully. Lista de personas borrada correctamente. %1 (%2) added to list successfully. 1=contact name, 2=contact ID Se ha añadido a %1 (%2) a la lista correctamente. %1 (%2) removed from list successfully. 1=contact name, 2=contact ID Se ha quitado a %1 (%2) de la lista correctamente. Group %1 created successfully. Grupo %1 creado correctamente. Group %1 joined successfully. Se ha entrado al grupo %1 correctamente. Left the %1 group successfully. Se ha salido del grupo %1 correctamente. Avatar uploaded. Avatar subido. The application is not registered with your server yet. Registering... La aplicación no está registrada con tu servidor todavía. Registrando... Getting OAuth token... Recibiendo identificador de autorización (token) de OAuth... OAuth support error Error de soporte de OAuth Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support. Tu instalación de QOAuth, una biblioteca usada por Dianara, no parece tener soporte de HMAC-SHA1. You probably need to install the OpenSSL plugin for QCA: %1, %2 or similar. Probablemente necesites instalar el conector de OpenSSL para QCA: %1, %2 o similar. Authorization error Error de autorización There was an OAuth error while trying to get the authorization token. Ha habido un error de OAuth mientras se intentaba obtener un identificador de autorización (token). QOAuth error %1 Error de QOAuth %1 Application authorized successfully. Aplicación autorizada correctamente. Waiting for proxy password... Esperando contraseña del proxy... Still waiting for profile. Trying again... Aún se está esperando el perfil. Intentándolo de nuevo... Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally. Algunos datos iniciales no se han recibido tras varios intentos. Algo puede estar fallando en tu servidor. Es posible que puedas usar el servicio con normalidad. All initial data received. Initialization complete. Se han recibido todos los datos iniciales. Inicialización completada. Ready. Preparado. SiteUsersList You can get a list of the newest users registered on your server by clicking the button below. Puedes recibir una lista de los usuarios más nuevos registrados en tu servidor, haciendo clic en el botón de abajo. More resources to find users: Más recursos para encontrar usuarios: Wiki page 'Users by language' Página wiki 'Usuarios por idioma' PPump user search service at inventati.org Servicio PPump de búsqueda de usuarios en inventati.org List of Followers for the Pump.io Community account Lista de los seguidores de la cuenta Pump.io Community Get list of users from your server Recibir lista de usuarios de tu servidor Close list Cerrar la lista Loading... Cargando... %1 users in %2 %1 = user count, %2 = server name %1 usuarios en %2 TimeLine Welcome to Dianara Bienvenido a Dianara Dianara is a <b>Pump.io</b> client. Dianara es un cliente <b>Pump.io</b>. Press <b>F1</b> if you want to open the Help window. Pulsa <b>F1</b> si quieres abrir la ventana de ayuda. 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>. Dianara's blog Blog de Dianara Newest Lo más nuevo Newer Más nuevos Older Más antiguos Requesting... Solicitando... Loading... Cargando... Page %1 of %2. Página %1 de %2. Showing %1 posts per page. Mostrando %1 mensajes por página. %1 posts in total. %1 mensajes en total. Click here or press Control+G to jump to a specific page Haz clic aquí o pulsa Control+G para saltar a una página específica '%1' cannot be updated because a comment is currently being composed. %1 = feed's name No se puede actualizar '%1' porque se está editando un comentario. %1 more posts pending for next update. %1 mensajes más pendientes para la próxima actualización. Click here to receive them now. Haz clic aquí para recibirlos ahora. There are no posts No hay mensajes If you don't have a Pump account yet, you can get one at the following address, for instance: Si aún no tienes una cuenta Pump, puedes conseguir una en la siguiente dirección, por ejemplo: There are tooltips everywhere, so if you hover over 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 mantienes el ratón sobre un botón o un campo de texto, es probable que veas alguna información adicional. Pump.io User Guide Guía de usuario de 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 ti. 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. Timestamp Invalid timestamp! ¡Hora/fecha no válida! A minute ago Hace un minuto %1 minutes ago Hace %1 minutos An hour ago Hace una hora %1 hours ago Hace %1 horas Just now Ahora mismo In the future En el futuro Yesterday Ayer %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 UserPosts Posts by %1 Mensajes de %1 Loading... Cargando... &Close &Cerrar Received '%1'. Se ha recibido '%1'. %1 posts %1 mensajes Error loading the timeline Error cargando la línea temporal dianara-v1.4.1/translations/dianara_es.qm0000644000175000017500000040502313212034136016551 0ustar janjancy<<+1>R@)DV%BjH="H:*PHHonIaqKULbQLbvMeRNNOP7P7R> S T\TGTݫU_VWV*GV*VVWj`WLoX/ Xƥ(aYYZy%=[ %[!+\\j Fwkn&4q׎eBr$)wjw&yTAybzz>>qhN-c`.E>GC8.{^(.u"4_:̸kՅUI5 QҥH~_7[*H8N_#gPdx:ݣ^]syXY$Yp &-!0<#,&<QDInb!M$LMP3aPk QE97RS:V84I\q2loC}p`N=#,7T8rlh>s^wth OQK%) 2h )BY Uj,*3'ʑ6F=%;%%i3*>KL=PQRxNid.,hBpH=tɥ}v;gwSl,J9dVq^9 zjtH6o66.666L61 FR '" nMcW!^ƭ>wZ4nuV&ϠN.qi؞d};|&I~753V8YV4ND$#J 2 wp k}%mq vSx t$1($1YF(J.s0c`0c"66h6'70MEA9.M.XR@-MZN1ZkAd)i6ijCl#'sCJzwh3x2z{e=}˚z6.Q&Nnk^}B|be8¿pd¿hÌ P^Ƃjn˺>20q'pqV޷Od?_ I#n0J9qn P%?;ʳv 1[: S"CZʑ  #n=$Y+h6U.;>B0P `3Ja$VfXr) s|ߌ}Lx:c:xwK[7bW< K_b%U>;x]NCII*I_IpV6NHi8,%,Rg<37DϤnԁz^³}u$ctwmVn4zXͣZa3g[3gyրvR@Tz6;7]WFy :(5<#XR,(_4 >1KTNKT{LܤRMR1V|;]L.f]Yza+bcfycwh9~hTXj su_yģzc{7{\|7}̏Nk /=,.B{F%VVx`h>c4wL>T-tU3t oGC:4OF@úX\w0)0\8ƨ2]on:/+eNcأ`ht*0+I-^#4uX4uc(4u}9$U<._<.f<.y=QjAt1{CN}GmbNI!MI%M?$OO7RQQT$Vх{3ZP)ZzZRf;;!fg2hC"mnp{7<r7u|yda/]5J\ NYngSA-9NoDT",1.̐rDC[V4&ґy&ڑڱh1OFSC۩#Do%0Z1;<;Xp<WN<<b<|S2NWAcXgjWlqNn+pMrt.~{kpajažotEN~RO x?ȯBAҕ>n׬Nndi(.!nQ&  deiLynEKU"4#N ")2K<2?46~~JF&MJ#bMJMrcs\]ee7QkZ*OpG*s#sn8|2v|VU]l'+DejN-#B>Bd?z/e qD`T rnť3ʪ$]T琊Ws4Q~RzN7eE  !_ -Q 0\~ 0u~z 1`#H 6 v 7w9 <.d Mg P24 Sgn ^na r% s w>s\ | ~>  tl V 4?E J C~+ ~/   l ` !  H o%. dw b fZ& ʞX Ur p3 & ~ Ԭ [ t*Ч SW hCq Nu AD% A]  ~) n$ o f !/ # (O" ?m @=c @Ccyb E~S Hkȶ Tu TG{{ TG [d' ]j c}< d 4MQ d< d<* d<Q: dk9 e] pO8 q\5 v'ts z3.t _>/; .:  ]D I I IG I I IF IH I^ Ig )TZ ~ I   yS N , o*  tO d ^ޡ '_R tF }^ta ՁRa 3Gd ֓2 N % T$U | ؄q8 o 0S Y35 :+   > ~e ~n ~  c=Z ke E "ed " ,i4P -֮R 0\ 0h 5<w\ 7"Q <خr = =/nk @[ A#k ED&A LuF XS Ya/ bX,+ bXv. bL eTlp f(:= f*2 mmTL o>9 y[>^h |6j P/j ! q3o SN #gd In V b  { mI )iS  U.! ' sd- 5 j~ OC أZ y@b . z CL  23 Ғ# >1 (=P - U_? /O 6? =x5 Bhp E# RV Y\s [c ]ӡm `u d._ eMj j8s zOJ {n@ {dq R( 4 q Be \> rw Vw xX m q; #17 s ͡8 = ( Б* ۅy ] <ٔ y |ST 7~T Ik w< 7& a5 = ;' Е6 #pn ) .A B| Gfv Nn V3 _P b e&a gZv h1zO iFCs u9J 1 @J | L  ^ > »l ^s q ˽+8 7:0 @ bc O#   `O !CV R3N!2WҖT'cfK+jO/b1MSD`fR/6RT3XhccgOKlNlJltll_lo#mhnBm$ 8T$8TF8T 8TLk^`z"Da>#Qi'm*N=3Tk' Z^?U~AA%!0SK!.UHpO6ny|R3 #u"F`^_^[izB:'5%a'5%Α'5%'E)̗+<,.0*A Btn@B}~CjCE.kF%J[PdTI _}8`pDbuceY.uf`^iW2^0/!~nԆ.kz?^Xa~d3؞¹eAM$TWe+N~CP1,dz]"bڨdsB#ޝ1i%1 de %2%1 by %2 ASActivityPblicoPublic ASActivityArtculoArticleASObject AudioAudioASObjectColeccin CollectionASObjectComentarioCommentASObjectEliminado el %1 Deleted on %1ASObjectArchivoFileASObject GrupoGroupASObject ImagenImageASObject4No hay ubicacin detalladaNo detailed locationASObjectNotaNoteASObjectOtroOtherASObject VdeoVideoASObjecty %1 ms and %1 othersASObjecty uno ms and one otherASObject CiudadHometownASPerson0&Autorizar la aplicacin&Authorize Application AccountDialog&Cancelar&Cancel AccountDialog&Guardar datos &Save Details AccountDialog&Desbloquear&Unlock 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 o pega aqu el codigo de verificacin proporcionado por tu servidor PumpBEnter or paste 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 an no tienes una cuenta, puedes registrar una en %1. Este enlace te llevar a un servidor pblico aleatorio.sIf you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. AccountDialog,Si necesitas ayuda: %1If you need help: %1 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. AccountDialogzPulsa Desbloquear si quieres configurar una cuenta diferente.:Press Unlock if you wish to configure a different account. AccountDialog4Gua de usuario de Pump.ioPump.io User Guide AccountDialogPNo se ha podido abrir el navegador web!Unable to open web browser! AccountDialogHEl cdigo de verificacin est vacoVerifier code is empty AccountDialog.Cdigo de verificacin:Verifier code: AccountDialog<Tu direccin Pump no es vlidaYour Pump address is invalid AccountDialog*Tu direccin pump.io:Your Pump.io address: AccountDialogRTu cuenta est configurada correctamente.$Your account is properly configured. 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. AccountDialogVTu direccin, como usuario@servidorpump.org*Your address, like username@pumpserver.org AccountDialog.&Aadir a seleccionados&Add to SelectedAudienceSelector&Cancelar&CancelAudienceSelector &Hecho&DoneAudienceSelectorLista 'Cc' 'Cc' ListAudienceSelectorLista 'Para' 'To' ListAudienceSelector&Todos los contactos All ContactsAudienceSelectorBorrar &lista Clear &ListAudienceSelectorSeguidores FollowersAudienceSelector ListasListsAudienceSelectorPersonas... People...AudienceSelectorPblicoPublicAudienceSelector8Selecciona 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&No&No AvatarButton(&S, dejar de seguir&Yes, stop following AvatarButtondEsts seguro de que quieres dejar de seguir a %1?+Are you sure you want to stop following %1? AvatarButtonVer mensajesBrowse messages AvatarButton SeguirFollow AvatarButtonRAbrir el perfil de %1 en el navegador web Open %1's profile in web browser AvatarButtonFAbrir tu perfil en el navegador web Open your profile in web browser AvatarButton&Enviar mensaje a %1Send message to %1 AvatarButtonDejar de seguirStop following AvatarButton"Dejar de seguir?Stop following? AvatarButton(Ocultar este mensajeHide this messageBannerNotificationHEsto pasa cuando llega el momento de autoactualizar las lneas temporales, pero no ests al principio de la primera pgina, para evitar interrupciones mientras leesThis happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you readBannerNotificationNo se han actualizado las lneas temporales automticamente para evitar interrupciones.@Timelines were not automatically updated to avoid interruptions.BannerNotification Actualizar ahora Update nowBannerNotificationCambiar... Change... ColorPickerElige un colorChoose a color ColorPicker<A %1 les gusta este comentario%1 like this commentComment:A %1 le gusta este comentario%1 likes this commentComment&No&NoComment&S, eliminarlo&Yes, delete itCommentlEsts seguro de que quieres eliminar este comentario?-Are you sure you want to delete this comment?CommentEliminarDeleteComment EditarEditComment,Borrar este comentarioErase this commentCommentMe gustaLikeCommentnDecir que te gusta o que ya no te gusta este comentarioLike or unlike this commentComment Modificado el %1Modified on %1Comment2Modificar este comentarioModify this commentCommentPublicado el %1 Posted on %1Comment CitarQuoteCommentBResponder citando este comentarioReply quoting this commentCommentYa no me gustaUnlikeCommentDADVERTENCIA: Eliminar comentario?WARNING: Delete comment?Comment(Ha ocurrido un errorAn error occurredCommenterBlockCancelarCancelCommenterBlock*Comprobar comentariosCheck for commentsCommenterBlockComentarCommentCommenterBlock2El comentario est vaco.Comment is empty.CommenterBlockHLos comentarios no estn disponiblesComments are not availableCommenterBlock&Editando comentarioEditing commentCommenterBlock8Error: Ya se est redactandoError: Already composingCommenterBlock.Cargando comentarios...Loading comments...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,Actualizar comentariosReload commentsCommenterBlock,Enviando comentario...Sending comment...CommenterBlock4Mostrar los %1 comentariosShow all %1 commentsCommenterBlock4Actualizando comentario...Updating comment...CommenterBlockPuedes pulsar Control+Enter para enviar el comentario con el tecladoAYou can press Control+Enter to send the comment with the keyboardCommenterBlockNo puedes editar un comentario en este momento, porque ya se est redactando otro comentario.YYou can't edit a comment at this time, because another comment is already being composed.CommenterBlock&&Cancelar el enlace &Cancel linkComposer,&Introducirlo de nuevo&Enter it againComposer&Formato&FormatComposer&No&NoComposer$&Usarlo igualmente&Use it anywayComposer&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 FormattingComposerCabeceraHeaderComposer4Cuntas columnas (ancho)?How many columns (width)?Composer0Cuntas filas (altura)?How many rows (height)?Composer$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 siteComposer,Insertar como imagen?Insert as image?Composer(Insertar como enlaceInsert as linkComposer8Insertar como imagen visibleInsert as visible imageComposerInsertar lnea Insert lineComposer Enlace no vlido Invalid linkComposerPDebera comenzar con uno de estos tipos:(It should start with one of these types:ComposerCursivaItalicComposer ListaListComposerHacer 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 StrikethroughComposerSmbolosSymbolsComposer TablaTableComposer$Tamao de la tabla Table SizeComposer8Opciones de formato de textoText Formatting OptionsComposerLa direccin que has introducido (%1) no es vlida. Las direcciones de imgenes han de empezar por http:// o https://`The address you entered (%1) is not valid. Image addresses should begin with http:// or https://ComposervParece que el enlace que ests pegando apunta a una imagen.4The link you are pasting seems to point to an image.ComposerbEl texto que has introducido no parece un enlace./The text you entered does not look like a link.Composer4Escribe un comentario aquType a comment hereComposerNEscribe un mensaje aqu para publicarloType a message here to post itComposerEscribe 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.ComposerEscribe o pega una direccin web aqu. Tambin puedes seleccionar texto antes, para convertirlo en un enlace.`Type or paste a web address here. You could also select some text first, to turn it into a link.ComposerEscribe o pega la direccin de la imagen aqu. El enlace ha de apuntar al archivo de imagen directamente.UType or paste the image address here. The link must point to the image file directly.ComposerSubrayado UnderlineComposer>S, pero guardando un &borradorYes, but saving a &draftComposer@Solo puedes adjuntar un archivo.You can attach only one file.ComposerlNo puedes soltar carpetas aqu, solo un nico archivo.1You cannot drop folders here, only a single file.Composer&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$Despus del avatar After avatar ConfigDialog2Despus del avatar, sutilAfter avatar, subtle ConfigDialog$Todos los archivos All files ConfigDialogLDestacar tambin en la barra de tareasAlso highlight taskbar entry ConfigDialogSiempreAlways ConfigDialog:Cualquier actividad destacadaAny highlighted activity ConfigDialog>Como notificaciones del sistemaAs system notifications ConfigDialog,Tamao de los avatares Avatar size ConfigDialogJTamao de los avatares en comentariosAvatar size in comments ConfigDialog Antes del avatar Before avatar ConfigDialog.Antes del avatar, sutilBefore avatar, subtle ConfigDialogParte inferiorBottom ConfigDialogColoresColors ConfigDialogComentariosComments ConfigDialog EditorComposer ConfigDialog(&Icono personalizado Custom &Icon ConfigDialog&Icono personalizado Custom icon ConfigDialogPredefinidoDefault ConfigDialogJDianara guarda datos en esta carpeta:#Dianara stores data in this folder: ConfigDialog`No informar a los seguidores al seguir a alguien-Don't inform followers when following someone ConfigDialog`No informar a los seguidores al manipular listas*Don't inform followers when handling lists ConfigDialog2No mostrar notificacionesDon't show notifications ConfigDialogDuracinDuration ConfigDialog$Normas de filtradoFiltering rules ConfigDialogTipos de letraFonts ConfigDialog$Opciones generalesGeneral Options ConfigDialog6Ocultar mensajes duplicadosHide duplicated posts ConfigDialog4Ocultar ventana al iniciarHide window on startup ConfigDialogTDestacar comentarios del autor del mensaje Highlight post author's comments ConfigDialog@Destacar tus propios comentariosHighlight your own comments ConfigDialogbActividades destacadas en la lnea temporal menor$Highlighted activities in minor feed ConfigDialogPActividades destacadas, excepto las mas#Highlighted activities, except mine ConfigDialog&Mensajes destacadosHighlighted posts ConfigDialogLIgnorar errores de SSL en las imgenesIgnore SSL errors in images ConfigDialog$Archivos de imagen Image files ConfigDialog&Errores importantesImportant errors ConfigDialogPInformar slo al autor de los "Me gusta")Inform only the author when liking things ConfigDialog Imagen no vlida Invalid image ConfigDialogTElemento destacado por normas de filtrado.(Item highlighted due to filtering rules. ConfigDialog*El elemento es nuevo. Item is new. ConfigDialogdSaltar a la lnea de nuevos mensajes al actualizar Jump to new posts line on update ConfigDialogLado izquierdo Left side ConfigDialog2Lneas temporales menores Minor Feeds ConfigDialog^Tamao de avatares en lneas temporales menoresMinor feed avatar sizes ConfigDialog(Configuracin de redNetwork configuration ConfigDialog NuncaNever ConfigDialogZNuevas actividades en la lnea temporal menorNew activities in minor feed ConfigDialogMensajes nuevos New posts ConfigDialogNoNo ConfigDialog8Estilo de las notificacionesNotification Style ConfigDialogNotificaciones Notifications ConfigDialog8Notificar cuando se reciban:Notify when receiving: ConfigDialog^Slo para imgenes insertadas desde sitios web.(Only for images inserted from web sites. ConfigDialogJSe usarn las notificaciones propias.Own notifications will be used. ConfigDialog6Notificaciones persistentesPersistent Notifications ConfigDialog2Contenido de los mensajes Post Contents ConfigDialog.Ttulos de los mensajes Post Titles ConfigDialogMensajesPosts ConfigDialogZMensajes por pgina, &otras lneas temporales Posts per page, &other timelines ConfigDialogPrivacidadPrivacy ConfigDialog"Ajustes de pro&xyPro&xy Settings ConfigDialog4Configuracin del programaProgram Configuration ConfigDialogPMensajes pblicos de manera &predefinidaPublic posts as &default ConfigDialogLado derecho Right side ConfigDialogS&eleccionar... S&elect... ConfigDialog<Selecciona icono personalizadoSelect custom icon ConfigDialog&Configurar f&iltrosSet Up F&ilters ConfigDialog6Mostrar iconos de actividadShow activity icons ConfigDialog<Mostrar contador de caracteresShow character counter ConfigDialog`Mostrar informacin adicional en los compartidosShow extended share information ConfigDialog2Mostrar informacin extraShow extra information ConfigDialogDMostrar imgenes a tamao completoShow full size images ConfigDialog\Mostrar informacin de los mensajes eliminados"Show information for deleted posts ConfigDialogfMostrar fragmentos en las lneas temporales menoresShow snippets in minor feeds ConfigDialog0Mostrar tu avatar actualShow your current avatar ConfigDialog0Lmite de los fragmentos Snippet limit ConfigDialogFLmite de los fragmentos destacadosSnippet limit when highlighted ConfigDialog&Bandeja del sistema System Tray ConfigDialogP&Tipo de icono en la bandeja del sistemaSystem Tray Icon &Type ConfigDialogJIcono del sistema, si est disponibleSystem iconset, if available ConfigDialogjLas notificaciones del sistema no estn disponibles!'System notifications are not available! ConfigDialogLa actividad es en respuesta a alguna cosa hecha por ti, como un comentario publicado en respuesta a una de tus notas.jThe activity is in reply to something done by you, such as a comment posted in reply to one of your notes. ConfigDialogLa actividad est relacionada con uno de tus objetos, como por ejemplo cuando a alguien le gusta uno de tus mensajes.YThe activity is related to one of your objects, such as someone liking one of your posts. ConfigDialogHLa imagen seleccionada no es vlida. The selected image is not valid. ConfigDialog>Esto es una notificacin bsicaThis is a basic notification ConfigDialogHEsto es una notificacin del sistemaThis is a system notification ConfigDialog`Intervalo de &actualizacin de la lnea temporalTimeline &update interval ConfigDialog"Lneas temporales Timelines ConfigDialogParte superiorTop ConfigDialognUsar nombre del adjunto como ttulo inicial del mensaje-Use attachment filename as initial post title ConfigDialog"Usar con cuidado.Use with care. ConfigDialogUsado tambin cuando se destacan mensajes dirigidos a ti en las lneas temporales.DUsed also when highlighting posts addressed to you in the timelines. ConfigDialogUsado tambin cuando se destacan tus propios mensajes en las lneas temporales.&Eliminar borrador seleccionado&Delete selected draft DraftsManager&No&No DraftsManager&S, eliminarlo&Yes, delete it DraftsManagerhEsts seguro de que quieres eliminar este borrador?+Are you sure you want to delete this draft? DraftsManager&Eliminar borrador? Delete draft? DraftsManager(Gestor de borradores Draft Manager DraftsManager CargarLoad DraftsManager.Gestionar borradores...Manage drafts... DraftsManagerGuardarSave DraftsManager&Borrador sin ttuloUntitled draft DraftsManager&Cancelar&Cancel EmailChangerOtra vez:Again: EmailChangerCambiarChange EmailChanger6Cambiar direccin de e-mailChange E-mail Address EmailChanger(Direccin de e-mail:E-mail Address: EmailChangerPLas direcciones de e-mail no coinciden!E-mail addresses don't match! EmailChanger4La contrasea est vaca!Password is empty! EmailChangerTu contrasea:Your Password: EmailChangerMostrarShowFDNotifications*%1 si %2 contiene: %3%1 if %2 contains: %3 FilterEditor&Aadir filtro &Add Filter FilterEditor&Cancelar&Cancel FilterEditor&Nuevo filtro &New Filter FilterEditor6&Quitar filtro seleccionado&Remove Selected Filter FilterEditor &Guardar filtros &Save Filters FilterEditor6Descripcin de la actividadActivity Description FilterEditorAplicacin Application FilterEditorID del autor Author ID FilterEditor"Filtros &actualesC&urrent Filters FilterEditor"Editor de filtros Filter Editor FilterEditorFiltros en usoFilters in use FilterEditorfAqu puedes establecer algunas normas para ocultar o destacar cosas. 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. Tambin podras destacar mensajes que contienen tu nombre.-Here you can set some rules for hiding or highlighting stuff. 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. You could also highlight messages that contain your name. FilterEditorOcultarHide FilterEditorDestacar Highlight FilterEditor"Palabras clave... Keywords... FilterEditor6Contenido de la publicacin Post Contents FilterEditorcontienecontains FilterEditorsiif FilterEditorAplicacinAppFilterMatchesWidget AutorAuthorFilterMatchesWidgetContenidoContentFilterMatchesWidgetDescripcin DescriptionFilterMatchesWidget&Cerrar&CloseFirstRunWizard &Edita tu perfil&Edit your profileFirstRunWizardl&Mostrar de nuevo la prxima vez que se inicie Dianara)&Show this again next time Dianara startsFirstRunWizard De manera predefinida, Dianara publicar slo para tus seguidores, pero es recomendable que publiques para Pblico, al menos a veces.wBy default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes.FirstRunWizard(Configur&a tu cuentaConfigure your &accountFirstRunWizard$Una vez que hayas configurado tu cuenta, es recomendable que edites tu perfil y aadas un avatar y algo ms de informacin, si no lo has hecho ya.Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already.FirstRunWizard^Abrir la ventana de ay&uda general del programa!Open general program &help windowFirstRunWizardXPublicar para &Pblico de manera predefinidaPost to &Public by defaultFirstRunWizardEl primer paso es configurar tu cuenta, usando el siguiente botn:IThe first step is setting up your account, by using the following button:FirstRunWizardHEste asistente te ayudar a empezar.&This wizard will help you get started.FirstRunWizard.Asistente de bienvenidaWelcome WizardFirstRunWizard,Bienvenido a Dianara!Welcome to Dianara!FirstRunWizardPuedes volver a acceder a esta ventana en cualquier momento desde el men de Ayuda.@You can access this window again at any time from the Help menu.FirstRunWizardCambiar... Change... FontPicker,Elige un tipo de letra Choose a font FontPicker&Cerrar&Close HelpWidgetLnea temporal de actividad, donde vers tus propios mensajes, o mensajes que has compartido.KActivity timeline, where you'll see your own posts, or posts shared by you. HelpWidgetEn este momento, se cargar tu perfil, las listas de contactos y las lneas temporales.HAt this point, your profile, contact lists and timelines will be loaded. HelpWidgetAyuda bsica Basic Help HelpWidget6Aparte de eso, puedes usar:Besides that, you can use: HelpWidgetElige una con las teclas de cursor y pulsa Enter para completar el nombre. Esto aadir a esta persona a la lista de destinatarios.vChoose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. HelpWidget:Opciones de lnea de comandosCommand line options HelpWidget ndiceContents HelpWidget~Control+1/2/3 para cambiar entre las lneas temporales menores.0Control+1/2/3 to switch between the minor feeds. HelpWidgetControl+Enter para acabar de crear una lista de destinatarios para un mensaje, en las listas 'Para' o 'Cc'.\Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. HelpWidgetControl+Enter para publicar, cuando hayas acabado de redactar una nota o un comentario. Si la nota est vaca, la puedes cancelar pulsando ESC.Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. HelpWidgetControl+G para ir a cualquier pgina de la lnea temporal directamente.5Control+G to go to any page in the timeline directly. HelpWidgetControl+Izquierda/Derecha para pasar una pgina en la lnea temporal.4Control+Left/Right to jump one page in the timeline. HelpWidgetControl+Arriba/Abajo/RePag/AvPag/Inicio/Fin para moverte por la lnea temporal.AControl+Up/Down/PgUp/PgDown/Home/End to move around the timeline. HelpWidgetDianara ofrece una interfaz D-Bus que permite un cierto control desde otras aplicaciones.RDianara offers a D-Bus interface that allows some control from other applications. HelpWidgetLnea temporal de favoritos, donde vers los mensajes que te han gustado. Esto puede usarse como un sistema de marcadores.pFavorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. HelpWidgetRSeguidores de la cuenta Pump.io Community&Followers of Pump.io Community account HelpWidgetEmpezandoGetting started HelpWidgetHAqu, tambin puedes activar la opcin para publicar siempre tus mensajes como pblicos de forma predefinida. Siempre puedes cambiar esto en el momento de publicar.Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. HelpWidgetSi aades una persona especfica a la lista 'Para', recibir tu mensaje en su pestaa de mensajes directos.kIf you add a specific person to the 'To' list, they will receive your message in their direct messages tab. HelpWidget Si usas una configuracin alternativa, con algo como '--config otraconf', entonces la interfaz estar en org.nongnu.dianara_otraconf.If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. HelpWidgetSi es la primera vez que usas Pump.io, echa un vistazo a esta gua:4If you're new to Pump.io, take a look at this guide: HelpWidgetSi tu servidor no tiene soporte HTTPS, puedes utilizar el parmetro --nohttps.KIf your server does not support HTTPS, you can use the --nohttps parameter. HelpWidgetEs posible adjuntar imgenes, audio, video y archivos generales, como documentos PDF, a tu mensaje.cIt is possible to attach images, audio, video, and general files, like PDF documents, to your post. HelpWidget|Recuerda que hay muchos lugares en Dianara donde puedes obtener ms informacin manteniendo el ratn sobre algn texto o botn, y esperando a que aparezca la descripcin emergente (tooltip).Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. HelpWidget&Control por tecladoKeyboard controls HelpWidget2Gestionando los contactosManaging contacts HelpWidget&Lnea temporal de mensajes, donde vers mensajes enviados a ti especficamente. Estos mensajes pueden haber sido enviados tambin a otras personas.Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. HelpWidgetLos mensajes nuevos aparecen destacados en un color diferente. Puedes marcarlos como ledos haciendo clic en cualquier parte vaca del mensaje.New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. HelpWidgetPublicandoPosting HelpWidget4Gua de usuario de Pump.ioPump.io User Guide HelpWidgetConfiguracinSettings HelpWidgetFLa quinta lnea temporal es la lnea temporal menor, tambin conocida como el "Mientras tanto". Es visible en el lado izquierdo, aunque se puede esconder. Aqu vers actividades secundarias hechas por todo el mundo, como acciones de comentar, marcar mensajes con "Me gusta" o seguir a gente.The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. HelpWidgetLa primera vez que inicies Dianara, deberas ver la ventana de Configuracin de la cuenta. All, introduce tu direccin de Pump.io como nombre@servidor y pulsa el botn Obtener cdigo de verificacin.The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. HelpWidgetLa interfaz se encuentra en %1, y puedes acceder a ella con herramientas como %2 o %3. Ofrece mtodos como %4 y %5.lThe interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. HelpWidgetLa lnea temporal principal, donde vers todo lo que ha publicado o compartido la gente a la que sigues.\The main timeline, where you'll see all the stuff posted or shared by the people you follow. HelpWidgetLas acciones ms comunes que se encuentran en los mens tienen atajos de teclado escritos al lado, como F5 o Control+N.nThe most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. HelpWidgetLa sexta y sptima lneas temporales tambin son lneas temporales menores, parecidas al "Mientras tanto", pero contienen nicamente actividades dirigidas a ti (Menciones) y actividades hechas por ti (Acciones).The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). HelpWidgetPA continuacin, tu navegador web habitual debera cargar la pgina de autorizacin en tu servidor Pump.io. All, tendrs que copiar el cdigo 'VERIFIER' completo, y pegarlo en el segundo campo de Dianara. Entonces pulsa 'Autorizar la aplicacin', y una vez que se confirme, pulsa 'Guardar datos'.Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. HelpWidget8Hay siete lneas temporales:There are seven timelines: HelpWidgetHay un campo de texto en la parte superior, donde puedes introducir directamente direcciones de contactos nuevos para seguirles.hThere is a text field at the top, where you can directly enter addresses of new contacts to follow them. HelpWidgetAll puedes gestionar tambin las listas de personas, que se utilizan principalmente para enviar mensajes a grupos de gente especficos.`There, you can also manage person lists, used mainly to send posts to specific groups of people. HelpWidgetEstas actividades pueden tener un botn "+". Plsalo para abrir el mensaje al que hacen referencia. Adems, como en muchos otros lugares, puedes mantener el ratn encima para ver informacin relevante en la descripcin emergente (tooltip).These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. HelpWidget"Lneas temporales Timelines HelpWidgetJDentro de la pestaa 'Vecinos' vers algunos recursos para encontrar gente, y tendrs la opcin de ver los ltimos usuarios registrados en tu servidor, directamente.Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. HelpWidgetUsa el parmetro --debug para tener informacin adicional en la ventana de la terminal, sobre lo que est haciendo el programa.mUse the --debug parameter to have extra information in your terminal window, about what the program is doing. HelpWidget&Usuarios por idiomaUsers by language HelpWidgetnMientras redactas una nota, pulsa Enter para pasar del ttulo al cuerpo del mensaje. Adems, pulsando la flecha arriba cuando te encuentres al principio del mensaje, vuelve al ttulo.While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. HelpWidgetTambin puedes enviar un mensaje directo (inicialmente privado) a este contacto desde este men.VYou can also send a direct message (initially private) to that contact from this menu. HelpWidgetTambin puedes teclear '@' y los primeros caracteres del nombre de un contacto para mostrar un men emergente con opciones que coincidan.wYou can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. HelpWidgetPuedes hacer clic en cualquier avatar en los mensajes, los comentarios, y la columna "Mientras tanto", y vers un men con varias opciones, una de las cuales es seguir o dejar de seguir a esta persona.You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. HelpWidget Puedes ajustar varias cosas a tu gusto en la configuracin, como el intervalo de tiempo entre actualizaciones de la lnea temporal, cuantos mensajes por pgina quieres, colores para destacar mensajes, notificaciones o qu aspecto tendr el icono de la bandeja del sistema.You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. HelpWidgetPuedes hacer mensajes privados aadiendo gente especfica a estas listas, y desmarcando las opciones Pblico y Seguidores.~You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. HelpWidgetPuedes encontrar una lista con algunos usuarios de Pump.io y otros datos aqu:GYou can find a list with some Pump.io users and other information here: HelpWidgetZPuedes publicar notas haciendo clic en el campo de texto de la parte superior de la ventana o pulsando Control+N. Aadir un ttulo al mensaje es opcional, pero muy recomendable, ya que ayudar a identificar mejor las referencias a tu mensaje en la lnea temporal menor, notificaciones por e-mail, etc.You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. HelpWidgetPuedes ver las listas de gente a la que sigues, y que te sigue, en la pestaa Contactos.UYou can see the lists of people you follow, and who follow you from the Contacts tab. HelpWidgetPuedes seleccionar quien ver tu mensaje usando los botones 'Para' y 'Cc'.EYou can select who will see your post by using the To and Cc buttons. HelpWidgetPuedes usar el parmetro --config para ejecutar el programa con una configuracin diferente. Esto puede ser til para usar dos o ms cuentas diferentes. Incluso puedes ejecutar dos instancias de Dianara a la vez.You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. HelpWidgetFPuedes usar el botn Formato para aadir formato al texto, como negrita o cursiva. Algunas de estas opciones requieren que se seleccione el texto antes de usarlas.You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. HelpWidgetDDeberas echar un vistazo a la ventana 'Configuracin del programa', en el men Configuracin - Configurar Dianara. All encontrars varias opciones interesantes.You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. HelpWidget&Cerrar&Close ImageViewer&Reiniciar&Restart ImageViewer &Guardar como... &Save As... ImageViewer$Todos los archivos All files ImageViewerCerrar visor Close Viewer ImageViewer<Descargando imagen completa...Downloading full image... ImageViewer8Error descargando la imagen!Error downloading image! ImageViewer2Error guardando la imagenError saving image ImageViewerAjustarFit ImageViewer ImagenImage ImageViewer$Archivos de imagen Image files ImageViewer<Girar la imagen a la izquierdaRotate image to the left ImageViewer8Girar la imagen a la derechaRotate image to the right 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<Vuelve a intentarlo ms tarde.Try again later. ImageViewerSin ttuloUntitled ImageViewer&Aadir a lista &Add to List ListsManager4&Borrar lista seleccionada&Delete Selected List ListsManager&No&No ListsManager&Quitar miembro&Remove Member ListsManager&S&Yes ListsManager&S, eliminarla&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&Cerrar&Close LogViewer&Borrar el &registro Clear &Log LogViewerRegistroLog LogViewer%1 bytes%1 bytes MainWindow%1 eliminados. %1 deleted. MainWindow%1 filtrados.%1 filtered out. MainWindow%1 filtradas.plural, several activities%1 filtered out. MainWindow%1 destacados%1 highlighted MainWindow%1 destacadas.%1 highlighted. MainWindow:%1 ms pendientes de recibir.%1 more pending to receive. MainWindow:%1 ms pendientes de recibir.plural, several activities%1 more pending to receive. MainWindow&Cuenta&Account MainWindow&Actividad &Activity MainWindow&&Configurar Dianara&Configure Dianara MainWindow&Contactos &Contacts MainWindow*&Filtros y destacados&Filters and Highlighting MainWindow Ay&uda&Help MainWindow &Ocultar ventana &Hide Window MainWindow&Registro&Log MainWindow&Mensajes &Messages MainWindow&No&No MainWindow$&Publicar una nota &Post a Note MainWindow &Salir&Quit MainWindow&Sesin&Session MainWindow &Mostrar ventana &Show Window MainWindowLnea &temporal &Timeline MainWindow,&Barra de herramientas&Toolbar MainWindow&Ver&View MainWindow.&S, cerrar el programa&Yes, close the program MainWindow"'%1' actualizada. '%1' updated. MainWindow1 eliminado. 1 deleted. MainWindow1 filtrado.1 filtered out. MainWindow1 filtrada. singular, refers to one activity1 filtered out. MainWindow1 destacado 1 highlighted MainWindow1 destacada.1 highlighted. MainWindow61 ms pendiente de recibir.1 more pending to receive. MainWindow61 ms pendiente de recibir.singular, 1 activity1 more pending to receive. MainWindow$Acerca de &DianaraAbout &Dianara MainWindow"Acerca de Dianara About Dianara MainWindowAdems:Also: MainWindowDAuto-actualizar lneas &temporalesAuto-update &Timelines MainWindowBAuto-actualizaciones desactivadasAuto-updating disabled MainWindow<Auto-actualizaciones activadasAuto-updating enabled MainWindow&Ayuda bsica Basic &Help MainWindowPor filtros By filters MainWindowNHaz clic aqu para configurar tu cuenta$Click here to configure your account MainWindow<Haz clic para editar tu perfilClick to edit your profile MainWindowhCerrando debido a que el entorno se est apagando...+Closing due to environment shutting down... MainWindowlDianara no se puede ocultar en la bandeja del sistema.,Dianara cannot be hidden in the system tray. MainWindowDianara es software libre, licenciado bajo la licencia GNU GPL, y utiliza algunos iconos Oxygen con licencia LGPL.lDianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. MainWindowbDianara es un cliente de red social para pump.io..Dianara is a pump.io social networking client. MainWindow"Dianara iniciado.Dianara started. MainWindow"Mensajes directosDirect messages MainWindowDRealmente quieres cerrar Dianara?$Do you really want to close Dianara? MainWindowTQuieres cerrar el programa completamente?,Do you want to close the program completely? MainWindowEditar &perfil Edit &Profile MainWindowRTraduccin al castellano por JanKusanagi.#English translation by JanKusanagi. MainWindow^Introduce la contrasea para tu servidor proxy:)Enter the password for your proxy server: MainWindow:Error almacenando la imagen!Error storing image! MainWindow&Favoritos Favor&ites MainWindow$&Pantalla completa Full &Screen MainWindow Inicializando...Initializing... MainWindow0ltima actualizacin: %1Last update: %1 MainWindowEnlace a: %1 Link to: %1 MainWindowJLista de algunos &usuarios de Pump.ioList of Some Pump.io &Users MainWindowVPaneles y barras de herramientas bloqueadosLocked Panels and Toolbars MainWindow,Marcar todo como ledoMark All as Read MainWindow6Marcando todo como ledo...Marking everything as read... MainWindowJMensajes enviados explcitamente a tiMessages sent explicitly to you MainWindowLActividades secundarias dirigidas a ti!Minor activities addressed to you MainWindowActividades secundarias hechas por todo el mundo, tales como respuestas a mensajesAlgunos &consejos sobre Pump.ioSome Pump.io &Tips MainWindowIniciando actualizacin automtica de lneas temporales, una vez cada %1 minutos.>Starting automatic update of timelines, once every %1 minutes. MainWindow &Barra de estado Status &Bar MainWindowrDeteniendo actualizacin automtica de lneas temporales.'Stopping automatic update of timelines. MainWindowlEl icono de la bandeja del sistema no est disponible."System tray icon is not available. 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 MainWindowLa gente a la que sigues, la que te sigue, y tus listas de personasEThe people you follow, the ones who follow you, and your person lists MainWindow4Hay %1 actividades nuevas.There are %1 new activities. MainWindow.Hay %1 mensajes nuevos.There are %1 new posts. MainWindow,Hay 1 actividad nueva.There is 1 new activity. MainWindow(Hay 1 mensaje nuevo.There is 1 new post. MainWindowHLnea temporal actualizada a las %1.Timeline updated at %1. MainWindow*Barra de herramientasToolbar MainWindow*Total de mensajes: %1Total posts: %1 MainWindowActualizar %1 Update %1 MainWindow$Visitar sitio &webVisit &Website MainWindowTCon Dianara puedes ver tus lneas temporales, crear nuevos mensajes, subir fotos y multimedia, interactuar con los mensajes, gestionar tus contactos y seguir gente nueva.With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. MainWindowTEsts redactando una nota o un comentario.&You are composing a note or a comment. MainWindowHas configurado un servidor proxy con autenticacin, pero la contrasea no est definida.TYou have configured a proxy server with authentication, but the password is not set. 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. MainWindow.Tu biografa est vacaYour biography is empty MainWindow4Los mensajes que te gustanYour favorited posts MainWindow(Tus propios mensajesYour own posts MainWindow*Recibir %1 ms nuevas Get %1 newer MinorFeedTObtener actividades secundarias anterioresGet previous minor activities MinorFeed,Actividades anterioresOlder Activities MinorFeedHAn no hay actividades para mostrar.$There are no activities to show yet. MinorFeed Cc: %1Cc: %1 MinorFeedItem:Abrir el mensaje referenciadoOpen referenced post MinorFeedItemPara: %1To: %1 MinorFeedItemUsando %1Using %1 MinorFeedItemRError: No se ha podido abrir el navegadorError: Unable to launch browser MiscHelpersNo se ha podido ejecutar el navegador web predefinido del sistema.5The default system web browser could not be executed. MiscHelpers`Puede que necesites instalar las utilidades XDG.,You might need to install the XDG utilities. MiscHelpers bytesbytes MiscHelpers&Cancelar&Cancel PageSelector&Primera&First PageSelector&Ir&Go PageSelectorltim&a&Last PageSelectorSaltar a pgina Jump to page PageSelectorMs nuevasNewer PageSelectorMs antiguasOlder PageSelector"Nmero de pgina: Page number: PageSelector&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 commentsPostLes gusta a %1 %1 like thisPostLes gusta a %1%1 likesPostLe gusta a %1 %1 likes thisPost.%1 miembros en el grupo%1 members in the groupPost*%1 ha compartido esto%1 shared thisPost,%1 han compartido esto#%1 = Names for more than one person%1 shared thisPost&Cerrar&ClosePost&No&NoPost&S, eliminarlo&Yes, delete itPost &S, compartirlo&Yes, share itPost0&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?PostrEsts seguro de que quieres compartir tu propio mensaje?-Are you sure you want to share your own post?PostAudio adjuntoAttached AudioPostArchivo adjunto Attached FilePostVdeo adjuntoAttached VideoPostCcCcPostfHaz clic en la imagen para verla en tamao completo&Click the image to see it in full sizePostTHaz clic para descargar el archivo adjunto Click to download the attachmentPostComentarCommentPostXCopiar el enlace del mensaje al portapapelesCopy post link to clipboardPostDNo se ha podido cargar la imagen!Couldn't load image!PostEliminarDeletePostHQuieres 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 EditarEditPostEditado: %1 Edited: %1Post&Borrar este mensajeErase this postPostXSi seleccionas parte del texto, ser citado.+If you select some text, it will be quoted.PostbLa imagen es animada. Haz clic para reproducirla.'Image is animated. Click on it to play.PostEnInPostUnirse al grupo Join GroupPostMe gustaLikePost>Decir que te gusta este mensajeLike this postPost$Cargando imagen...Loading image...Post Modificado el %1Modified on %1Post,Modificar este mensajeModify this postPost8Normalizar colores del textoNormalize text colorsPostHAbrir el mensaje en el navegador webOpen post in web browserPostXAbrir el mensaje padre, al que ste responde/Open the parent post, to which this one repliesPost PadreParentPostMensajePostPostPublicado el %1 Posted on %1Post2Responder a este mensaje.Reply to this post.PostCompartirSharePost&Compartir mensaje? Share post?PostPCompartir este mensaje con tus contactos"Share this post with your contactsPost&Compartido %1 vecesShared %1 timesPost Compartido el %1 Shared on %1Post$Compartido una vez Shared oncePost TamaoSizePostParaToPostTipoTypePostYa no me gustaUnlikePost$Dejar de compartirUnsharePost>Dejar de compartir el mensaje? Unshare post?Post>Dejar de compartir este mensajeUnshare this postPostUsando %1Using %1PostA travs de %1Via %1Post>ADVERTENCIA: Eliminar mensaje?WARNING: Delete post?PostTe gusta esto You like thisPost&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$Cambiar &e-mail...Change &E-mail... ProfileEditorCambiar tu avatar crear una publicacin en tu lnea temporal con l. Si borras esta publicacin, tu avatar se borrar tambin.zChanging your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. 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&Cancelar&Cancel ProxyDialog&Servidor &Hostname ProxyDialog&Puerto&Port ProxyDialog&Guardar&Save ProxyDialog&Usuario&User ProxyDialog(No utilizar un proxyDo not use a proxy ProxyDialog Nota: La contrasea no se guarda de forma segura. Si lo deseas, puedes dejar el campo vaco, y se te pedir la contrasea al iniciar.Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. ProxyDialogCon&trasea Pass&word ProxyDialog&Tipo de proxy Proxy &Type ProxyDialog,Configuracin de proxyProxy Configuration ProxyDialog&Usar &autenticacinUse &Authentication ProxyDialogDTu nombre de usuario para el proxyYour proxy username ProxyDialog0%1 KiB de %2 KiB subidos%1 KiB of %2 KiB uploaded Publisher8&Cancelar, volver al mensaje&Cancel, go back to the post PublisherL&No, publicar slo para mis seguidores&No, post to my followers only Publisher(&S, hacerlo pblico&Yes, make it public Publisher&Aadir...Ad&d... PublisherpAade un ttulo breve para el mensaje aqu (recomendado)1Add a brief title for the post here (recommended) Publisher$Todos los archivos All files Publisher AudioAudio Publisher@Archivo de audio no seleccionadoAudio file not set Publisher"Archivos de audio Audio files PublisherCancelarCancel Publisher\Cancelar el adjunto y volver a una nota normal4Cancel the attachment, and go back to a regular note Publisher&Cancelar el mensajeCancel the post Publisher Cc...Cc... Publisher0Actualmente, Dianara limita las subidas de archivos a 10 MiB por mensaje, para evitar posibles problemas de almacenamiento, o de red, en los servidores.yDianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. PublisherQuieres hacer el mensaje pblico en lugar de slo para seguidores?>Do you want to make the post public instead of followers-only? Publisher4Se ha cargado un borrador. Draft loaded. Publisher$Borrador guardado. Draft saved. PublisherBorradoresDrafts Publisher8ERROR: Ya se est redactandoERROR: Already composing Publisher"Editando mensaje. Editing post. Publisher ErrorError Publisher8Error: Ya se est redactandoError: Already composing Publisher<El archivo es demasiado grandeFile is too big Publisher>No se ha encontrado el archivo.File not found. PublisherBNo se ha seleccionado un archivo.File not selected. Publisher.Archivo no seleccionado File not set PublisherZEncontrar el archivo de audio en tus carpetas#Find the audio file in your folders PublisherHEncontrar el archivo en tus carpetasFind the file in your folders PublisherBEncontrar la foto en tus carpetas Find the picture in your folders PublisherDEncontrar el vdeo en tus carpetasFind the video in your folders Publisher`Pulsa Control+Enter para publicar con el teclado+Hit Control+Enter to post with the keyboard PublishernSi publicas de esta manera, nadie podr ver tu mensaje.?If you post like this, no one will be able to see your message. PublishernIgnorando peticin de nueva nota desde otra aplicacin.3Ignoring new note request from another application. Publisher$Archivos de imagen Image files Publisher4Archivo de audio no vlidoInvalid audio file Publisher"Archivo no vlido Invalid file Publisher Imagen no vlida Invalid image Publisher4Archivo de vdeo no vlidoInvalid video file Publisher&Es propiedad de %1.It is owned by %1. PublisherHNota empezada desde otra aplicacin.&Note started from another application. Publisher OtrosOther PublisherFotoPicture 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... Publisher QuitarRemove PublisherResolucin Resolution Publisher>Seleccionar archivo de audio...Select Audio File... Publisher,Seleccionar archivo...Select File... Publisher&Seleccionar foto...Select Picture... Publisher(Seleccionar video...Select Video... Publisher<Selecciona un archivo de audioSelect one audio file Publisher*Selecciona un archivoSelect one file Publisher*Selecciona una imagenSelect one image Publisher<Selecciona un archivo de vdeoSelect one video file 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 PublisherAadir un ttulo ayuda a hacer el contenido del "Mientras tanto" ms informativo>Setting a title helps make the Meanwhile feed more informative PublisherComo lo que ests subiendo es una imagen, podras reducirla un poco o guardarla en un formato ms comprimido, como JPG.sSince you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. Publisher TamaoSize Publisher2Lamentamos las molestias.Sorry for the inconvenience. PublisherREl formato de audio no se puede detectar.$The audio format cannot be detected. PublisherPEl tipo de archivo no se puede detectar.!The file type cannot be detected. 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. PublisherXNo se puede acceder al archivo seleccionado:%The selected file cannot be accessed: PublisherREl formato de vdeo no se puede detectar.$The video format cannot be detected. PublisherEsto es una medida temporal, ya que los servidores no pueden establecer sus propios lmites todava.OThis is a temporary measure, since the servers cannot set their own limits yet. Publisher TtuloTitle PublisherPara...To... PublisherTipoType PublisherActualizarUpdate PublisherActualizando... Updating... PublisherJSubir multimedia, como fotos o vdeos%Upload media, like pictures or videos Publisher VdeoVideo Publisher"Archivos de vdeo Video files Publisher*Vdeo no seleccionado Video not set PublisherJAdvertencia: An no tienes seguidores"Warning: You have no followers yet PublisherNo puedes crear un mensaje para %1 en este momento, porque ya se est redactando un mensaje.YYou can't create a message for %1 at this time, because a post is already being composed. 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. PublisherNo puedes cargar un borrador en este momento, porque ya se est redactando un mensaje.NYou can't load a draft at this time, because a post is already being composed. PublisherbEs posible que no tengas los permisos necesarios.-You might not have the necessary permissions. PublisherEsts intentando publicar slo para tus seguidores, pero no tienes ningn seguidor todava.SYou're trying to post to your followers only, but you don't have any followers yet. PublisherbSe ha aadido a %1 (%2) a la lista correctamente.#%1 (%2) added to list successfully.PumpControllerdSe ha quitado a %1 (%2) de la lista correctamente.'%1 (%2) removed from list successfully.PumpController%1 intentos %1 attemptsPumpController2%1 comentarios recibidos.%1 comments received.PumpControllerSe ha publicado '%1' correctamente. Actualizando contenido del mensaje...3%1 published successfully. Updating post content...PumpController1 intento 1 attemptPumpController,1 comentario recibido.1 comment received.PumpControllerAccionesActionsPumpControllerActividadActivityPumpController,Aadiendo elementos...Adding items...PumpControllerHAadiendo una persona a una lista...Adding person to list...PumpControllerSe han recibido todos los datos iniciales. Inicializacin completada.3All initial data received. Initialization complete.PumpControllerHAplicacin autorizada correctamente.$Application authorized successfully.PumpController*Error de autorizacinAuthorization errorPumpController|Autorizado para usar la cuenta %1. Recibiendo datos iniciales.3Authorized to use account %1. Getting initial data.PumpController>Avatar publicado correctamente.Avatar published successfully.PumpControllerAvatar subido.Avatar uploaded.PumpController&Pasarela incorrecta Bad GatewayPumpController(Solicitud incorrecta Bad RequestPumpControllerPNo se puede seguir a %1 en este momento.Can't follow %1 at this time.PumpControllerZComprobando direccin %1 antes de seguirla...'Checking address %1 before following...PumpControllerLComentario %1 publicado correctamente.Comment %1 posted successfully.PumpControllerPComentario %1 actualizado correctamente. Comment %1 updated successfully.PumpController Creando grupo...Creating group...PumpController8Creando lista de personas...Creating person list...PumpController:Borrando lista de personas...Deleting person list...PumpController,E-mail actualizado: %1E-mail updated: %1PumpController*Error conectando a %1Error connecting to %1PumpControllerPError cargando la lnea temporal menor!Error loading minor feed!PumpControllerDError cargando la lnea temporal!Error loading timeline!PumpControllerFavoritos FavoritesPumpControllerfArchivo subido correctamente. Publicando mensaje....File uploaded successfully. Posting message...PumpControllerSeguidores FollowersPumpControllerSiguiendo FollowingPumpControllerDSiguiendo a %1 (%2) correctamente.Following %1 (%2) successfully.PumpControllerProhibido ForbiddenPumpControllerNTiempo de espera de la pasarela agotadoGateway TimeoutPumpController$Recibiendo '%1'...Getting '%1'...PumpControllerxRecibiendo identificador de autorizacin (token) de OAuth...Getting OAuth token...PumpControllerFRecibiendo una lista de personas...Getting a person list...PumpController2Recibiendo comentarios...Getting comments...PumpController8Recibiendo los "me gusta"...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 usuarios del servidor %1...Getting site users for %1...PumpController Ya no disponibleGonePumpController<Grupo %1 creado correctamente.Group %1 created successfully.PumpControllerPSe ha entrado al grupo %1 correctamente.Group %1 joined successfully.PumpControllerError HTTP HTTP errorPumpControllerError internoInternal Server ErrorPumpController*Unindose al grupo...Joining group...PumpController*Saliendo del grupo...Leaving group...PumpControllerPSe ha salido del grupo %1 correctamente.Left the %1 group successfully.PumpController>Se han recibido los "me gusta".Likes received.PumpControllerBLista de usuarios de %1 recibida.List of %1 users 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.PumpControllerCargando imagen externa de %1 a pesar de los errores SSL, como se ha configurado...ILoading external image from %1 regardless of SSL errors, as configured...PumpControllerMientras tanto MeanwhilePumpControllerMencionesMentionsPumpController@Mensaje eliminado correctamente.Message deleted successfully.PumpControllerlMensaje marcado o desmarcado "Me gusta" correctamente.&Message liked or unliked successfully.PumpControllerMensajesMessagesPumpController,Movido permanentementeMoved PermanentlyPumpController(Movido temporalmenteMoved TemporarilyPumpControllerNo encontrado Not FoundPumpControllerNo implementadoNot ImplementedPumpControllerlError de OAuth mientras se autorizaba a la aplicacin.*OAuth error while authorizing application.PumpController2Error de soporte de OAuthOAuth support errorPumpControllerVParte de la lista de 'seguidores' recibida.%Partial list of 'followers' received.PumpControllerTParte de la lista de 'siguiendo' recibida.%Partial list of 'following' received.PumpControllerXLista de personas '%1' creada correctamente.&Person list '%1' created successfully.PumpControllerPLista de personas borrada correctamente.!Person list deleted successfully.PumpController6Lista de personas recibida.Person list received.PumpControllerFMensaje %1 publicado correctamente.Post %1 published successfully.PumpControllerJMensaje %1 actualizado correctamente.Post %1 updated successfully.PumpControllerNMensaje de %1 compartido correctamente.Post by %1 shared successfully.PumpController Perfil recibido.Profile received.PumpController&Perfil actualizado.Profile updated.PumpController$Error de QOAuth %1QOAuth error %1PumpControllerPreparado.Ready.PumpController(Se ha recibido '%1'.Received '%1'.PumpControllerLQuitando a una persona de una lista...Removing person from list...PumpControllerHErrores de SSL en la conexin a %1!SSL errors in connection to %1!PumpController0Versin del servidor: %1Server version: %1PumpController,Servicio no disponibleService UnavailablePumpControllerBAlgunos datos iniciales no se han recibido tras varios intentos. Algo puede estar fallando en tu servidor. Es posible que puedas usar el servicio con normalidad.Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally.PumpControllerAlgunos datos iniciales no se han recibido. Reiniciando la inicializacin...@Some initial data was not received. Restarting initialization...PumpControllerrAn se est esperando el perfil. Intentndolo de nuevo...*Still waiting for profile. Trying again...PumpController^Se ha dejado de seguir a %1 (%2) correctamente.'Stopped following %1 (%2) successfully.PumpControllerLa aplicacin no est registrada con tu servidor todava. Registrando...FThe application is not registered with your server yet. Registering...PumpControllerLos comentarios de este mensaje no se pueden cargar debido a que faltan datos en el servidor.NThe comments for this post cannot be loaded due to missing data on the server.PumpControllerBNo hay ninguna cuenta autorizada.There is no authorized account.PumpControllerHa habido un error de OAuth mientras se intentaba obtener un identificador de autorizacin (token).EThere was an OAuth error while trying to get the authorization token.PumpControllerLnea temporalTimelinePumpController.Intentando seguir a %1.Trying to follow %1.PumpControllerPNo se ha podido verificar la direccin!Unable to verify the address!PumpControllerNo autorizado UnauthorizedPumpControllerLCdigo de error HTTP no gestionado: %1Unhandled HTTP error code %1PumpController\Mensaje sin ttulo %1 publicado correctamente.(Untitled post %1 published successfully.PumpController`Mensaje sin ttulo %1 actualizado correctamente.&Untitled post %1 updated successfully.PumpController,Actualizando perfil...Updating profile...PumpControllerSubiendo %1 Uploading %1PumpController2Lnea temporal de usuario User timelinePumpControllerBEsperando contrasea del proxy...Waiting for proxy password...PumpControllerProbablemente necesites instalar el conector de OpenSSL para QCA: %1, %2 o similar.KYou probably need to install the OpenSSL plugin for QCA: %1, %2 or similar.PumpControllerTu instalacin de QOAuth, una biblioteca usada por Dianara, no parece tener soporte de HMAC-SHA1._Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support.PumpController"%1 usuarios en %2%1 users in %2 SiteUsersListCerrar la lista Close list SiteUsersListPRecibir lista de usuarios de tu servidor"Get list of users from your server SiteUsersListlLista de los seguidores de la cuenta Pump.io Community3List of Followers for the Pump.io Community account SiteUsersListCargando... Loading... SiteUsersListJMs recursos para encontrar usuarios:More resources to find users: SiteUsersListnServicio PPump de bsqueda de usuarios en inventati.org*PPump user search service at inventati.org SiteUsersListBPgina wiki 'Usuarios por idioma'Wiki page 'Users by language' SiteUsersListPuedes recibir una lista de los usuarios ms nuevos registrados en tu servidor, haciendo clic en el botn de abajo.^You can get a list of the newest users registered on your server by clicking the button below. SiteUsersListr%1 mensajes ms pendientes para la prxima actualizacin.&%1 more posts pending for next update.TimeLine*%1 mensajes en total.%1 posts in total.TimeLineNo se puede actualizar '%1' porque se est editando un comentario.E'%1' cannot be updated because a comment is currently being composed.TimeLine6Lnea 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.TimeLineHaz clic aqu o pulsa Control+G para saltar a una pgina especfica8Click here or press Control+G to jump to a specific pageTimeLineHHaz clic aqu para recibirlos ahora.Click here to receive them now.TimeLineJDianara es un cliente <b>Pump.io</b>.#Dianara is a Pump.io client.TimeLineBlog de DianaraDianara's blogTimeLineFLnea temporal de mensajes directosDirect Messages TimelineTimeLine6Lnea 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.TimeLinenAqu vers los mensajes dirigidos especficamente a ti.4Here, you'll see posts specifically directed to you.TimeLineSi an no tienes una cuenta Pump, puedes conseguir una en la siguiente direccin, por ejemplo:]If you don't have a Pump account yet, you can get one at the following address, for instance:TimeLineCargando... Loading...TimeLineMs nuevosNewerTimeLineLo ms nuevoNewestTimeLineMs antiguosOlderTimeLine Pgina %1 de %2.Page %1 of %2.TimeLineTMensajes y comentarios que te han gustado. Posts and comments you've liked.TimeLinejPulsa <b>F1</b> si quieres abrir la ventana de ayuda.4Press F1 if you want to open the Help window.TimeLine4Gua de usuario de Pump.ioPump.io User GuideTimeLineSolicitando... Requesting...TimeLineBMostrando %1 mensajes por pgina.Showing %1 posts per page.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.TimeLineNo hay mensajesThere are no postsTimeLinebHay descripciones emergentes (tooltips) por todas partes, por lo que si mantienes el ratn sobre un botn o un campo de texto, es probable que veas alguna informacin adicional.There are tooltips everywhere, so if you hover over 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 minuto A minute ago TimestampHace un mes A month ago TimestampHace un ao A year ago TimestampHace una hora An hour ago TimestampEn el futuro In the future Timestamp,Hora/fecha no vlida!Invalid timestamp! TimestampAhora mismoJust now TimestampAyer Yesterday Timestamp%1 mensajes%1 posts UserPosts&Cerrar&Close UserPosts@Error cargando la lnea temporalError loading the timeline UserPostsCargando... Loading... UserPostsMensajes de %1 Posts by %1 UserPosts(Se ha recibido '%1'.Received '%1'. UserPostsdianara-v1.4.1/translations/dianara_pl.ts0000644000175000017500000063215413212033716016600 0ustar janjan ASActivity Public %1 by %2 1=kind of object: note, comment, etc; 2=author's name ASObject Note Noun, an object type Article Noun, an object type Image Noun, an object type Audio Noun, an object type Video Noun, an object type File Noun, an object type Comment Noun, as in object type: a comment Group Noun, an object type Collection Noun, an object type Other As in: other type of post No detailed location Deleted on %1 and one other and %1 others ASPerson Hometown Miasto AccountDialog Account Configuration Konfiguracja konta First, enter your Webfinger ID, your pump.io address. Najpierw podaj swój Webfinger ID, czyli adres pump.io. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. Twój adres wygląda mniej więcej tak: użytkownik@serwerpump.org. Możesz go znaleźć na stronie serwera, w swoim profilu. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example Jeżeli twój profil znajduje się pod adresem https://pump.przykład/twojeimię, twój adres to twojeimię@pump.przykład If you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. 1=link to website If you need help: %1 Pump.io User Guide Your Pump.io address: Twój adres pump.io: Get &Verifier Code Pobierz kod &weryfikacyjny After clicking this button, a web browser will open, requesting authorization for Dianara Po kliknięciu tego przycisku otworzy się przeglądarka i poprosi o dostęp dla Dianary 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! Gdy autoryzujesz Dianarę ze strony serwera Pump, otrzymasz kod weryfikacyjny VERIFIER. Skopiuj go i wklej w poniższe pole. Verifier code: Kod weryfikacyjny: &Authorize Application &Autoryzuj program &Save Details Zapi&sz informacje If the browser doesn't open automatically, copy this address manually Jeżeli przeglądarka nie otworzy się automatycznie, wejdź na ten adres Unable to open web browser! &Cancel &Anuluj Your address, like username@pumpserver.org Enter or paste the verifier code provided by your Pump server here Your account is properly configured. Press Unlock if you wish to configure a different account. &Unlock A web browser will start now, where you can get the verifier code Otworzy się teraz przeglądarka, z której uzyskasz kod weryfikacyjny Your Pump address is invalid Twój adres Pump jest nieprawidłowy Verifier code is empty Nie podałeś kodu weryfikacyjnego Dianara is authorized to access your data Dianara uzyskała autoryzację do dostępu do twoich danych 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. ON THE LEFT should change to ON THE RIGHT in RTL languages Clear &List &Done &Cancel &Anuluj Public Followers Lists People... Selected People AvatarButton Open %1's profile in web browser Otwórz profil użytkownika %1 w przeglądarce Open your profile in web browser Otwórz swój profil w przeglądarce Send message to %1 Browse messages Stop following Przestań obserwować Follow Obserwuj Stop following? Na pewno przestać obserwować? Are you sure you want to stop following %1? Czy na pewno przestać obserwować użytkownika %1? &Yes, stop following &Tak, przestań obserwować &No &Nie BannerNotification Timelines were not automatically updated to avoid interruptions. This happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you read Update now Hide this message ColorPicker Change... Choose a color Comment Like or unlike this comment Quote This is a verb, infinitive Infinitive verb doesn't really work here Cytuj Reply quoting this comment Delete Usuń Posted on %1 Modified on %1 Edit Modify this comment Erase this comment Unlike Like %1 like this comment Plural: %1=list of people like John, Jane, Smith %1 polubili ten komentarz %1 likes this comment Singular: %1=name of just 1 person %1 polubił ten komentarz WARNING: Delete comment? UWAGA: Usunąć komentarz? Are you sure you want to delete this comment? Czy na pewno usunąć ten komentarz? &Yes, delete it &Tak, usuń go &No &Nie CommenterBlock You can press Control+Enter to send the comment with the keyboard Możesz wdusić Control+Enter aby wysłać komentarz przy użyciu klawiatury Comment Infinitive verb Reload comments Cancel Press ESC to cancel the comment if there is no text Wciśnij Escape aby anulować komentarz jeżeli nie ma tekstu Check for comments Show all %1 comments Comments are not available Error: Already composing You can't edit a comment at this time, because another comment is already being composed. Editing comment Loading comments... Posting comment failed. Try again. Błąd przy wysyłaniu komentarza. Spróbuj ponownie. An error occurred Sending comment... Wysyłanie komentarza... Updating comment... Comment is empty. Komentarz jest pusty. Composer Click here or press Control+N to post a note... Kliknij tutaj albo wciśnij Control+N aby zapostować... Symbols Symbole Formatting Formatowanie Normal Zwykły Bold Pogrubienie Italic Kursywa Underline Podkreślenie Strikethrough Przekreślenie Header Nagłówek List Table Preformatted block Clumsy, need tweaking Wcześniej sformatowany blok tekstu Quote block Cytat Make a link Dodaj odnośnik Insert an image from a web site Wstaw obrazek z adresu internetowego Insert line Wstaw linię &Format Button for text formatting and related options &Formatowanie Type a comment here Wpisz tutaj You can attach only one file. You cannot drop folders here, only a single file. Insert as image? The link you are pasting seems to point to an image. Insert as visible image Insert as link Table Size How many rows (height)? How many columns (width)? Invalid link The text you entered does not look like a link. It should start with one of these types: It = the link, from previous sentence &Use it anyway &Enter it again &Cancel link Error: Invalid URL Błąd: Niepoprawny URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Wpisany adres (%1) jest niepoprawny. Adresy do obrazków muszą zaczynać się od http:// lub https:// Yes, but saving a &draft Text Formatting Options Opcje formatowania tekstu Paste Text Without Formatting Wklej tekst bez formatowania Type a message here to post it Insert a link Type or paste a web address here. You could also select some text first, to turn it into a link. 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. The link must point to the image file directly. Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No &Nie ConfigDialog Program Configuration Konfiguracja programu minutes minut Timeline &update interval Częstotliwość akt& &ualizacji osi czasu posts Goes after a number, as: 25 posts postów &Posts per page, main timeline Wyświetlanie &postów na stronie (w głównej osi czasu) posts This goes after a number, like: 10 posts postów Posts per page, &other timelines Top Bottom &Tabs position &Movable tabs characters This is a suffix, after a number Snippet limit Post Titles Post Contents Minor Feeds Only for images inserted from web sites. Use with care. Use attachment filename as initial post title Inform only the author when liking things System Tray Icon &Type Timelines Posts Notifications System Tray Dianara stores data in this folder: &Save Configuration Public posts as &default Pro&xy Settings Network configuration Set Up F&ilters Filtering rules Highlighted activities, except mine Any highlighted activity Always Never Comments Left side tabs on left side/west; RTL not affected Right side tabs on right side/east; RTL not affected You are among the recipients of the activity, such as a comment addressed to you. Used also when highlighting posts addressed to you in the timelines. The activity is in reply to something done by you, such as a comment posted in reply to one of your notes. You are the object of the activity, such as someone adding you to a list. The activity is related to one of your objects, such as someone liking one of your posts. Used also when highlighting your own posts in the timelines. Item highlighted due to filtering rules. Item is new. Show snippets in minor feeds Show information for deleted posts No Before avatar Before avatar, subtle After avatar After avatar, subtle Hide duplicated posts Jump to new posts line on update Snippet limit when highlighted Minor feed avatar sizes Show activity icons Avatar size Avatar size in comments Show extended share information Show extra information Highlight post author's comments Highlight your own comments Ignore SSL errors in images Show full size images Show character counter Don't inform followers when following someone Don't inform followers when handling lists As system notifications Using own notifications Don't show notifications seconds Next to a duration, in seconds Notification Style Duration Persistent Notifications Also highlight taskbar entry Notify when receiving: New posts Highlighted posts New activities in minor feed Highlighted activities in minor feed Important errors Default System iconset, if available Show your current avatar Custom icon S&elect... Custom &Icon Hide window on startup General Options Fonts Colors Composer Privacy This is a system notification System notifications are not available! Own notifications will be used. This is a basic notification Select custom icon Image files Pliki graficzne All files Wszystkie pliki Invalid image Niepoprawny obrazek The selected image is not valid. Wybrany obrazek nie jest poprawny. &Cancel &Anuluj ContactCard Hometown Miasto Joined: %1 Updated: %1 Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name Opis %1 This user doesn't have a biography No biography for %1 %1=contact name Brak opisu dla %1 Open Profile in Web Browser Send Message Browse Messages User Options Follow Obserwuj Stop Following Stop following? Na pewno przestać obserwować? Are you sure you want to stop following %1? Czy na pewno przestać obserwować użytkownika %1? &Yes, stop following &Tak, przestań obserwować &No &Nie ContactList Type a partial name or ID to find a contact... F&ull List ContactManager username@server.org or https://server.org/username &Enter address to follow: &Follow Reload Followers Reload Following Export Followers Export Following Reload Lists &Neighbors Follo&wers Followin&g &Lists Export list of 'following' to a file Export list of 'followers' to a file Cannot export to this file: Please enter another file name, or choose a different folder. The server seems to be a Pump server, but the account does not exist. %1 doesn't seem to be a Pump server. %1 is a hostname Following this account at this time will probably not work. The %1 server seems unavailable. %1 is a hostname Unknown Refers to server version Error The user address %1 does not exist, or the %2 server is down. Server version Check the address, and keep in mind that usernames are case-sensitive. Do you want to try following this address anyway? (not recommended) Yes, follow anyway No, cancel About to follow %1... DownloadWidget Open Verb, as in: Open the downloaded file Download Save the attached file to your folders Cancel Save File As... All files Wszystkie pliki File not found! Abort download? Do you want to stop downloading the attached file? &Yes, stop &No, continue Download aborted Download completed Attachment downloaded successfully to %1 %1 = filename Open the downloaded attachment with your system's default program for this type of file. Download failed Downloading attachment failed: %1 %1 = filename Downloading %1 KiB... %1 KiB downloaded DraftsManager Draft Manager Load Save Manage drafts... &Delete selected draft &Close Untitled draft Delete draft? Are you sure you want to delete this draft? &Yes, delete it &Tak, usuń go &No &Nie EmailChanger Change E-mail Address Change Zmień &Cancel &Anuluj E-mail Address: Again: Your Password: E-mail addresses don't match! Password is empty! FDNotifications Show FilterEditor Filter Editor %1 if %2 contains: %3 This explains a filter rule, like: Hide if Author ID contains JohnDoe Here you can set some rules for hiding or highlighting stuff. 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. You could also highlight messages that contain your name. Hide Highlight Post Contents Author ID Application Activity Description Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel &Anuluj if contains &New Filter C&urrent Filters FilterMatchesWidget Content The contents of the post matched Author App Application, short if possible Description FirstRunWizard Welcome Wizard Welcome to Dianara! This wizard will help you get started. You can access this window again at any time from the Help menu. The first step is setting up your account, by using the following button: Configure your &account Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already. &Edit your profile By default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes. Post to &Public by default Open general program &help window &Show this again next time Dianara starts &Close FontPicker Change... Choose a font HelpWidget Basic Help Getting started Settings Timelines Posting Managing contacts Keyboard controls Command line options Contents The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. At this point, your profile, contact lists and timelines will be loaded. You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. If you're new to Pump.io, take a look at this guide: Pump.io User Guide You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. There are seven timelines: The main timeline, where you'll see all the stuff posted or shared by the people you follow. Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. Activity timeline, where you'll see your own posts, or posts shared by you. Favorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. LEFT SIDE should change to RIGHT SIDE on RTL languages The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. It is possible to attach images, audio, video, and general files, like PDF documents, to your post. You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. You can select who will see your post by using the To and Cc buttons. If you add a specific person to the 'To' list, they will receive your message in their direct messages tab. You can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. Choose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. You can see the lists of people you follow, and who follow you from the Contacts tab. There, you can also manage person lists, used mainly to send posts to specific groups of people. There is a text field at the top, where you can directly enter addresses of new contacts to follow them. Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. You can also send a direct message (initially private) to that contact from this menu. You can find a list with some Pump.io users and other information here: Users by language Followers of Pump.io Community account The most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. Besides that, you can use: Control+Up/Down/PgUp/PgDown/Home/End to move around the timeline. Control+Left/Right to jump one page in the timeline. Control+G to go to any page in the timeline directly. Control+1/2/3 to switch between the minor feeds. Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. Use the --debug parameter to have extra information in your terminal window, about what the program is doing. If your server does not support HTTPS, you can use the --nohttps parameter. Dianara offers a D-Bus interface that allows some control from other applications. The interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. &Close ImageViewer Untitled Image &Save As... &Restart Restart animation Fit As in: fit image to window Rotate image to the left RTL: This actually means LEFT, anticlockwise Rotate image to the right RTL: This actually means RIGHT, clockwise &Close Save Image... Close Viewer Downloading full image... Error downloading image! Try again later. Save Image As... Image files Pliki graficzne All files Wszystkie pliki 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 Type a name for the new list... Type an optional description here Create L&ist &Add to List WARNING: Delete list? Are you sure you want to delete %1? 1=Name of a person list &Yes, delete it &Tak, usuń go &No &Nie 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 LogViewer Log Clear &Log &Close MainWindow The main timeline Główna oś czasu Messages sent explicitly to you Wiadomości wysłane bezpośrednio do ciebie Your own posts Twoje własne wiadomości Your favorited posts Twoje ulubione wiadomości &Contacts &Kontakty Initializing... Inicjalizacja... Your account is not configured yet. Twoje konto nie zostało jeszcze skonfigurowane. Dianara started. Dianara wystartowała. The people you follow, the ones who follow you, and your person lists Running with Qt v%1. &Session &Sesja Auto-update &Timelines Mark All as Read Oznacz wszystkie jako przeczytane &Post a Note Wyślij &wiadomość &Quit &Zakończ &View &Widok Side &Panel Panel &boczny &Toolbar Status &Bar Pasek &stanu Full &Screen Pełny &ekran &Log &Dziennik wiadomości S&ettings &Ustawienia Edit &Profile Edytuj &profil &Configure Dianara &Konfiguracja Dianary &Help &Pomoc Basic &Help Visit &Website Odwiedź &stronę www Some Pump.io &Tips List of Some Pump.io &Users Pump.io &Network Status Website About &Dianara &O Dianarze Toolbar Open the log viewer Auto-updating enabled Auto-updating disabled Proxy password required You have configured a proxy server with authentication, but the password is not set. Enter the password for your proxy server: Your biography is empty Click to edit your profile Starting automatic update of timelines, once every %1 minutes. Stopping automatic update of timelines. Received %1 older posts in '%2'. %1 is a number, %2 = name of a timeline 1 highlighted singular, refers to a post %1 highlighted plural, refers to posts Direct messages By filters 1 more pending to receive. singular, one post %1 more pending to receive. plural, several posts Also: Last update: %1 '%1' updated. %1 is the name of a feed Click here to configure your account Locked Panels and Toolbars Side Panel Panel boczny Show Welcome Wizard 1 filtered out. singular, refers to a post %1 filtered out. plural, refers to posts 1 deleted. singular, refers to a post %1 deleted. plural, refers to posts No new posts. Favor&ites Your Pump.io account is not configured Received %1 older activities in '%2'. %1 is a number, %2 = name of feed 1 more pending to receive. singular, 1 activity %1 more pending to receive. plural, several activities 1 filtered out. singular, refers to one activity %1 filtered out. plural, several activities Error storing image! %1 bytes Marking everything as read... Dianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. Closing due to environment shutting down... Shutting down Dianara... System tray icon is not available. Dianara cannot be hidden in the system tray. Do you want to close the program completely? Minor feed updated at %1. Update %1 There is 1 new activity. There are %1 new activities. 1 highlighted. singular, refers to an activity %1 highlighted. plural, refers to activities No new activities. Link to: %1 With Dianara you can see your timelines, create new posts, upload pictures and other media, 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. If there was another translator before you, add your name after theirs ;) Autor polskiego tłumaczenia: Łukasz "Cyber Killer" Korpalski i Derping Muffins. &Hide Window &Show Window &Pokaż okno Quit? You are composing a note or a comment. Do you really want to close Dianara? &Yes, close the program &No &Nie &Account There is 1 new post. Jest 1 nowa wiadomość. There are %1 new posts. Są %1 nowe wiadomości. Minor activities done by everyone, such as replying to posts Minor activities addressed to you Minor activities done by you Press F1 for help &Filters and Highlighting Report a &Bug Pump.io User &Guide Timeline updated at %1. Oś czasu zaktualizowana o %1. Total posts: %1 &Timeline &Oś czasu &Messages &Wiadomości &Activity &Aktywność About Dianara O Dianarze Dianara is a pump.io social networking client. Dianara jest klientem sieci społecznościowej pump.io. MinorFeed Older Activities Get previous minor activities There are no activities to show yet. Get %1 newer As in: Get 3 newer (activities) MinorFeedItem Using %1 Application used to generate this activity To: %1 1=people to whom this activity was sent Cc: %1 1=people to whom this activity was sent as CC Open referenced post MiscHelpers bytes Error: Unable to launch browser The default system web browser could not be executed. You might need to install the XDG utilities. PageSelector Jump to page Page number: &First As in: first page &Last As in: last page Newer As in: newer pages Older As in: older pages &Go &Cancel &Anuluj PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel &Anuluj Post Via %1 Type As in: type of object Posted on %1 1=Date Modified on %1 To Using %1 1=Program used for posting or sharing Share Delete Usuń Loading image... Attached File %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 Click to download the attachment Post Noun, not verb Shared on %1 Open post in web browser Copy post link to clipboard Normalize text colors In If you select some text, it will be quoted. Unshare Unshare this post Edit Comment verb, for the comment button Cc &Close Parent As in 'Open the parent post'. Try to use the shortest word! Open the parent post, to which this one replies Reply to this post. Share this post with your contacts Modify this post Erase this post Join Group %1 members in the group Image is animated. Click on it to play. Size Image size (resolution) Couldn't load image! Attached Audio Attached Video Shared once Shared %1 times Edited: %1 You like this Unlike Like this post Like Are you sure you want to share your own post? Share post? Do you want to share %1's post? &Yes, share it &No &Nie 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 &Tak, usuń go Click the image to see it in full size ProfileEditor Profile Editor Edytor profilu This is your Pump address To jest twój adres Pump This is the e-mail address associated with your account, for things such as notifications and password recovery To jest adres e-mail powiązany z twoim kontem, używany do powiadomień i odzyskiwania hasła Change &E-mail... Change &Avatar... Zmień &awatar This is your visible name To jest twoje wyświetlane imię Changing your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. &Save Profile &Zapisz profil &Cancel &Anuluj Webfinger ID E-mail Avatar Awatar Full &Name Pełne &imię &Hometown Miasto &Bio &Coś o tobie Not set In reference to the e-mail not being set for the account Nie ustawiony Select avatar image Wybierz obrazek Image files Pliki graficzne All files Wszystkie pliki Invalid image Niepoprawny obrazek The selected image is not valid. Wybrany obrazek nie jest poprawny. ProxyDialog Proxy Configuration Do not use a proxy Your proxy username Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. &Save &Cancel &Anuluj Proxy &Type &Hostname &Port Use &Authentication &User Pass&word Publisher Select Picture... Find the picture in your folders Title To... Select who will see this post Select who will get a copy of this post Setting a title helps make the Meanwhile feed more informative Remove Cancel the attachment, and go back to a regular note Drafts Cc... Picture Audio Video Other as in other kinds of files Ad&d... Upload media, like pictures or videos Hit Control+Enter to post with the keyboard Cancel Cancel the post Picture not set Select Audio File... Find the audio file in your folders Audio file not set Select Video... Find the video in your folders Video not set Select File... Find the file in your folders File not set File not found. Error: Already composing You can't edit a post at this time, because a post is already being composed. Update You can't create a message for %1 at this time, because a post is already being composed. Draft loaded. ERROR: Already composing You can't load a draft at this time, because a post is already being composed. Draft saved. Posting failed. Try again. Warning: You have no followers yet You're trying to post to your followers only, but you don't have any followers yet. If you post like this, no one will be able to see your message. Do you want to make the post public instead of followers-only? &Yes, make it public &Cancel, go back to the post Posting... Updating... Post is empty. File not selected. Select one image Image files Pliki graficzne Select one file Invalid file The file type cannot be detected. All files Wszystkie pliki Since you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. File is too big Dianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. This is a temporary measure, since the servers cannot set their own limits yet. Sorry for the inconvenience. Error The selected file cannot be accessed: It is owned by %1. %1 = a username You might not have the necessary permissions. Resolution Image resolution (size) Type Size %1 KiB of %2 KiB uploaded Invalid image Niepoprawny obrazek Add a brief title for the post here (recommended) Post verb Note started from another application. Ignoring new note request from another application. Editing post. &No, post to my followers only The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Select one audio file Audio files Invalid audio file The audio format cannot be detected. Select one video file Video files Invalid video file The video format cannot be detected. PumpController Getting list of 'Following'... Getting list of 'Followers'... Getting site users for %1... %1 is a server name Getting list of person lists... Creating person list... Deleting person list... Getting a person list... Adding person to list... Removing person from list... Creating group... Joining group... Leaving group... Getting likes... Getting comments... Getting '%1'... %1 is the name of a feed Timeline Messages User timeline Uploading %1 1=filename Error loading timeline! Error loading minor feed! Unable to verify the address! HTTP error For the following HTTP error codesyou can check http://en.wikipedia.org/wiki/List_of_HTTP_status_codes in your language Gateway Timeout HTTP 504 error string Service Unavailable HTTP 503 error string Bad Gateway HTTP 502 error string Not Implemented HTTP 501 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 Server version: %1 Profile received. Followers Following Profile updated. E-mail updated: %1 Avatar published successfully. Message liked or unliked successfully. 1 comment received. %1 comments received. Post by %1 shared successfully. 1=author of the post we are sharing Received '%1'. %1 is the name of a feed Adding items... Message deleted successfully. List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. List of %1 users received. %1 is a server name Person list deleted successfully. Person list received. File uploaded successfully. Posting message... Avatar uploaded. OAuth error while authorizing application. %1 attempts 1 attempt Some initial data was not received. Restarting initialization... Can't follow %1 at this time. %1 is a user ID Trying to follow %1. %1 is a user ID Checking address %1 before following... Likes received. Authorized to use account %1. Getting initial data. There is no authorized account. Updating profile... The comments for this post cannot be loaded due to missing data on the server. Activity Favorites Meanwhile Mentions Actions %1 published successfully. Updating post content... %1 is the type of object: note, image... Untitled post %1 published successfully. %1 is a piece of the post Post %1 published successfully. %1 is the title of the post Untitled post %1 updated successfully. %1 is a piece of the post Post %1 updated successfully. %1 is the title of the post Comment %1 updated successfully. %1 is a piece of the comment Comment %1 posted successfully. %1 is a piece of the comment Following %1 (%2) successfully. %1 is a person's name, %2 is the ID Stopped following %1 (%2) successfully. %1 is a person's name, %2 is the ID List of 'lists' received. Person list '%1' created successfully. %1 (%2) added to list successfully. 1=contact name, 2=contact ID %1 (%2) removed from list successfully. 1=contact name, 2=contact ID Group %1 created successfully. Group %1 joined successfully. Left the %1 group successfully. SSL errors in connection to %1! Loading external image from %1 regardless of SSL errors, as configured... %1 is a hostname The application is not registered with your server yet. Registering... Getting OAuth token... OAuth support error Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support. You probably need to install the OpenSSL plugin for QCA: %1, %2 or similar. Authorization error There was an OAuth error while trying to get the authorization token. QOAuth error %1 Application authorized successfully. Waiting for proxy password... Still waiting for profile. Trying again... Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally. All initial data received. Initialization complete. Ready. SiteUsersList You can get a list of the newest users registered on your server by clicking the button below. More resources to find users: Wiki page 'Users by language' PPump user search service at inventati.org List of Followers for the Pump.io Community account Get list of users from your server Close list Loading... %1 users in %2 %1 = user count, %2 = server name TimeLine Welcome to Dianara 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. Dianara's blog If you don't have a Pump account yet, you can get one at the following address, for instance: Dianara is a <b>Pump.io</b> client. Press <b>F1</b> if you want to open the Help window. There are tooltips everywhere, so if you hover over a button or a text field with your mouse, you'll probably see some extra information. Pump.io User Guide 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. Newest Newer Older Requesting... Loading... Page %1 of %2. Showing %1 posts per page. %1 posts in total. Click here or press Control+G to jump to a specific page '%1' cannot be updated because a comment is currently being composed. %1 = feed's name %1 more posts pending for next update. Click here to receive them now. There are no posts Timestamp Invalid timestamp! Zły czas! A minute ago Minutę temu %1 minutes ago %1 minut temu An hour ago Godzinę temu %1 hours ago %1 godzin temu Just now In the future Yesterday %1 days ago %1 dni temu A month ago Miesiąc temu %1 months ago %1 miesięcy temu A year ago Rok temu %1 years ago %1 lat temu UserPosts Posts by %1 Loading... &Close Received '%1'. %1 posts Error loading the timeline dianara-v1.4.1/translations/dianara_EMPTY.ts0000644000175000017500000062617113212033716017065 0ustar janjan ASActivity Public %1 by %2 1=kind of object: note, comment, etc; 2=author's name ASObject Note Noun, an object type Article Noun, an object type Image Noun, an object type Audio Noun, an object type Video Noun, an object type File Noun, an object type Comment Noun, as in object type: a comment Group Noun, an object type Collection Noun, an object type Other As in: other type of post No detailed location Deleted on %1 and one other and %1 others ASPerson Hometown 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 If you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. 1=link to website If you need help: %1 Pump.io User Guide Your Pump.io address: Your address, like username@pumpserver.org Enter or paste the verifier code provided by your Pump server here 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: &Authorize Application &Save Details &Cancel Your account is properly configured. Press Unlock if you wish to configure a different account. &Unlock 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 If the browser doesn't open automatically, copy this address manually Unable to open web browser! 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. ON THE LEFT should change to ON THE RIGHT in RTL languages Clear &List &Done &Cancel Public Followers Lists People... Selected People AvatarButton Open %1's profile in web browser Open your profile in web browser Send message to %1 Browse messages Stop following Follow Stop following? Are you sure you want to stop following %1? &Yes, stop following &No BannerNotification Timelines were not automatically updated to avoid interruptions. This happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you read Update now Hide this message ColorPicker Change... Choose a color Comment Posted on %1 Modified on %1 Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Edit Modify this comment Delete Erase this comment Unlike Like %1 likes this comment Singular: %1=name of just 1 person %1 like this comment Plural: %1=list of people like John, Jane, Smith WARNING: Delete comment? Are you sure you want to delete this comment? &Yes, delete it &No CommenterBlock Reload comments Comment Infinitive verb You can press Control+Enter to send the comment with the keyboard Cancel Press ESC to cancel the comment if there is no text Check for comments Show all %1 comments Comments are not available Error: Already composing You can't edit a comment at this time, because another comment is already being composed. Editing comment Loading comments... Posting comment failed. Try again. An error occurred Sending comment... Updating comment... Comment is empty. Composer Click here or press Control+N to post a note... Symbols Formatting Normal Bold Italic Underline Strikethrough Header List Table 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 You can attach only one file. You cannot drop folders here, only a single file. Insert as image? The link you are pasting seems to point to an image. Insert as visible image Insert as link Table Size How many rows (height)? How many columns (width)? Insert a link Type or paste a web address here. You could also select some text first, to turn it into a link. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Invalid link The text you entered does not look like a link. It should start with one of these types: It = the link, from previous sentence &Use it anyway &Enter it again &Cancel link Insert an image from a URL Type or paste the image address here. The link must point to the image file directly. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Yes, but saving a &draft Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog Program Configuration minutes Timeline &update interval Left side tabs on left side/west; RTL not affected Right side tabs on right side/east; RTL not affected Minor Feeds Show snippets in minor feeds Only for images inserted from web sites. Use with care. Public posts as &default Top Bottom &Tabs position &Movable tabs Pro&xy Settings Network configuration Set Up F&ilters Filtering rules Post Titles Post Contents Comments You are among the recipients of the activity, such as a comment addressed to you. Used also when highlighting posts addressed to you in the timelines. The activity is in reply to something done by you, such as a comment posted in reply to one of your notes. You are the object of the activity, such as someone adding you to a list. The activity is related to one of your objects, such as someone liking one of your posts. Used also when highlighting your own posts in the timelines. Item highlighted due to filtering rules. Item is new. 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 Highlighted activities, except mine Any highlighted activity Always Never characters This is a suffix, after a number Snippet limit Show information for deleted posts No Before avatar Before avatar, subtle After avatar After avatar, subtle Hide duplicated posts Jump to new posts line on update Snippet limit when highlighted Minor feed avatar sizes Show activity icons Avatar size Avatar size in comments Show extended share information Show extra information Highlight post author's comments Highlight your own comments Ignore SSL errors in images Show full size images Use attachment filename as initial post title Show character counter Don't inform followers when following someone Don't inform followers when handling lists Inform only the author when liking things As system notifications Using own notifications Don't show notifications seconds Next to a duration, in seconds Notification Style Duration Persistent Notifications Also highlight taskbar entry Notify when receiving: New posts Highlighted posts New activities in minor feed Highlighted activities in minor feed Important errors Default System iconset, if available Show your current avatar Custom icon System Tray Icon &Type S&elect... Custom &Icon Hide window on startup General Options Fonts Colors Timelines Posts Composer Privacy Notifications System Tray Dianara stores data in this folder: &Save Configuration &Cancel This is a system notification System notifications are not available! Own notifications will be used. This is a basic notification Select custom icon Image files All files Invalid image The selected image is not valid. ContactCard Hometown Joined: %1 Updated: %1 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 Send Message Browse Messages User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList Type a partial name or ID to find a contact... F&ull List ContactManager username@server.org or https://server.org/username &Enter address to follow: &Follow Reload Followers Reload Following Export Followers Export Following Reload Lists &Neighbors Follo&wers Followin&g &Lists Export list of 'following' to a file Export list of 'followers' to a file Cannot export to this file: Please enter another file name, or choose a different folder. The server seems to be a Pump server, but the account does not exist. %1 doesn't seem to be a Pump server. %1 is a hostname Following this account at this time will probably not work. The %1 server seems unavailable. %1 is a hostname Unknown Refers to server version Error The user address %1 does not exist, or the %2 server is down. Server version Check the address, and keep in mind that usernames are case-sensitive. Do you want to try following this address anyway? (not recommended) Yes, follow anyway No, cancel About to follow %1... DownloadWidget Open Verb, as in: Open the downloaded file Download Save the attached file to your folders Cancel Save File As... All files File not found! Abort download? Do you want to stop downloading the attached file? &Yes, stop &No, continue Download aborted Download completed Attachment downloaded successfully to %1 %1 = filename Open the downloaded attachment with your system's default program for this type of file. Download failed Downloading attachment failed: %1 %1 = filename Downloading %1 KiB... %1 KiB downloaded DraftsManager Draft Manager Load Save Manage drafts... &Delete selected draft &Close Untitled draft Delete draft? Are you sure you want to delete this draft? &Yes, delete it &No EmailChanger Change E-mail Address Change &Cancel E-mail Address: Again: Your Password: E-mail addresses don't match! Password is empty! FDNotifications Show FilterEditor Filter Editor %1 if %2 contains: %3 This explains a filter rule, like: Hide if Author ID contains JohnDoe Here you can set some rules for hiding or highlighting stuff. 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. You could also highlight messages that contain your name. Hide Highlight Post Contents Author ID Application Activity Description Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel if contains &New Filter C&urrent Filters FilterMatchesWidget Content The contents of the post matched Author App Application, short if possible Description FirstRunWizard Welcome Wizard Welcome to Dianara! This wizard will help you get started. You can access this window again at any time from the Help menu. The first step is setting up your account, by using the following button: Configure your &account Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already. &Edit your profile By default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes. Post to &Public by default Open general program &help window &Show this again next time Dianara starts &Close FontPicker Change... Choose a font HelpWidget Basic Help Getting started Settings Timelines Posting Managing contacts Keyboard controls Command line options Contents The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. At this point, your profile, contact lists and timelines will be loaded. You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. If you're new to Pump.io, take a look at this guide: Pump.io User Guide You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. There are seven timelines: The main timeline, where you'll see all the stuff posted or shared by the people you follow. Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. Activity timeline, where you'll see your own posts, or posts shared by you. Favorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. LEFT SIDE should change to RIGHT SIDE on RTL languages The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. It is possible to attach images, audio, video, and general files, like PDF documents, to your post. You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. You can select who will see your post by using the To and Cc buttons. If you add a specific person to the 'To' list, they will receive your message in their direct messages tab. You can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. Choose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. You can see the lists of people you follow, and who follow you from the Contacts tab. There, you can also manage person lists, used mainly to send posts to specific groups of people. There is a text field at the top, where you can directly enter addresses of new contacts to follow them. Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. You can also send a direct message (initially private) to that contact from this menu. You can find a list with some Pump.io users and other information here: Users by language Followers of Pump.io Community account The most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. Besides that, you can use: Control+Up/Down/PgUp/PgDown/Home/End to move around the timeline. Control+Left/Right to jump one page in the timeline. Control+G to go to any page in the timeline directly. Control+1/2/3 to switch between the minor feeds. Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. Use the --debug parameter to have extra information in your terminal window, about what the program is doing. If your server does not support HTTPS, you can use the --nohttps parameter. Dianara offers a D-Bus interface that allows some control from other applications. The interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. &Close ImageViewer Untitled Image &Save As... &Restart Restart animation Fit As in: fit image to window Rotate image to the left RTL: This actually means LEFT, anticlockwise Rotate image to the right RTL: This actually means RIGHT, clockwise &Close Save Image... Close Viewer Downloading full image... Error downloading image! Try again later. 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 Type a name for the new list... Type an optional description here Create L&ist &Add to List WARNING: Delete list? Are you sure you want to delete %1? 1=Name of a person list &Yes, delete it &No 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 LogViewer Log Clear &Log &Close MainWindow Minor activities done by everyone, such as replying to posts Minor activities addressed to you Minor activities done by you &Contacts The people you follow, the ones who follow you, and your person lists Press F1 for help Initializing... Dianara started. Running with Qt v%1. Your account is not configured yet. Click here to configure your account &Session Auto-update &Timelines Mark All as Read &Post a Note &Quit &View &Toolbar Status &Bar Full &Screen &Log S&ettings Edit &Profile &Account &Filters and Highlighting &Configure Dianara &Help Basic &Help Visit &Website Report a &Bug Pump.io User &Guide Some Pump.io &Tips List of Some Pump.io &Users Pump.io &Network Status Website About &Dianara Toolbar Open the log viewer Auto-updating enabled Auto-updating disabled Proxy password required You have configured a proxy server with authentication, but the password is not set. Enter the password for your proxy server: Your biography is empty Click to edit your profile Starting automatic update of timelines, once every %1 minutes. Stopping automatic update of timelines. Received %1 older posts in '%2'. %1 is a number, %2 = name of a timeline 1 highlighted singular, refers to a post %1 highlighted plural, refers to posts Direct messages By filters 1 more pending to receive. singular, one post %1 more pending to receive. plural, several posts Also: Last update: %1 '%1' updated. %1 is the name of a feed Received %1 older activities in '%2'. %1 is a number, %2 = name of feed 1 more pending to receive. singular, 1 activity %1 more pending to receive. plural, several activities Dianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. Shutting down Dianara... System tray icon is not available. Dianara cannot be hidden in the system tray. Do you want to close the program completely? Timeline updated at %1. Update %1 Locked Panels and Toolbars Side Panel Show Welcome Wizard There is 1 new post. There are %1 new posts. 1 filtered out. singular, refers to a post %1 filtered out. plural, refers to posts 1 deleted. singular, refers to a post %1 deleted. plural, refers to posts No new posts. Total posts: %1 &Timeline The main timeline &Messages Messages sent explicitly to you &Activity Your own posts Favor&ites Your favorited posts Your Pump.io account is not configured Minor feed updated at %1. There is 1 new activity. There are %1 new activities. 1 highlighted. singular, refers to an activity %1 highlighted. plural, refers to activities 1 filtered out. singular, refers to one activity %1 filtered out. plural, several activities No new activities. Error storing image! %1 bytes Link to: %1 Marking everything as read... About Dianara Dianara is a pump.io social networking client. With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. English translation by JanKusanagi. TRANSLATORS: Change this with your language and name. If there was another translator before you, add your name after theirs ;) Thanks to all the testers, translators and packagers, who help make Dianara better! &Hide Window &Show Window Closing due to environment shutting down... Quit? You are composing a note or a comment. Do you really want to close Dianara? &Yes, close the program &No MinorFeed Older Activities Get previous minor activities There are no activities to show yet. Get %1 newer As in: Get 3 newer (activities) MinorFeedItem Using %1 Application used to generate this activity To: %1 1=people to whom this activity was sent Cc: %1 1=people to whom this activity was sent as CC Open referenced post MiscHelpers bytes Error: Unable to launch browser The default system web browser could not be executed. You might need to install the XDG utilities. PageSelector Jump to page Page number: &First As in: first page &Last As in: last page Newer As in: newer pages Older As in: older pages &Go &Cancel PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Click the image to see it in full size Click to download the attachment Post Noun, not verb Via %1 To Cc Shared on %1 Using %1 1=Program used for posting or sharing Open post in web browser Copy post link to clipboard Normalize text colors &Close Posted on %1 1=Date Type As in: type of object Modified on %1 In Parent As in 'Open the parent post'. Try to use the shortest word! Open the parent post, to which this one replies Comment verb, for the comment button Reply to this post. If you select some text, it will be quoted. Share Share this post with your contacts Unshare Unshare this post Edit Modify this post Delete Erase this post Join Group %1 members in the group Image is animated. Click on it to play. Size Image size (resolution) Couldn't load image! Loading image... Attached Audio Attached Video Attached File %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 Edited: %1 You like this Unlike Like this post Like Are you sure you want to share your own post? 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 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 &E-mail... Change &Avatar... This is your visible name Changing your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. &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. ProxyDialog Proxy Configuration Do not use a proxy Your proxy username Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. &Save &Cancel Proxy &Type &Hostname &Port Use &Authentication &User Pass&word Publisher Setting a title helps make the Meanwhile feed more informative Title Add a brief title for the post here (recommended) Remove Cancel the attachment, and go back to a regular note Drafts To... Select who will see this post Cc... Select who will get a copy of this post Picture Audio Video Other as in other kinds of files Ad&d... Upload media, like pictures or videos Post verb Hit Control+Enter to post with the keyboard Cancel Cancel the post Note started from another application. Ignoring new note request from another application. File not found. Select Picture... Find the picture in your folders Picture not set Select Audio File... Find the audio file in your folders Audio file not set Select Video... Find the video in your folders Video not set Select File... Find the file in your folders File not set Error: Already composing You can't edit a post at this time, because a post is already being composed. Update Editing post. You can't create a message for %1 at this time, because a post is already being composed. Draft loaded. ERROR: Already composing You can't load a draft at this time, because a post is already being composed. Draft saved. Posting failed. Try again. Warning: You have no followers yet You're trying to post to your followers only, but you don't have any followers yet. If you post like this, no one will be able to see your message. Do you want to make the post public instead of followers-only? &Yes, make it public &No, post to my followers only &Cancel, go back to the post Posting... Updating... Post is empty. File not selected. Select one image Image files Invalid image The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Select one audio file Audio files Invalid audio file The audio format cannot be detected. Select one video file Video files Invalid video file The video format cannot be detected. Select one file Invalid file The file type cannot be detected. All files Since you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. File is too big Dianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. This is a temporary measure, since the servers cannot set their own limits yet. Sorry for the inconvenience. Error The selected file cannot be accessed: It is owned by %1. %1 = a username You might not have the necessary permissions. Resolution Image resolution (size) Type Size %1 KiB of %2 KiB uploaded PumpController Authorized to use account %1. Getting initial data. There is no authorized account. Getting list of 'Following'... Getting list of 'Followers'... Getting list of person lists... Creating person list... Deleting person list... Getting a person list... Adding person to list... Removing person from list... Creating group... Joining group... Leaving group... Getting likes... The comments for this post cannot be loaded due to missing data on the server. Getting comments... Getting '%1'... %1 is the name of a feed Timeline Messages Uploading %1 1=filename HTTP error For the following HTTP error codesyou can check http://en.wikipedia.org/wiki/List_of_HTTP_status_codes in your language Gateway Timeout HTTP 504 error string Service Unavailable HTTP 503 error string Not Implemented HTTP 501 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. Followers Following Profile updated. Received '%1'. %1 is the name of a feed Loading external image from %1 regardless of SSL errors, as configured... %1 is a hostname Activity Updating profile... Getting site users for %1... %1 is a server name Favorites Meanwhile Mentions Actions User timeline Error loading timeline! Error loading minor feed! Unable to verify the address! Bad Gateway HTTP 502 error string Server version: %1 E-mail updated: %1 %1 published successfully. Updating post content... %1 is the type of object: note, image... Untitled post %1 published successfully. %1 is a piece of the post Post %1 published successfully. %1 is the title of the post Avatar published successfully. Untitled post %1 updated successfully. %1 is a piece of the post Post %1 updated successfully. %1 is the title of the post Comment %1 updated successfully. %1 is a piece of the comment Message liked or unliked successfully. Likes received. Comment %1 posted successfully. %1 is a piece of the comment 1 comment received. %1 comments received. Post by %1 shared successfully. 1=author of the post we are sharing Adding items... Message deleted successfully. Following %1 (%2) successfully. %1 is a person's name, %2 is the ID Stopped following %1 (%2) successfully. %1 is a person's name, %2 is the ID List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. List of 'lists' received. List of %1 users received. %1 is a server name Person list '%1' created successfully. Person list deleted successfully. Person list received. %1 (%2) added to list successfully. 1=contact name, 2=contact ID %1 (%2) removed from list successfully. 1=contact name, 2=contact ID Group %1 created successfully. Group %1 joined successfully. Left the %1 group successfully. File uploaded successfully. Posting message... Avatar uploaded. SSL errors in connection to %1! The application is not registered with your server yet. Registering... Getting OAuth token... OAuth support error Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support. You probably need to install the OpenSSL plugin for QCA: %1, %2 or similar. Authorization error There was an OAuth error while trying to get the authorization token. QOAuth error %1 Application authorized successfully. OAuth error while authorizing application. Waiting for proxy password... Still waiting for profile. Trying again... %1 attempts 1 attempt Some initial data was not received. Restarting initialization... Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally. All initial data received. Initialization complete. Ready. Can't follow %1 at this time. %1 is a user ID Trying to follow %1. %1 is a user ID Checking address %1 before following... SiteUsersList You can get a list of the newest users registered on your server by clicking the button below. More resources to find users: Wiki page 'Users by language' PPump user search service at inventati.org List of Followers for the Pump.io Community account Get list of users from your server Close list Loading... %1 users in %2 %1 = user count, %2 = server name 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, for instance: Press <b>F1</b> if you want to open the Help window. 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 over a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Pump.io User Guide 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. Newest Newer Older Requesting... Loading... Page %1 of %2. Showing %1 posts per page. %1 posts in total. Click here or press Control+G to jump to a specific page '%1' cannot be updated because a comment is currently being composed. %1 = feed's name %1 more posts pending for next update. Click here to receive them now. There are no posts Timestamp Invalid timestamp! Just now In the future A minute ago %1 minutes ago An hour ago %1 hours ago Yesterday %1 days ago A month ago %1 months ago A year ago %1 years ago UserPosts Posts by %1 Loading... &Close Received '%1'. %1 posts Error loading the timeline dianara-v1.4.1/translations/dianara_he.ts0000664000175000017500000071140013212033716016553 0ustar janjan ASActivity Public ציבורי %1 by %2 1=kind of object: note, comment, etc; 2=author's name %1 מאת %2 ASObject Note Noun, an object type איגרת מברק Article Noun, an object type מאמר Image Noun, an object type תמונה Audio Noun, an object type אודיו Video Noun, an object type וידאו File Noun, an object type קובץ Comment Noun, as in object type: a comment תגובה Group Noun, an object type קבוצה Collection Noun, an object type אוסף Other As in: other type of post אחר No detailed location אין מיקום מפורט Deleted on %1 נמחק ב%1 and one other ועוד אחד נוסף and %1 others ועוד %1 אחרים ASPerson Hometown עיר מגורים AccountDialog Account Configuration תצורת חשבון First, enter your Webfinger ID, your pump.io address. ראשית, הזן מזהה Webfinger, כתובת pump.io שלך. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. כתובתך נראת כמו username@pumpserver.org, ובאפשרותך למצוא אותה בתוך הפרופיל שלך, בממשק רשת. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example אם הפרופיל של נמצא בכתובת https://pump.example/yourname, אזי כתובתך היא yourname@pump.example If you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. 1=link to website אם אין לך חשבון עדיין, באפשרותך לרשום אחד אצל %1. קישור זה יוביל אותך לשרת פומבי אקראי. If you need help: %1 אם אתה צריך עזרה: %1 Pump.io User Guide מדריך משתמש Pump.io Your Pump.io address: כתובת Pump.io שלך: Your address, like username@pumpserver.org כתובתך, כמו username@pumpserver.org Enter or paste the verifier code provided by your Pump server here הזן או הדבק קוד מאמת המסופק על ידי שרת Pump שלך כאן Get &Verifier Code השג קוד &מאמת After clicking this button, a web browser will open, requesting authorization for Dianara לאחר לחיצה על לחצן זה, דפדפן רשת יפתח, תוך כדי בקשה לאימות עבור 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! ברגע שהסמכת את Dianara מתוך ממשק רשת של שרת Pump, אתה תקבל קוד אשר נקרא VERIFIER. העתק והדבק אותו לתוך השדה מטה. Verifier code: קוד מאמת: &Authorize Application &התר יישום &Save Details &שמור פרטים &Cancel &ביטול Your account is properly configured. חשבונך מוגדר כראוי. Press Unlock if you wish to configure a different account. הקש בטל נעילה אם ברצונך להגדיר חשבון אחר. &Unlock בטל &נעילה A web browser will start now, where you can get the verifier code עכשיו יעלה דפדפן רשת, כעת תהא באפשרותך להשיג קוד מאמת Your Pump address is invalid כתובת Pump הינה שגויה Verifier code is empty קוד מאמת הינו ריק Dianara is authorized to access your data ‏Dianara הינה מורשית לגשת למידע שלך If the browser doesn't open automatically, copy this address manually אם הדפדפן לא נפתח אוטומטית, העתק את כתובת זו ידנית Unable to open web browser! 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. ON THE LEFT should change to ON THE RIGHT in RTL languages בחר אנשים מתוך הרשימה שמימינך. באפשרותך לגרור אותם בעזרת העכבר, לחץ עליהם פעם או פעמיים (לחיצה-כפולה), או בחר אותם והשתמש בלחצן מטה. Clear &List טהר &רשימה &Done &סיים &Cancel &ביטול Public ציבור Followers עוקבים Lists רשימות People... אנשים... Selected People אנשים נבחרים AvatarButton Open %1's profile in web browser פתח את הפרופיל של %1 בתוך דפדפן רשת Open your profile in web browser פתח את הפרופיל שלי בתוך דפדפן רשת Send message to %1 לכבוד שלח הודעה אל %1 Browse messages עיין בהודעות Stop following הפסק לעקוב Follow עקוב Stop following? להפסיק לעקוב? Are you sure you want to stop following %1? האם אתה בטוח כי ברצונך להפסיק לעקוב אחר %1? &Yes, stop following &כן, הפסק לעקוב &No &לא BannerNotification Timelines were not automatically updated to avoid interruptions. צרי זמן לא עודכנו אוטומטית כדי להימנע מהפרעות. This happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you read זה מתרחש כאשר זהו זמן לעדכן את צירי הזמן, אולם אינך בראש העמוד הראשון, כדי להימנע מן הפרעות במהלך קריאה Update now עדכן עכשיו Hide this message הסתר את הודעה זו ColorPicker Change... התאם... Choose a color בחירת צבע Comment Posted on %1 פורסם ב%1 Modified on %1 שונה ב%1 Like or unlike this comment סמן או בטל סימון תגובה זו Quote This is a verb, infinitive צטט Reply quoting this comment השב תוך ציטוט תגובה זו Edit ערוך Modify this comment שנה את תגובה זו Delete מחק Erase this comment מחק את תגובה זו Unlike בטל סימון Like אהבתי %1 likes this comment Singular: %1=name of just 1 person %1 אוהב(ת) את תגובה זו %1 like this comment Plural: %1=list of people like John, Jane, Smith %1 אוהבים את תגובה זו WARNING: Delete comment? ﬡזﬣﬧﬣ: למחוק תגובה? Are you sure you want to delete this comment? האם אתה בטוח כי ברצונך למחוק את תגובה זו? &Yes, delete it &כן, מחק זאת &No &לא CommenterBlock Reload comments טען מחדש תגובות Comment Infinitive verb הגב You can press Control+Enter to send the comment with the keyboard באפשרותך ללחוץ Control+Enter כדי לשלוח את התגובה בעזרת המקלדת Cancel ביטול Press ESC to cancel the comment if there is no text הקש ESC כדי לבטל את התגובה אם אין שום טקסט Check for comments בדוק אם ישנן תגובות חדשות Show all %1 comments הצג את כל %1 התגובות Comments are not available תגובות אינן זמינות Error: Already composing שגיאה: כבר מלחין You can't edit a comment at this time, because another comment is already being composed. אין באפשרותך לערוך תגובה בזמן זה, מכיוון שתגובה אחרת כבר מצויה בשלבי כתיבה. Editing comment עריכת תגובה Loading comments... כעת טוען תגובות... Posting comment failed. Try again. פרסום תגובה נכשל. נסה שוב. An error occurred אירעה שגיאה Sending comment... כעת שולח תגובה... Updating comment... כעת מעדכן תגובה... Comment is empty. תגובה הינה ריקה. Composer Click here or press Control+N to post a note... הקש כאן או לחץ Control+N כדי לפרסם מברק... Symbols סמלים Formatting עיצוב Normal רגיל Bold בולט Italic נטוי Underline קו תחתון Strikethrough קו חוצה Header תקורה List רשימה Table טבלה 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 הקלד תגובה כאן You can attach only one file. You cannot drop folders here, only a single file. Insert as image? להכניס בתור תמונה? The link you are pasting seems to point to an image. נראה כי הקישור אותו אתה מדביק מצביע לתמונה. Insert as visible image הכנס בתור תמונה נראית Insert as link הכנס בתור קישור Table Size How many rows (height)? כמה שורות (גובה)? How many columns (width)? כמה טורים (רוחב)? Insert a link הכנס קישור Type or paste a web address here. You could also select some text first, to turn it into a link. הקלד או הדבק כתובת רשת כאן. באפשרותך גם לבחור קטע טקסט תחילה, כדי להפוך אותו לקישור. Make a link from selected text הכן קישור מתוך טקסט נבחר Type or paste a web address here. The selected text (%1) will be converted to a link. הקלד או הדבק כתובת רשת כאן. הטקסט הנבחר (%1) יומר לקישור. Invalid link קישור לא תקין The text you entered does not look like a link. הטקסט אשר הזנת לא נראה כמו קישור. It should start with one of these types: It = the link, from previous sentence עליו להתחיל עם אחד מטיפוסים אלו: &Use it anyway ה&שתמש בזה בכל זאת &Enter it again ה&זן אותו שוב &Cancel link &בטל קישור Insert an image from a URL הכנס תמונה מתוך URL Type or paste the image address here. The link must point to the image file directly. הקלד או הדבק כתובת תמונה כאן. על הקישור להצביע ישירות על קובץ תמונה. Error: Invalid URL שגיאה: URL לא כשר The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// הכתובת אשר הזנת (%1) אינה תקינה. כתובות תמונה צריכות להתחיל עם http://‎ או https://‎ Yes, but saving a &draft Cancel message? לבטל הודעה? Are you sure you want to cancel this message? האם אתה בטוח כי ברצונך לבטל את הודעה זו? &Yes, cancel it &כן, בטל זאת &No &לא ConfigDialog Program Configuration תצורת תוכנית minutes דקות Timeline &update interval תדירות &עדכון ציר זמן Left side tabs on left side/west; RTL not affected צד שמאל Right side tabs on right side/east; RTL not affected צד ימין Minor Feeds ערוצים שוליים Show snippets in minor feeds הצג קטעים בתוך ערוצים שוליים Only for images inserted from web sites. רק לתמונות מוכנסות מתוך אתר רשת. Use with care. יש לעשות שימוש זהיר. Public posts as &default פוסטים פומביים כ&שגרה Top עליון Bottom תחתון &Tabs position מיקום &כרטיסיות &Movable tabs כרטיסיות ניתנות לה&זזה Pro&xy Settings הגדרות &Proxy Network configuration תצורת רשת Set Up F&ilters הגדר &מסננים Filtering rules כללי סינון Post Titles כותרות פוסט Post Contents תגובות פוסט Comments תגובות You are among the recipients of the activity, such as a comment addressed to you. אתה הוא אחד מבין הנמענים של הפעילות, כגון תגובה אשר מוענה אליך. Used also when highlighting posts addressed to you in the timelines. בשימוש גם בהדגשת פוסטים אשר מכוונים אליך בצירי זמן. The activity is in reply to something done by you, such as a comment posted in reply to one of your notes. הפעילות מצויה בתוך מענה למשהו אשר נעשה על ידיך, כגון תגובה אשר פורסמה בתשובה לאחד מן המברקים שלך. You are the object of the activity, such as someone adding you to a list. אתה הוא היעד של הפעילות, כמו מישהו שמוסיף אותך לרשימה. The activity is related to one of your objects, such as someone liking one of your posts. הפעילות הינה קשורה לאחד מהיעדים שלך, כמו מישהו שאוהב את אחד הפוסטים שלך. Used also when highlighting your own posts in the timelines. בשימוש גם בהדגשת הפוסטים שלך בצירי זמן. Item highlighted due to filtering rules. פריט מודגש בשל כללי סינון. Item is new. פריט הינו חדש. 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 פוסטים לכל עמוד, &צירי זמן אחרים Highlighted activities, except mine פעילויות מודגשות, למעט שלי Any highlighted activity כל פעילות מודגשת Always תמיד Never אף פעם characters This is a suffix, after a number תווים Snippet limit מגבלת קטע Show information for deleted posts הצג מידע עבור פוסטים מחוקים No Before avatar Before avatar, subtle After avatar After avatar, subtle Hide duplicated posts הסתר פוסטים כפולים Jump to new posts line on update קפוץ לשורת פוסטים חדשה בעדכון Snippet limit when highlighted מגבלת קטע כאשר מודגש Minor feed avatar sizes מידות אווטאר ערוץ שולי Show activity icons Avatar size מידת אווטאר Avatar size in comments מידת אווטאר בתוך תגובות Show extended share information הצג מידע שיתוף נוסף Show extra information הצג מידע נוסף Highlight post author's comments הדגש תגובות של מחבר פוסט Highlight your own comments הדגש את התגובות שלי Ignore SSL errors in images התעלם משגיאות SSL בתמונות Show full size images הצג תמונות אווטאר בגודל מלא Use attachment filename as initial post title השתמש בשם קובץ מצורף בתור כותרת פוסט פותחת Show character counter הצג מונה תווים Don't inform followers when following someone אל תיידע עוקבים בעת מעקב אחר מישהו Don't inform followers when handling lists אל תיידע עוקבים בעת התעסקות ברשימות Inform only the author when liking things help refine this translation תיידע רק את המחבר כאשר אני מסמן דברים As system notifications בתור התראות מערכת Using own notifications באמצעות התראות עצמיות Don't show notifications אל תציג התראות seconds Next to a duration, in seconds שניות Notification Style סגנון התראה Duration משך Persistent Notifications התראות נמשכות Also highlight taskbar entry Notify when receiving: התרע בעת קבלה: New posts פוסטים חדשים Highlighted posts פוסטים מודגשים New activities in minor feed פעילויות חדשות בתוך ערוץ שולי Highlighted activities in minor feed הדגש פעילויות בתוך ערוץ שולי Important errors שגיאות חשובות Default שגרתי System iconset, if available מערך סמלי מערכת, אם זמין Show your current avatar הצג את האווטאר הנוכחי שלך Custom icon סמל מותאם System Tray Icon &Type &טיפוס סמל מגש מערכת S&elect... &בחר... Custom &Icon סמל &מותאם Hide window on startup הסתר חלון בעת הפעלה General Options אפשרויות כלליות Fonts גופנים Colors צבעים Timelines צירי זמן Posts פוסטים Composer מלחין Privacy פרטיות Notifications התראות System Tray מגש מערכת Dianara stores data in this folder: ‏Dianara מאחסנת מידע בתיקייה זו: ‎ &Save Configuration &שמור תצורה &Cancel &ביטול This is a system notification זוהי התראת מערכת System notifications are not available! התראות מערכת אינן זמינות! Own notifications will be used. התראות עצמיות ייכנסו לשימוש. This is a basic notification זוהי התראה בסיסית Select custom icon בחירת סמל מותאם Image files קבצי תמונה All files כל הקבצים Invalid image תמונה לא כשרה The selected image is not valid. התמונה הנבחרת אינה תקינה. ContactCard Hometown עיר מגורים Joined: %1 הצטרף: %1 Updated: %1 עודכן: %1 Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name ביו של %1 This user doesn't have a biography למשתמש זה אין ביוגרפיה No biography for %1 %1=contact name אין ביוגרפיה עבור %1 Open Profile in Web Browser פתח פרופיל בתוך דפדפן רשת Send Message שלח הודעה Browse Messages עיין בהודעות User Options אפשרויות משתמש Follow עקוב Stop Following הפסק לעקוב Stop following? להפסיק לעקוב? Are you sure you want to stop following %1? האם אתה בטוח כי ברצונך להפסיק לעקוב אחר %1? &Yes, stop following &כן, הפסק לעקוב &No &לא ContactList Type a partial name or ID to find a contact... הקלד שם או מזהה חלקי כדי למצוא איש קשר... F&ull List &רשימה מלאה ContactManager username@server.org or https://server.org/username username@server.org או https://server.org/username &Enter address to follow: ה&זן כתובת למעקב: &Follow &עקוב Reload Followers טען מחדש עוקבים Reload Following טען מחדש עוקב Export Followers יצא עוקבים Export Following יצא עוקב Reload Lists טען מחדש רשימות &Neighbors &שכנים Cannot export to this file: Please enter another file name, or choose a different folder. The server seems to be a Pump server, but the account does not exist. נראה כי השרת הינו שרת Pump, אולם החשבון לא קיים. %1 doesn't seem to be a Pump server. %1 is a hostname ‏%1 לא נראה כמו שרת Pump. Following this account at this time will probably not work. מעקב אחר חשבון זה בעת זו כנראה לא יעבוד. The %1 server seems unavailable. %1 is a hostname נראה כי השרת %1 לא זמין. Unknown Refers to server version לא ידועה Error שגיאה The user address %1 does not exist, or the %2 server is down. כתובת משתמש %1 לא קיימת, או שהשרת %2 הינו מושבת. Server version גרסת שרת Check the address, and keep in mind that usernames are case-sensitive. בדוק את הכתובת, וזכור כי שמות משתמש רגישים להבדל בין אותיות גדולות וקטנות. Do you want to try following this address anyway? האם ברצונך לנסות לעקוב אחר כתובת זו בכל זאת? (not recommended) (לא מומלץ) Yes, follow anyway כן, עקוב בכל זאת No, cancel לא, בטל About to follow %1... כעת עומד לעקוב אחר %1... Follo&wers עו&קבים Followin&g עוק&ב &Lists &רשימות Export list of 'following' to a file יצא רשימה של 'עוקב' לקובץ Export list of 'followers' to a file יצא רשימה של 'עוקבים' לקובץ DownloadWidget Open Verb, as in: Open the downloaded file פתח Download הורד Save the attached file to your folders שמור אב הקובץ המצורף לתיקייה שלך Cancel ביטול Save File As... שמור קובץ בתור... All files כל הקבצים File not found! קובץ לא נמצא! Abort download? לנטוש הורדה? Do you want to stop downloading the attached file? האם ברצונך להפסיק להוריד את הקובץ המצורף? &Yes, stop &כן, עצור &No, continue &לא, המשך Download aborted הורדה בוטלה Download completed הורדה הושלמה Attachment downloaded successfully to %1 %1 = filename תצריף הורד בהצלחה אל %1 Open the downloaded attachment with your system's default program for this type of file. Jan: I re-imported the previous translation for the similar sentence; might need checking פתח את התצריף המורד באמצעות התוכנית השגרתית של המערכת שלך עבור טיפוס קובץ זה. Download failed הורדה נכשלה Downloading attachment failed: %1 %1 = filename הורדת תצריף נכשלה: %1 Downloading %1 KiB... כעת מוריד %1 קי״ב... %1 KiB downloaded %1 קי״ב הורדו DraftsManager Draft Manager Load Save Manage drafts... &Delete selected draft &Close &סגור Untitled draft Delete draft? Are you sure you want to delete this draft? &Yes, delete it &כן, מחק זאת &No &לא EmailChanger Change E-mail Address החלף כתובת דוא״ל Change החלף &Cancel &ביטול E-mail Address: כתובת דוא״ל: Again: שוב: Your Password: סיסמתך: E-mail addresses don't match! כתובות דוא״ל לא תואמות! Password is empty! סיסמה הינה ריקה! FDNotifications Show FilterEditor Filter Editor עריכת מסננים %1 if %2 contains: %3 This explains a filter rule, like: Hide if Author ID contains JohnDoe %1 כאשר %2 מכיל: %3 Here you can set some rules for hiding or highlighting stuff. 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. You could also highlight messages that contain your name. BUG: stuff > things כאן באפשרותך לקבוע מספר כללים לצורך הסתרת או הדגשת דברים. באפשרותך לסנן לפי תוכן, מחבר או יישום. למשל, באפשרותך לסנן הודעות אשר פורסמו באמצעות היישום Open Farm Game, או אשר מכילות את הצירוף NSFW בתוך הודעה. באפשרותך גם להדגיש הודעות אשר מכילות את שמך. Hide הסתר Highlight הדגש Post Contents translated into singular form due to string contains תוכן פוסט Author ID מזהה מחבר Application יישום Activity Description תיאור פעילות Keywords... מילות מפתח... &Add Filter הוס&ף מסנן Filters in use מסננים בשימוש &Remove Selected Filter הס&ר מסנן נבחר &Save Filters &שמור מסננים &Cancel &ביטול if כאשר contains מכיל &New Filter מסנן &חדש C&urrent Filters מסננים &נוכחיים FilterMatchesWidget Content The contents of the post matched תוכן Author מחבר App Application, short if possible אפליקציה Description תיאור FirstRunWizard Welcome Wizard אשף התחלה Welcome to Dianara! ברוך בואך אל Dianara! This wizard will help you get started. אשף זה יעזור לך להתחיל. You can access this window again at any time from the Help menu. תוכל לגשת לחלון זה בכל עת מתוך התפריט עזרה. The first step is setting up your account, by using the following button: הצעד הראשון הוא להתקין את חשבונך, באמצעות הכפתור הבא: Configure your &account הגדר את ה&חשבון שלי Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already. ברגע שהגדרת את חשבונך, רצוי כי תערוך את הפרופיל שלך ותוסיף אווטאר ועוד נתונים אחרים, אם לא עשית זאת כבר. &Edit your profile ערוך את ה&פרופיל שלי By default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes. באופן שגרתי, Dianara תפרסם רק לעוקבים שלך, אולם רצוי לפרסם לציבור, לפחות לעתים. Post to &Public by default פרסם ל&ציבור באופן שגרתי Open general program &help window פתח חלון &עזרה כללית &Show this again next time Dianara starts &הצג זאת שוב בפעם הבאה כאשר Dianara תתחיל &Close &סגור FontPicker Change... התאם... Choose a font בחירת גופן HelpWidget Basic Help עזרה בסיסית Getting started מקום להתחלה התחלה Settings הגדרות Timelines צירי זמן Posting פרסום Managing contacts ניהול אנשי קשר Keyboard controls בקרי מקלדת Command line options אפשרויות שורת פקודה Contents תכנים The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. בפעם הראשונה אשר בה תתחילו את Dianara, אתם צריכים לראות את דו שיח תצורת חשבון. שם, הזינו את כתובת Pump.io שלכם בתור name@server ולחצו על הלחצן השג קוד מאמת. Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. אחר כך, הדפדפן רשת השגרתי שלכם צריך להעלות עמוד אימות בתוך שרת Pump.io שלכם. שם, יהיה עליכם להעתיק קוד VERIFIER במלואו, ולהדביקו לתוך השדה השני של Dianara. אחר כך לחצו התר יישום, וברגע שזה מאומת, הקישו שמור פרטים. At this point, your profile, contact lists and timelines will be loaded. בנקודה זו, הפרופיל, רשימות אנשי הקשר וצירי הזמן שלך יוטענו. You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. עליכם לקחת מבט על חלון תצורת תוכנית, תחת התפריט הגדרות - הגדר את Dianara. ישנם מספר אפשרויות שם. Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. לתשומת לבך, ישנם מקומות רבים בתוך Dianara אשר באפשרותך לקבל עוד מידע על ידי העברת סמן העכבר על גבי טקסט או לחצן, ולהמתין להופעתה של תיבה צצה. If you're new to Pump.io, take a look at this guide: אם אתם חדשים בפרויקט Pump.io, קחו מבט על מדריך זה: Pump.io User Guide מדריך משתמש Pump.io You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. באפשרותכם להגדיר מספר דברים לנוחותכם בתוך ההגדרות, כגון תדירות זמן בין עדכוני ציר זמן, על כמה פוסטים להופיע לכל עמוד, צבעי הדגשה, התראות או כיצד סמל מגש מערכת נראה. Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. בתור פומבי כאן, באפשרותכם להפעיל את האפשרות לפרסם תמיד את הפוסטים שלך לציבור באופן שגרתי. תמיד תוכלו לשנות זאת בעת פרסום. There are seven timelines: ישנם שבעה צירי זמן: The main timeline, where you'll see all the stuff posted or shared by the people you follow. בציר זמן הראשי, מקום בו תראה את כל הדברים אשר פורסמו או שותפו על ידי האנשים אחריהם אתה עוקב. Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. ציר זמן הודעות, מקום בו תראה הודעות אשר נשלחו ספיציפית אליך. קיימת אפשרות כי הודעות אלו נשלחו לאנשים אחרים גם כן. Activity timeline, where you'll see your own posts, or posts shared by you. ציר זמן פעילות, מקום בו תראה את הפוסטים שלך, או פוסטים אשר שיתפת. Favorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. ציר זמן מועדפים, מקום בו תראה את הפוסטים והתגובות אשר סימנת. זה יכול לשרת אותך בתור מערכת סימניות. The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. LEFT SIDE should change to RIGHT SIDE on RTL languages הציר זמן החמישי הוא הציר זמן השולי, מוכר גם בתור ״בינתיים״. זה נראה על הצד הימני, בכל זאת זה ניתן להסתרה. כאן תראו פעילויות שוליות אשר מבוצעות על ידי כל מי שאתם עוקבים, כגון פעולות תגובה, פוסטים אשר סומנו או אנשים אשר נתונים למעקבכם. The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). הצירי זמן השישי והשביעי הינם גם צירי זמן שוליים, דומה לציר זמן ״בינתיים״, אך מכיל רק פעילויות אשר מוענו ישירות אליכם (איזכורים) ופעילויות אשר בוצעו על ידיכם (פעולות). These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. לפעילויות אלה אפשרי כי יהיה כפתור '+' בתוכן. לחצו עליו כדי לפתוח את הפוסט אליו הם מפנים. גם כן, כמו במקומות אחרים רבים, באפשרותכם לעבור בעזרת העכבר שלכם כדי לראות מידע רלוונטי בתוך תיבה צצה. New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. הודעות חדשות מוופיעות בהדגשה בצבע שונה. באפשרותכם לסמן אותן בתור כאלה שנקראו על ידי הקשה על אזורים ריקים של ההודעה. You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. בגדר רשות באפשרותכם לפרסם מברקים על ידי הקשה בשדה טקסט בראש החלון או על ידי לחיצה Control+N. הגדרת כותרת לפוסט שלך הינו אופציונלי, אולם רצוי מאוד, שכן זה יעזור לזהות הפניות לפוסטים שלך בערוץ השולי טוב יותר, התראות דוא״ל, וכו׳. It is possible to attach images, audio, video, and general files, like PDF documents, to your post. אפשר לצרף תמונות, אודיו, וידאו, וגם קבצים כלליים, כגון מסמכי PDF, לפוסטים שלכם. You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. באפשרותכם להשתמש בלחצן עיצוב כדי להוסיף עיצוב לטקסט שלכם, כגון בולט או נטוי. חלק מאפשרויות אלה מצריכות בחירת טקסט בטרם אלה נכנסות לכדי שימוש. You can select who will see your post by using the To and Cc buttons. באפשרותכם לבחור מי יראה את הפוסטים שלכם באמצעות הלחצנים לכבוד וגם העתק. If you add a specific person to the 'To' list, they will receive your message in their direct messages tab. אם תוסיפו אדם ספיציפי לרשימת 'לכבוד', הם יקבלו את ההודעה שלכם בתוך כרטיסיית הודעות ישירות. You can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. באפשרותכם גם להקליד '@' ואת התו הראשון של השם של איש קשר כדי להעלות תפריט קופץ עם אפשרויות תואמות. Choose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. בחרו אחת בעזרת מקשי חץ ולחצו Enter כדי להשלים את השם. זה יוסיף את אדם זה לרשימת נמענים. You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. באפשרותכם ליצור הודעות פרטיות על ידי הוספת אנשים מסוימים לרשימות אלה, והאפשרויות אי-בחירת העוקבים או פומבי. You can see the lists of people you follow, and who follow you from the Contacts tab. באפשרותכם לראות את הרשימות של אנשים אשר אחריהם אתם עוקבים, ואלו אשר עוקבים אחריכם מתוך כרטיסיית אנשי קשר. There, you can also manage person lists, used mainly to send posts to specific groups of people. שם, באפשרותכם גם לנהל רשימות אישים, בשימוש בעיקר כדי לשלוח פוסטים לקבוצות מסוימות של אנשים. There is a text field at the top, where you can directly enter addresses of new contacts to follow them. ישנו שדה טקסט במעלה, מקום בו באפשרותכם להזין ישירות כתובות של אנשי קשר חדשים כדי לעקוב אחריהם. Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. תחת הכרטיסייה 'שכנים' אתם תראו מספר משאבים למציאת אנשים, ולצד זאת תעמוד לרשותכם האפשרות לעיין במשתמשים הרשומים האחרונים מתוך השרת שלכם ישירות. You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. באפשרותכם להקיש על כל אווטאר בפוסטים, בתגובות, ובטור ״בינתיים״, ויופיע לכם תפריט עם מספר אפשרויות, אחת מאלה היא לעקוב או לבטל מעקב אחר אדם זה. You can also send a direct message (initially private) to that contact from this menu. באפשרותך גם לשלוח הודעה ישירה (פרטית בהתחלה) לאיש קשר זה מתוך תפריט זה. You can find a list with some Pump.io users and other information here: באפשרותכם למצוא רשימה עם משתמשי Pump.io מסוימים ומידע אחר כאן: Users by language משתמשים לפי שפה Followers of Pump.io Community account עוקבי חשבון קהילת Pump.io The most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. לפעולות הכי נפוצות אשר מצויים בתפריטים יש קיצורי מקלדת, כגון F5 או Control+N. Besides that, you can use: נוסף על כך, באפשרותכם להשתמש באלו: Control+Up/Down/PgUp/PgDown/Home/End to move around the timeline. ‏Control+מעלה/מטה/PgUp/PgDown/Home/End כדי לזוז סביב הציר זמן. Control+Left/Right to jump one page in the timeline. ‏Control+שמאל/ימין כדי לקפוץ עמוד אחד בתוך ציר זמן. Control+G to go to any page in the timeline directly. ‏Control+G כדי לגשת לכל עמוד בתוך ציר זמן ישירות. Control+1/2/3 to switch between the minor feeds. ‏Control+1/2/3 כדי להחליף בין ערוצים שוליים. Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. ‏Control+Enter כדי לפרסם, כאשר אתה מסיים לכתוב מברק או תגובה. אם המברק הינו ריק, באפשרותך לבטלה על ידי לחיצה על ESC. While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. בזמן כתיבת מברק, לחצו Enter כדי לקפוץ מהכותרת לגוף הודעה. גם כן, לחיצה על החץ מעלה כאשר אתם מצויים בתחילת ההודעה, תקפיץ אתכם בחזרה לכותרת. Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. ‏Control+Enter כדי לסיים ליצור רשימת נמענים לפרסום, בתוך הרשימות 'לכבוד' או 'העתק'. You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. באפשרותכם להשתמש בפרמטר config-- כדי להריץ את התוכנית בעזרת תצורה אחרת. זה יכול להועיל כדי להשתמש בשני חשבונות או יותר. באפשרותכם אף להריץ שני תהליכים של Dianara בו זמנית. Use the --debug parameter to have extra information in your terminal window, about what the program is doing. השתמשו בפרמטר debug-- כדי להקנות לכם מידע נוסף בתוך חלון מסוף, אודות מה התוכנית עושה. If your server does not support HTTPS, you can use the --nohttps parameter. אם השרת שלכם לא תומך HTTPS, אתם יכולים להשתמש בפרמטר nohttps--. Dianara offers a D-Bus interface that allows some control from other applications. ‏Dianara מציעה ממשק D-Bus אשר מתיר בקרה מסוימת מתוך יישומים אחרים. The interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. הממשק נמצא על %1, ובאפשרותכם לקבל גישה אליו בעזרת כלים כגון %2 או %3. זה מציע שיטות כמו %4 וגם %5. If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. אם אתם משתמשים בתצורה חלופית, עם משהו כמו 'config otherconf--', אזי הממשק יהיה בכתובת org.nongnu.dianara_otherconf. &Close &סגור ImageViewer Untitled ללא כותרת Image תמונה &Save As... &שמור בתור... &Restart Restart animation התחל &מחדש Fit As in: fit image to window התאם Rotate image to the left RTL: This actually means LEFT, anticlockwise סובב תמונה לשמאל Rotate image to the right RTL: This actually means RIGHT, clockwise סובב תמונה לימין &Close &סגור Save Image... שמור תמונה... Close Viewer סגור צופה Downloading full image... כעת מוריד תמונה במלואה... Error downloading image! שגיאה בהורדת תמונה! Try again later. נסה שוב מאוחר יותר. 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. התרחשה שגיאה במהלך שמירת %1. שם קובץ צריך להישמר בסיומות jpg. או .png. ListsManager Name שם Members חברים Add Mem&ber הוס&ף חבר &Remove Member הס&ר חבר &Delete Selected List &מחק רשימה נבחרת Add New &List הוסף &רשימה חדשה Type a name for the new list... הקלד שם לרשימה החדשה... Type an optional description here הקלד תיאור אופציונאלי כאן Create L&ist &צור רשימה &Add to List הוס&ף לרשימה WARNING: Delete list? ﬡזﬣﬧﬣ: למחוק רשימה? Are you sure you want to delete %1? 1=Name of a person list האם אתה בטוח כי ברצונך למחוק את %1? &Yes, delete it &כן, מחק זאת &No &לא 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 האם אתה בטוח כי ברצונך להסיר את %1 מתוך הרשימה %2? &Yes &כן LogViewer Log יומן Clear &Log טהר יו&מן &Close &סגור MainWindow Minor activities done by everyone, such as replying to posts פעילויות שוליות אשר נעשו על ידי כולם, כגון משוב על פוסט Minor activities addressed to you השג פעילויות שוליות אשר הופנו על ידך Minor activities done by you פעילויות שוליות אשר נעשו על ידך &Contacts &אנשי קשר The people you follow, the ones who follow you, and your person lists האנשים אחריהם אתה עוקב, אלו שעוקבים אחריך, ורשימות האישים שלך Press F1 for help הקש F1 לעזרה Initializing... כעת מאתחל... Dianara started. ‏Dianara התחילה. Running with Qt v%1. מורצת בעזרת Qt גרסא %1. Your account is not configured yet. חשבונך אינו מוגדר עדיין. Click here to configure your account הקש כאן כדי להגדיר את חשבונך &Session &סשן Auto-update &Timelines עדכן &אוטומטית צירי זמן Mark All as Read סמן הכל כנקרא &Post a Note &פרסם מברק &Quit י&ציאה &View &תצוגה Side &Panel סרגל &צד &Toolbar סרגל &כלים Status &Bar &שורת מצב Full &Screen &מסך מלא &Log יו&מן S&ettings &הגדרות Edit &Profile ערוך &פרופיל &Account &חשבון &Filters and Highlighting &מסננים והדגשות &Configure Dianara &הגדר את Dianara &Help &עזרה Basic &Help &עזרה בסיסית Visit &Website בקר &אתר רשת Report a &Bug דווח על &באג Pump.io User &Guide &מדריך משתמש Pump.io Some Pump.io &Tips כמה &טיפים של Pump.io List of Some Pump.io &Users רשימה של &משתמשי Pump.io מסוימים Pump.io &Network Status Website אתר רשת מצב &רשת Pump.io About &Dianara אודות &Dianara Toolbar סרגל כלים Open the log viewer פתח צופה יומן Auto-updating enabled עדכון אוטומטי מאופשר Auto-updating disabled עדכון אוטומטי מנוטרל Proxy password required סיסמת ציר נדרשת You have configured a proxy server with authentication, but the password is not set. הגדרת שרת ציר לצד אימות, אך הסיסמה אינה מוגדרת. Enter the password for your proxy server: הזן את הסיסמה עבור שרת ציר שלך: Your biography is empty הביוגרפיה שלך הינה ריקה Click to edit your profile הקש כדי לערוך את הפרופיל שלך Starting automatic update of timelines, once every %1 minutes. מתחיל עדכון אוטומטי של צירי זמן, אחת לכל %1 דקות. Stopping automatic update of timelines. הפסיק עדכון אוטומטי של צירי זמן. Received %1 older posts in '%2'. %1 is a number, %2 = name of a timeline נתקבלו %1 פוסטים ישנים יותר בתוך '%2'. 1 highlighted singular, refers to a post 1 הודגש %1 highlighted plural, refers to posts %1 הודגשו Direct messages הודעות ישירות By filters לפי מסננים 1 more pending to receive. singular, one post 1 נוסף ממתין להתקבל. %1 more pending to receive. plural, several posts %1 נוספים ממתינים להתקבל. Also: גם: Last update: %1 עדכון אחרון: %1 '%1' updated. %1 is the name of a feed ‏'%1' עודכן. Received %1 older activities in '%2'. %1 is a number, %2 = name of feed נתקבלו %1 פעילויות ישנות יותר בתוך '%2'. 1 more pending to receive. singular, 1 activity 1 נוספת ממתינה להתקבל. %1 more pending to receive. plural, several activities %1 נוספות ממתינות להתקבל. Dianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. ‏Dianara היא תכנה חופשית, רשויה תחת הרשיון GNU GPL, ומשתמשת במספר סמלי Oxygen תחת הרשיון LGPL. Shutting down Dianara... כעת מכבה את Dianara... System tray icon is not available. סמל מגש מערכת אינו זמין. Dianara cannot be hidden in the system tray. אי אפשר להסתיר את Dianara בתוך מגש מערכת. Do you want to close the program completely? האם ברצונך לסגור את התוכנית לחלוטין? Timeline updated at %1. ציר זמן עודכן בשעה %1. Update %1 עדכן %1 Locked Panels and Toolbars Side Panel סרגל צד Show Welcome Wizard הצג אשף התחלה There is 1 new post. ישנו פוסט חדש 1. There are %1 new posts. ישנם %1 פוסטים חדשים. 1 filtered out. singular, refers to a post 1 סונן. %1 filtered out. plural, refers to posts %1 סוננו. 1 deleted. singular, refers to a post 1 נמחק. %1 deleted. plural, refers to posts %1 נמחקו. No new posts. אין פוסטים חדשים. Total posts: %1 סה״כ פוסטים: %1 &Timeline &ציר זמן The main timeline ציר זמן הראשי &Messages &הודעות Messages sent explicitly to you הודעות אשר נשלחו במפורש אליך &Activity &פעילות Your own posts הפוסטים שלך Favor&ites &מועדפים Your favorited posts הפוסטים המועדפים שלך Your Pump.io account is not configured חשבון Pump.io שלך אינו מוגדר Minor feed updated at %1. ערוץ שולי התעדכן ב%1. There is 1 new activity. ישנה פעילות חדשה 1. There are %1 new activities. ישנן %1 פעילויות חדשות. 1 highlighted. singular, refers to an activity 1 הודגשה. %1 highlighted. plural, refers to activities %1 הודגשו. 1 filtered out. singular, refers to one activity 1 סוננה. %1 filtered out. plural, several activities %1 סוננו. No new activities. אין פעילויות חדשות. Error storing image! שגיאה באחסון תמונה! %1 bytes %1 בתים Link to: %1 קישור אל: %1 Marking everything as read... סמן את הכל כנקרא... About Dianara אודות Dianara Dianara is a pump.io social networking client. <p dir=rtl>Dianara היא לקוח רשת חברתית pump.io.</p> With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. <p dir=rtl>בעזרת Dianara באפשרותך לראות צירי זמן, ליצור פוסטים חדשים, להעלות תצלומים ומדיות אחרות, להידבר עם פרסומים, לנהל את אנשי הקשר שלך ולעקוב אחר אנשים חדשים.</p> English translation by JanKusanagi. TRANSLATORS: Change this with your language and name. If there was another translator before you, add your name after theirs ;) תרגום לעברית מאת GreenLunar. Thanks to all the testers, translators and packagers, who help make Dianara better! תודה לכל הבוחנים, המתרגמים והאורזים, אשר עזרו להפוך את Dianara לטובה יותר! &Hide Window &הסתר חלון &Show Window &הצג חלון Closing due to environment shutting down... כעת סוגר בשל כיבוי סביבה... Quit? לצאת? You are composing a note or a comment. אתה כעת כותב מברק או תגובה. Do you really want to close Dianara? האם אתה באמת רוצה לסגור את Dianara? &Yes, close the program &כן, סגור את תוכנית זו &No &לא MinorFeed Older Activities פעילויות ישנות יותר Get previous minor activities השג פעילויות שוליות קודמות There are no activities to show yet. אין פעילויות להצגה עדיין. Get %1 newer As in: Get 3 newer (activities) השג %1 חדשות MinorFeedItem Using %1 Application used to generate this activity באמצעות %1 To: %1 1=people to whom this activity was sent לכבוד: %1 Cc: %1 1=people to whom this activity was sent as CC העתק: %1 Open referenced post פתח פוסט מאזכר MiscHelpers bytes בתים Error: Unable to launch browser The default system web browser could not be executed. You might need to install the XDG utilities. PageSelector Jump to page קפוץ אל עמוד Page number: עמוד מספר: &First As in: first page &ראשון &Last As in: last page &אחרון Newer As in: newer pages חדש יותר Older As in: older pages ישן יותר &Go &עבור &Cancel &ביטול PeopleWidget &Search: &חפש: Enter a name here to search for it הזן שם לחיפוש כאן Add a contact to a list הוסף איש קשר לרשימה &Cancel &ביטול Post Click the image to see it in full size לחץ על התמונה כדי לראותה בגודל מלא Click to download the attachment לחץ כדי להוריד את התצריף Post Noun, not verb פוסט Via %1 דרך %1 To לכבוד Cc העתק Shared on %1 שותף ב%1 Using %1 1=Program used for posting or sharing באמצעות %1 Open post in web browser פתח פוסט בתוך דפדפן רשת Copy post link to clipboard העתק קישור פוסט ללוח גזירה Normalize text colors הפוך צבעי טקסט לצבע אחיד נרמל צבעי טקסט &Close &סגור Posted on %1 1=Date פורסם ב%1 Type As in: type of object טיפוס Modified on %1 שונה ב%1 In בתוך Parent As in 'Open the parent post'. Try to use the shortest word! הורה Open the parent post, to which this one replies פתח פוסט הורה, אשר לו זה מגיב Comment verb, for the comment button הגב Reply to this post. השב לפוסט זה. If you select some text, it will be quoted. אם תבחר טקסט מסוים, זה יצוטט. Share שתף Share this post with your contacts שתף את פוסט זה עם אנשי קשר Unshare בטל שיתוף Unshare this post בטל שיתוף פוסט זה Edit ערוך Modify this post שנה את פוסט זה Delete מחק Erase this post מחק את פוסט זה Join Group הצטרף לקבוצה %1 members in the group %1 חברים בקבוצה Image is animated. Click on it to play. תמונה הינה מונפשת. לחץ כדי להריץ אותה. Size Image size (resolution) מידה Couldn't load image! לא מסוגל לטעון תמונה! Loading image... כעת טוען תמונה... Attached Audio אודיו מצורף Attached Video וידאו מצורף Attached File קובץ מצורף %1 likes this One person %1 אוהב(ת) זאת %1 like this More than one person %1 אהבו זאת 1 like סימון 1 %1 likes %1 סימונים 1 comment תגובה 1 %1 comments %1 תגובות %1 shared this %1 = One person name %1 שיתף(ה) זאת %1 shared this %1 = Names for more than one person %1 שיתפו זאת Shared once שותף פעם אחת Shared %1 times שותף %1 פעמים Edited: %1 נערך: %1 You like this אהבת זאת Unlike בטל סימון Like this post סמן את פוסט זה Like אהבתי Are you sure you want to share your own post? האם אתה בטוח כי ברצונך לשתף את הפוסט של עצמך? Share post? לשתף פוסט? Do you want to share %1's post? האם ברצונך לשתף את הפוסט של %1? &Yes, share it &כן, שתף זאת &No &לא Unshare post? לבטל שיתוף פוסט? Do you want to unshare %1's post? האם ברצונך לבטל שיתוף של הפוסט של %1? &Yes, unshare it &כן, אל תשתף זאת WARNING: Delete post? ﬡזﬣﬧﬣ: למחוק פוסט? Are you sure you want to delete this post? האם אתה בטוח כי ברצונך למחוק את פוסט זה? &Yes, delete it &כן, מחק זאת ProfileEditor Profile Editor עריכת פרופיל This is your Pump address זוהי כתובת Pump שלך This is the e-mail address associated with your account, for things such as notifications and password recovery זוהי הכתובת דוא״ל אשר משוייכת עם חשבונך, לצורך דברים כגון התראות או השבת סיסמה Change &E-mail... החלף &דוא״ל... Change &Avatar... החלף &אווטאר... This is your visible name זהו השם הנראה שלך Changing your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. החלפת האווטאר שלך תיצור פוסט בציר זמן שלך עמו. אם תמחק את הפוסט הזה האווטאר שלך יימחק גם כן. &Save Profile &שמור דיוקן &Cancel &ביטול Webfinger ID מזהה Webfinger 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. התמונה הנבחרת אינה כשרה. ProxyDialog Proxy Configuration תצורת Proxy Do not use a proxy אל תשתמש בציר Your proxy username שם משתמש ציר Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. הערה: סיסמה אינה מאוחסנת באופן מאובטח. אם תרצה, באפשרותך להותיר את השדה ריק, ואתה תישאל לסיסמה בעת הפעלה. &Save &שמור &Cancel &ביטול Proxy &Type &טיפוס ציר &Hostname שם מא&רח &Port &פורט Use &Authentication &אימות משתמש &User &משתמש Pass&word &סיסמה Publisher Setting a title helps make the Meanwhile feed more informative קביעת כותרת גורמת לערוץ ״בינתיים״ להיות יותר אינפורמטיבי Title כותרת Add a brief title for the post here (recommended) הוסף כותרת מקוצרת לפוסט כאן (רצוי) Remove הסר Cancel the attachment, and go back to a regular note בטל את התצריף, וחזור למברק רגיל Drafts Public ציבור Followers עוקבים Lists רשימות People... אנשים... To... לכבוד... Select who will see this post בחר מי יראה את פוסט זה Cc... העתק... Select who will get a copy of this post בחר מי יקבל העתק של פוסט זה Picture תצלום Audio אודיו Video וידאו Other as in other kinds of files אחר Ad&d... הוס&ף... Upload media, like pictures or videos העלה מדיה, כגון תצלומים או סרטונים Post verb פרסם Hit Control+Enter to post with the keyboard הקש Control+Enter כדי לפרסם בעזרת המקלדת Cancel ביטול Cancel the post בטל את הפוסט Note started from another application. מברק נפתח מתוך יישום אחר. Ignoring new note request from another application. מתעלם מבקשת מברק אחרת מתוך יישום אחר. Select Picture... בחר תצלום... Find the picture in your folders מצא את התצלום בתוך התיקיות שלך Picture not set תצלום לא נבחר Select Audio File... בחר קובץ אודיו... Find the audio file in your folders מצא את הקובץ אודיו בתוך התיקיות שלך Audio file not set קובץ אודיו לא נבחר Select Video... בחר וידאו... Find the video in your folders מצא את הוידאו בתוך התיקיות שלך Video not set וידאו לא נבחר Select File... בחר קובץ... Find the file in your folders מצא את הקובץ בתוך התיקיות שלך File not set קובץ לא נבחר Error: Already composing שגיאה: כבר מלחין You can't edit a post at this time, because a post is already being composed. אין באפשרותך לערוך פוסט בזמן זה, מכיוון שתגובה אחרת כבר מצויה בשלבי כתיבה. Update עדכן Editing post. עריכת פוסט. It is owned by %1. %1 = a username Editing post עריכת פוסט You can't create a message for %1 at this time, because a post is already being composed. אין באפשרותך ליצור הודעה עבור %1 בזמן זה, מכיוון שפרסום מצוי כעת בשלבי כתיבה. Draft loaded. ERROR: Already composing You can't load a draft at this time, because a post is already being composed. Draft saved. Posting failed. Try again. פרסום נכשל. נסה שוב. Warning: You have no followers yet אזהרה: אין לך עוקבים עדיין You're trying to post to your followers only, but you don't have any followers yet. אתה מנסה לפרסם לעוקבים שלך, אבל אין לך שום עוקבים עדיין. If you post like this, no one will be able to see your message. אם תפרסם בדרך זו, לאף אחד לא תהא האפשרות לראות את הודעתך. Do you want to make the post public instead of followers-only? האם ברצונך להפוך את הפרסום לפומבי במקום לעוקבים-בלבד? &Yes, make it public &כן, הפוך זאת לפומבי &No, post to my followers only &לא, תפרסם לעוקבים שלי בלבד &Cancel, go back to the post &בטל, חזור לפוסט Posting... כעת מפרסם... Updating... כעת מעדכן... Post is empty. פוסט הינו ריק. File not selected. קובץ לא נבחר. Select one image בחירת תמונה יחידה Image files קבצי תמונה Invalid image תמונה לא כשרה The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. פורמט תמונה לא ניתן לאיתור. הסיומת עשויה להיות שגויה, כמו תמונת GIF אשר שמה שונה אל image.jpg או דומה. Select one audio file בחירת קובץ אודיו יחיד Audio files קבצי אודיו Invalid audio file קובץ אודיו לא כשר The audio format cannot be detected. פורמט אודיו לא ניתן לאיתור. Select one video file בחירת קובץ וידאו יחיד Video files קבצי וידאו Invalid video file קובץ וידאו לא כשר The video format cannot be detected. פורמט וידאו לא ניתן לאיתור. Select one file בחירת קובץ יחיד Invalid file קובץ לא כשר The file type cannot be detected. טיפוס קובץ לא ניתן לאיתור. All files כל הקבצים Since you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. מאחר ואנחנו מעלים תמונה, באפשרותך להקטין את מידותיה מעט או לשמור אותה בפורמט דחוס, כמו JPG. File is too big קובץ הינו גדול מדי Dianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. ‏Dianara בעת זו מגבילה העלאות לסך של 10 MiB לכל פוסט, כדי למנוע בעיות אחסון או רשת אפשריות בתוך השרתים. This is a temporary measure, since the servers cannot set their own limits yet. זוהי אמת מידה זמנית, מאחר והשרתים לא מסוגלים לקבוע גבולות בעצמם עדיין. Sorry for the inconvenience. סליחה על אי הנוחות. File not found. Error שגיאה The selected file cannot be accessed: You might not have the necessary permissions. Resolution Image resolution (size) רזולוציה Type טיפוס Size גודל %1 KiB of %2 KiB uploaded %1 קי״ב מתוך %2 קי״ב הועלו PumpController Authorized to use account %1. Getting initial data. מורשה להשתמש בחשבון %1. משיג מידע ראשוני. There is no authorized account. אין חשבון מורשה. Getting list of 'Following'... כעת משיג רשימה של 'עוקב'... Getting list of 'Followers'... כעת משיג רשימה של 'עוקבים'... Getting list of person lists... כעת משיג רשימה של רשימות אישים... Creating person list... כעת יוצר רשימת אישים... Deleting person list... כעת מוחק רשימת אישים... Getting a person list... כעת משיג רשימת אישים... Adding person to list... כעת מוסיף אדם לתוך רשימה... Removing person from list... כעת מסיר אדם מתוך רשימה... Creating group... כעת יוצר קבוצה... Joining group... כעת מצטרף לקבוצה... Leaving group... כעת עוזב קבוצה... Getting likes... כעת משיג סימונים... The comments for this post cannot be loaded due to missing data on the server. התגובות לפוסט זה לא יכולות להיטען בשל מידע חסר מן השרת. Getting comments... כעת משיג תגובות... Getting '%1'... %1 is the name of a feed כעת משיג '%1'... Timeline ציר זמן Messages הודעות Uploading %1 1=filename כעת מעלה %1 HTTP error For the following HTTP error codesyou can check http://en.wikipedia.org/wiki/List_of_HTTP_status_codes in your language שגיאת HTTP Gateway Timeout HTTP 504 error string פקיעת זמן דרך-שער Service Unavailable HTTP 503 error string שירות לא זמין Not Implemented HTTP 501 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 שגיאה בהתחברות אל %1 Unhandled HTTP error code %1 קוד שגיאת HTTP לא מטופל %1 Profile received. פרופיל נתקבל. Followers עוקבים Following עוקב Profile updated. פרופיל עודכן. Received '%1'. %1 is the name of a feed נתקבל '%1'. Loading external image from %1 regardless of SSL errors, as configured... %1 is a hostname כעת טוען תמונות חיצוניות מתוך %1 בלי התחשבות בשגיאות SSL, כפי שהוגדר... Activity פעילות Updating profile... כעת מעדכן פרופיל... Getting site users for %1... %1 is a server name כעת משיג משתמשי אתר של %1... Favorites מועדפים Meanwhile בינתיים Mentions איזכורים Actions פעולות User timeline ציר זמן משתמש Error loading timeline! שגיאה בטעינת ציר זמן! Error loading minor feed! שגיאה בטעינת ערוץ שולי! Unable to verify the address! לא מסוגל לאמת את הכתובת! Bad Gateway HTTP 502 error string דרך-שער רע Server version: %1 גרסת שרת: %1 E-mail updated: %1 דוא״ל עודכן: %1 %1 published successfully. Updating post content... %1 is the type of object: note, image... %1 התפרסם בהצלחה. כעת מעדכן תוכן פוסט... Untitled post %1 published successfully. %1 is a piece of the post פוסט (בלי כותרת) %1 פורסם בהצלחה. Post %1 published successfully. %1 is the title of the post פוסט %1 פורסם בהצלחה. Avatar published successfully. אווטאר פורסם בהצלחה. Untitled post %1 updated successfully. %1 is a piece of the post פוסט (בלי כותרת) %1 עודכן בהצלחה. Post %1 updated successfully. %1 is the title of the post פוסט %1 עודכן בהצלחה. Comment %1 updated successfully. %1 is a piece of the comment תגובה %1 עודכנה בהצלחה. Message liked or unliked successfully. הודעה סומנה או שסימונה בוטל בהצלחה. Likes received. סימונים נתקבלו. Comment %1 posted successfully. %1 is a piece of the comment תגובה %1 פורסמה בהצלחה. 1 comment received. תגובה 1 נתקבלה. %1 comments received. נתקבלו %1 תגובות. Post by %1 shared successfully. 1=author of the post we are sharing פוסט מאת %1 שותף בהצלחה. Adding items... כעת מוסיף פריטים... Message deleted successfully. הודעה נמחקה בהצלחה. Following %1 (%2) successfully. %1 is a person's name, %2 is the ID עוקב אחר %1 ‏(%2) בהצלחה. Stopped following %1 (%2) successfully. %1 is a person's name, %2 is the ID הפסיק לעקוב אחר %1 ‏(%2) בהצלחה. List of 'following' completely received. רשימת 'עוקב' נתקבלה לגמרי. Partial list of 'following' received. רשימה חלקית של 'עוקב' נתקבלה. List of 'followers' completely received. רשימת 'עוקבים' נתקבלה לגמרי. Partial list of 'followers' received. רשימה חלקית של 'עוקבים' נתקבלה. List of 'lists' received. רשימת של 'lists' נתקבלה. List of %1 users received. %1 is a server name רשימה של משתמשי %1 נתקבלה. Person list '%1' created successfully. רשימת אישים '%1' נוצרה בהצלחה. Person list deleted successfully. רשימת אישים נמחקה בהצלחה. Person list received. רשימת אישים נתקבלה. %1 (%2) added to list successfully. 1=contact name, 2=contact ID %1 ‏(%2) התווסף לתוך רשימה בהצלחה. %1 (%2) removed from list successfully. 1=contact name, 2=contact ID %1 ‏(%2) הוסר מתוך רשימה בהצלחה. Group %1 created successfully. קבוצה %1 נוצרה בהצלחה. Group %1 joined successfully. קבוצה %1 צורפה בהצלחה. Left the %1 group successfully. עזב את הקבוצה %1 בהצלחה. File uploaded successfully. Posting message... קובץ הועלה בהצלחה. כעת מפרסם הודעה... Avatar uploaded. אווטאר הועלה. SSL errors in connection to %1! שגיאות SSL בחיבור אל %1! The application is not registered with your server yet. Registering... היישום אינו רשום עם השרת שלך עדיין. כעת רושם... Getting OAuth token... כעת משיג אסימון OAuth... OAuth support error שגיאת תמיכת OAuth Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support. נראה כי להתקנת QOAuth שלך, ספרייה אשר מנוצלת על ידי Dianara, אין תמיכת HMAC-SHA1. You probably need to install the OpenSSL plugin for QCA: %1, %2 or similar. עליך כנראה להתקין תוסף OpenSSL לצורך QCA: %1, %2 או דומה. Authorization error שגיאת אימות There was an OAuth error while trying to get the authorization token. אירעה שגיאת OAuth במהלך ניסיון להשיג אסימון הרשאה. QOAuth error %1 שגיאת QOAuth %1 Application authorized successfully. יישום אושר בהצלחה. OAuth error while authorizing application. שגיאת OAuth במהלך אישור יישום. Waiting for proxy password... כעת ממתין לסיסמת ציר... Still waiting for profile. Trying again... עדיין ממתין לפרופיל. כעת מנסה שוב... %1 attempts %1 ניסיונות 1 attempt ניסיון 1 Some initial data was not received. Restarting initialization... מידע ראשוני מסוים לא נתקבל. מתחיל מחדש פתיחה... Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally. מידע ראשוני מסוים לא נתקבל עובר ניסיונות מספר. משהו עלול להיות משובש בשרת שלך. אפשרי כי עוד תוכל להשתמש בשירות באופן רגיל. All initial data received. Initialization complete. כל המידע הראשוני נתקבל. פתיחה הושלמה. Ready. מוכן. Can't follow %1 at this time. %1 is a user ID לא מסוגל לעקוב אחר %1 ברגע זה. Trying to follow %1. %1 is a user ID כעת מנסה לעקוב אחר %1. Checking address %1 before following... כעת בודק כתובת %1 בטרם עקיבה... SiteUsersList You can get a list of the newest users registered on your server by clicking the button below. באפשרותך להשיג רשימה של המשתמשים הרשומים החדשים ביותר על השרת שלך על ידי הקשה על הלחצן מטה. More resources to find users: עוד משאבים כדי למצוא משתמשים: Wiki page 'Users by language' עמוד Wiki 'משתמשים לפי שפה' PPump user search service at inventati.org שירות חיפוש משתמשים PPump אצל inventati.org List of Followers for the Pump.io Community account רשימת עוקבים שח\ל חשבון קהילת Pump.io Get list of users from your server השג רשימת משתמשים מתוך השרת שלי Close list סגור רשימה Loading... כעת טוען... %1 users in %2 %1 = user count, %2 = server name %1 משתמשים בתוך %2 TimeLine Welcome to Dianara ברוך בואך אל Dianara Dianara is a <b>Pump.io</b> client. &rlm;Dianara היא לקוח <b>Pump.io</b>. If you don't have a Pump account yet, you can get one at the following address, for instance: אם אין לך חשבון Pump עדיין, באפשרותך להשיג חשבון בכתובת הבאה, למשל: Press <b>F1</b> if you want to open the Help window. הקש <b>F1</b> אם ברצונך לפתוח חלון עזרה. First, configure your account from the <b>Settings - Account</b> menu. ראשית, הגדר את החשבון שלך מתוך התפריט <b>הגדרות - חשבון</b>. 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. באפשרותך גם להגדיר את נתוני ותצלום הפרופיל שלך מתוך התפריט <b>הגדרות - ערוך פרופיל</b>. There are tooltips everywhere, so if you hover over a button or a text field with your mouse, you'll probably see some extra information. ישנן תיבות צצות בכל מקום, כך שאם אתה עובר על לחצן או שדה טקסט בעזרת העכבר שלך, אתה עשוי לראות מידע נוסף. Dianara's blog בלוג Dianara Pump.io User Guide מדריך משתמש 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. פוסטים ותגובות אשר אהבת. Newest הכי חדש Newer חדש יותר Older ישן יותר Requesting... כעת מבקש... Loading... כעת טוען... Page %1 of %2. עמוד %1 מתוך %2. Showing %1 posts per page. מציג %1 פוסטים בכל עבוד. %1 posts in total. %1 פוסטים בסך הכל. Click here or press Control+G to jump to a specific page הקש כאן או לחץ Control+G כדי לקפוץ לעמוד מסוים '%1' cannot be updated because a comment is currently being composed. %1 = feed's name '%1' לא ניתן לעדכון מכיוון שתגובה מצויה בשלבי כתיבה. %1 more posts pending for next update. %1 פוסטים נוספים ממתינים לעדכון הבא. Click here to receive them now. לחץ כאן כדי לקבלם עכשיו. There are no posts אין פוסטים Timestamp Invalid timestamp! חותמת זמן לא כשרה! Just now ממש עכשיו In the future בעתיד A minute ago לפני דקה %1 minutes ago לפני %1 דקות An hour ago לפני שעה %1 hours ago לפני %1 שעות Yesterday אתמול %1 days ago לפני %1 ימים A month ago לפני חודש %1 months ago לפני %1 חודשים A year ago לפני שנה %1 years ago לפני %1 שנים UserPosts Posts by %1 פוסטים מאת %1 Loading... כעת טוען... &Close &סגור Received '%1'. נתקבל '%1'. %1 posts פרסומים %1 פוסטים Error loading the timeline שגיאה בטעינת ציר זמן dianara-v1.4.1/translations/dianara_fr.ts0000644000175000017500000062621213212033716016572 0ustar janjan ASActivity Public %1 by %2 1=kind of object: note, comment, etc; 2=author's name ASObject Note Noun, an object type Article Noun, an object type Image Noun, an object type Audio Noun, an object type Video Noun, an object type File Noun, an object type Comment Noun, as in object type: a comment Group Noun, an object type Collection Noun, an object type Other As in: other type of post No detailed location Deleted on %1 and one other and %1 others ASPerson Hometown AccountDialog Your Pump.io address: Get &Verifier Code Verifier code: Enter or paste the verifier code provided by your Pump server here &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 If you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. 1=link to website If you need help: %1 Pump.io User Guide Your address, like username@pumpserver.org 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! &Authorize Application &Cancel Your account is properly configured. Press Unlock if you wish to configure a different account. &Unlock 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 Unable to open web browser! 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. ON THE LEFT should change to ON THE RIGHT in RTL languages Clear &List &Done &Cancel Public Followers Lists People... Selected People AvatarButton Open %1's profile in web browser Open your profile in web browser Send message to %1 Browse messages Stop following Follow Stop following? Are you sure you want to stop following %1? &Yes, stop following &No BannerNotification Timelines were not automatically updated to avoid interruptions. This happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you read Update now Hide this message ColorPicker Change... Choose a color Comment Posted on %1 Modified on %1 Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Edit Modify this comment Delete Erase 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 You can press Control+Enter to send the comment with the keyboard Reload comments Comment Infinitive verb Cancel Press ESC to cancel the comment if there is no text Check for comments Show all %1 comments Comments are not available Error: Already composing You can't edit a comment at this time, because another comment is already being composed. Editing comment Loading comments... Posting comment failed. Try again. An error occurred Sending comment... Updating 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 List Table 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 You can attach only one file. You cannot drop folders here, only a single file. Insert as image? The link you are pasting seems to point to an image. Insert as visible image Insert as link Table Size How many rows (height)? How many columns (width)? Insert a link Type or paste a web address here. You could also select some text first, to turn it into a link. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Invalid link The text you entered does not look like a link. It should start with one of these types: It = the link, from previous sentence &Use it anyway &Enter it again &Cancel link Insert an image from a URL Type or paste the image address here. The link must point to the image file directly. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Yes, but saving a &draft Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog minutes Top Bottom 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 Pro&xy Settings Network configuration Set Up F&ilters Filtering rules Highlighted activities, except mine Any highlighted activity Always Never Comments Left side tabs on left side/west; RTL not affected Right side tabs on right side/east; RTL not affected You are among the recipients of the activity, such as a comment addressed to you. Used also when highlighting posts addressed to you in the timelines. The activity is in reply to something done by you, such as a comment posted in reply to one of your notes. You are the object of the activity, such as someone adding you to a list. The activity is related to one of your objects, such as someone liking one of your posts. Used also when highlighting your own posts in the timelines. Item highlighted due to filtering rules. Item is new. Show snippets in minor feeds Show information for deleted posts No Before avatar Before avatar, subtle After avatar After avatar, subtle Hide duplicated posts Jump to new posts line on update Snippet limit when highlighted Minor feed avatar sizes Show activity icons Avatar size Avatar size in comments Show extended share information Show extra information Highlight post author's comments Highlight your own comments Ignore SSL errors in images Show full size images Show character counter Don't inform followers when following someone Don't inform followers when handling lists As system notifications Using own notifications Don't show notifications seconds Next to a duration, in seconds Notification Style Duration Persistent Notifications Also highlight taskbar entry Notify when receiving: New posts Highlighted posts New activities in minor feed Highlighted activities in minor feed Important errors Default System iconset, if available Show your current avatar Custom icon System Tray Icon &Type S&elect... Custom &Icon Hide window on startup Timelines Posts Composer Privacy Notifications System Tray This is a system notification System notifications are not available! Own notifications will be used. This is a basic notification Select custom icon Post Titles characters This is a suffix, after a number Snippet limit Post Contents Minor Feeds Only for images inserted from web sites. Use with care. Use attachment filename as initial post title Inform only the author when liking things General Options Fonts Colors Dianara stores data in this folder: &Save Configuration &Cancel Image files All files Invalid image The selected image is not valid. ContactCard Hometown Joined: %1 Updated: %1 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 Send Message Browse Messages User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList Type a partial name or ID to find a contact... F&ull List ContactManager username@server.org or https://server.org/username &Enter address to follow: &Follow Reload Followers Reload Following Export Followers Export Following Reload Lists &Neighbors Follo&wers Followin&g &Lists Export list of 'following' to a file Export list of 'followers' to a file Cannot export to this file: Please enter another file name, or choose a different folder. The server seems to be a Pump server, but the account does not exist. %1 doesn't seem to be a Pump server. %1 is a hostname Following this account at this time will probably not work. The %1 server seems unavailable. %1 is a hostname Unknown Refers to server version Error The user address %1 does not exist, or the %2 server is down. Server version Check the address, and keep in mind that usernames are case-sensitive. Do you want to try following this address anyway? (not recommended) Yes, follow anyway No, cancel About to follow %1... DownloadWidget Open Verb, as in: Open the downloaded file Download Save the attached file to your folders Cancel Save File As... All files File not found! Abort download? Do you want to stop downloading the attached file? &Yes, stop &No, continue Download aborted Download completed Attachment downloaded successfully to %1 %1 = filename Open the downloaded attachment with your system's default program for this type of file. Download failed Downloading attachment failed: %1 %1 = filename Downloading %1 KiB... %1 KiB downloaded DraftsManager Draft Manager Load Save Manage drafts... &Delete selected draft &Close Untitled draft Delete draft? Are you sure you want to delete this draft? &Yes, delete it &No EmailChanger Change E-mail Address Change &Cancel E-mail Address: Again: Your Password: E-mail addresses don't match! Password is empty! FDNotifications Show FilterEditor Filter Editor %1 if %2 contains: %3 This explains a filter rule, like: Hide if Author ID contains JohnDoe Here you can set some rules for hiding or highlighting stuff. 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. You could also highlight messages that contain your name. Hide Highlight Post Contents Author ID Application Activity Description Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel if contains &New Filter C&urrent Filters FilterMatchesWidget Content The contents of the post matched Author App Application, short if possible Description FirstRunWizard Welcome Wizard Welcome to Dianara! This wizard will help you get started. You can access this window again at any time from the Help menu. The first step is setting up your account, by using the following button: Configure your &account Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already. &Edit your profile By default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes. Post to &Public by default Open general program &help window &Show this again next time Dianara starts &Close FontPicker Change... Choose a font HelpWidget Basic Help Getting started The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. At this point, your profile, contact lists and timelines will be loaded. You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. Settings You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. Timelines Contents Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. If you're new to Pump.io, take a look at this guide: Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. The main timeline, where you'll see all the stuff posted or shared by the people you follow. Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. Activity timeline, where you'll see your own posts, or posts shared by you. Favorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. Posting New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. It is possible to attach images, audio, video, and general files, like PDF documents, to your post. You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. If you add a specific person to the 'To' list, they will receive your message in their direct messages tab. Choose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. Managing contacts You can see the lists of people you follow, and who follow you from the Contacts tab. There, you can also manage person lists, used mainly to send posts to specific groups of people. You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. Keyboard controls Pump.io User Guide There are seven timelines: The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. LEFT SIDE should change to RIGHT SIDE on RTL languages The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). You can select who will see your post by using the To and Cc buttons. You can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. There is a text field at the top, where you can directly enter addresses of new contacts to follow them. You can also send a direct message (initially private) to that contact from this menu. You can find a list with some Pump.io users and other information here: Users by language Followers of Pump.io Community account The most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. Besides that, you can use: Control+Up/Down/PgUp/PgDown/Home/End to move around the timeline. Control+Left/Right to jump one page in the timeline. Control+G to go to any page in the timeline directly. Control+1/2/3 to switch between the minor feeds. Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. Dianara offers a D-Bus interface that allows some control from other applications. The interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. Command line options Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. Use the --debug parameter to have extra information in your terminal window, about what the program is doing. If your server does not support HTTPS, you can use the --nohttps parameter. If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. &Close ImageViewer Untitled Image &Save As... &Restart Restart animation Fit As in: fit image to window Rotate image to the left RTL: This actually means LEFT, anticlockwise Rotate image to the right RTL: This actually means RIGHT, clockwise &Close Save Image... Close Viewer Downloading full image... Error downloading image! Try again later. 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 LogViewer Log Clear &Log &Close MainWindow Status &Bar &Timeline The main timeline &Activity Your own posts Your favorited posts &Messages Messages sent explicitly to you &Contacts Initializing... Your account is not configured yet. Dianara started. Minor activities done by everyone, such as replying to posts Minor activities addressed to you Minor activities done by you The people you follow, the ones who follow you, and your person lists Press F1 for help Running with Qt v%1. Click here to configure your account &Session Auto-update &Timelines Mark All as Read &Post a Note &Quit &View Locked Panels and Toolbars Side Panel &Toolbar Full &Screen &Log S&ettings Edit &Profile &Account Basic &Help Report a &Bug Pump.io User &Guide Some Pump.io &Tips List of Some Pump.io &Users Pump.io &Network Status Website Toolbar Open the log viewer Auto-updating enabled Auto-updating disabled Proxy password required You have configured a proxy server with authentication, but the password is not set. Enter the password for your proxy server: Starting automatic update of timelines, once every %1 minutes. Stopping automatic update of timelines. Received %1 older posts in '%2'. %1 is a number, %2 = name of a timeline 1 highlighted singular, refers to a post %1 highlighted plural, refers to posts Direct messages By filters 1 more pending to receive. singular, one post %1 more pending to receive. plural, several posts Also: Last update: %1 '%1' updated. %1 is the name of a feed Show Welcome Wizard 1 filtered out. singular, refers to a post %1 filtered out. plural, refers to posts 1 deleted. singular, refers to a post %1 deleted. plural, refers to posts There is 1 new activity. There are %1 new activities. 1 highlighted. singular, refers to an activity %1 highlighted. plural, refers to activities 1 filtered out. singular, refers to one activity %1 filtered out. plural, several activities No new activities. Error storing image! %1 bytes Marking everything as read... Dianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. Closing due to environment shutting down... Quit? You are composing a note or a comment. Do you really want to close Dianara? &Yes, close the program &No Shutting down Dianara... System tray icon is not available. Dianara cannot be hidden in the system tray. Do you want to close the program completely? Timeline updated at %1. Update %1 Your Pump.io account is not configured Link to: %1 With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. English translation by JanKusanagi. TRANSLATORS: Change this with your language and name. If there was another translator before you, add your name after theirs ;) &Configure Dianara &Filters and Highlighting &Help Visit &Website About &Dianara Your biography is empty Click to edit your profile No new posts. Total posts: %1 Favor&ites Received %1 older activities in '%2'. %1 is a number, %2 = name of feed Minor feed updated at %1. 1 more pending to receive. singular, 1 activity %1 more pending to receive. plural, several activities &Hide Window &Show Window There is 1 new post. There are %1 new posts. About Dianara Dianara is a pump.io social networking client. Thanks to all the testers, translators and packagers, who help make Dianara better! MinorFeed Older Activities Get previous minor activities There are no activities to show yet. Get %1 newer As in: Get 3 newer (activities) MinorFeedItem Using %1 Application used to generate this activity To: %1 1=people to whom this activity was sent Cc: %1 1=people to whom this activity was sent as CC Open referenced post MiscHelpers bytes Error: Unable to launch browser The default system web browser could not be executed. You might need to install the XDG utilities. PageSelector Jump to page Page number: &First As in: first page &Last As in: last page Newer As in: newer pages Older As in: older pages &Go &Cancel PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Like Like this post 1 like 1 comment Shared %1 times Shared on %1 Edited: %1 In Using %1 1=Program used for posting or sharing Share Edit Loading image... %1 likes %1 comments Delete Post Noun, not verb Via %1 Posted on %1 1=Date To If you select some text, it will be quoted. Unshare Unshare this post Open post in web browser Click to download the attachment Cc Copy post link to clipboard Normalize text colors &Close Type As in: type of object Modified on %1 Parent As in 'Open the parent post'. Try to use the shortest word! Open the parent post, to which this one replies Comment verb, for the comment button Reply to this post. Share this post with your contacts Modify this post Erase this post Join Group %1 members in the group Image is animated. Click on it to play. Size Image size (resolution) Couldn't load image! Attached Audio Attached Video Attached File %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 Are you sure you want to share your own post? 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 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 &E-mail... Change &Avatar... This is your visible name Changing your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. &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. ProxyDialog Proxy Configuration Do not use a proxy Your proxy username Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. &Save &Cancel Proxy &Type &Hostname &Port Use &Authentication &User Pass&word Publisher Title Select Picture... Find the picture in your folders Add a brief title for the post here (recommended) Remove Cancel the attachment, and go back to a regular note Drafts To... Select who will get a copy of this post Other as in other kinds of files Cancel Cancel the post Picture not set Select Audio File... Find the audio file in your folders Audio file not set Select Video... Find the video in your folders Video not set Select File... Find the file in your folders File not set File not found. Error: Already composing You can't edit a post at this time, because a post is already being composed. Update You can't create a message for %1 at this time, because a post is already being composed. Draft loaded. ERROR: Already composing You can't load a draft at this time, because a post is already being composed. Draft saved. Posting failed. Try again. Warning: You have no followers yet You're trying to post to your followers only, but you don't have any followers yet. If you post like this, no one will be able to see your message. Do you want to make the post public instead of followers-only? &Yes, make it public &Cancel, go back to the post Updating... Post is empty. File not selected. Select one image Image files Select one file Invalid file The file type cannot be detected. All files Since you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. File is too big Dianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. This is a temporary measure, since the servers cannot set their own limits yet. Sorry for the inconvenience. Error The selected file cannot be accessed: It is owned by %1. %1 = a username You might not have the necessary permissions. Resolution Image resolution (size) Type Size %1 KiB of %2 KiB uploaded Invalid image Setting a title helps make the Meanwhile feed more informative Cc... Post verb Note started from another application. Ignoring new note request from another application. Editing post. &No, post to my followers only The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Select one audio file Audio files Invalid audio file The audio format cannot be detected. Select one video file Video files Invalid video file The video format cannot be detected. Posting... Select who will see this post Picture Audio Video Ad&d... Upload media, like pictures or videos 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... Creating group... Joining group... Leaving group... Getting likes... Getting comments... Activity Favorites Meanwhile Mentions Actions Uploading %1 1=filename HTTP error For the following HTTP error codesyou can check http://en.wikipedia.org/wiki/List_of_HTTP_status_codes in your language Gateway Timeout HTTP 504 error string Service Unavailable HTTP 503 error string Not Implemented HTTP 501 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. Followers Following Profile updated. Comment %1 posted successfully. %1 is a piece of the comment Avatar published successfully. 1 comment received. %1 comments received. Post by %1 shared successfully. 1=author of the post we are sharing Adding items... Following %1 (%2) successfully. %1 is a person's name, %2 is the ID Stopped following %1 (%2) successfully. %1 is a person's name, %2 is the ID List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. Person list deleted successfully. Person list received. File uploaded successfully. Posting message... OAuth error while authorizing application. Authorized to use account %1. Getting initial data. There is no authorized account. Updating profile... Getting list of 'Following'... Getting list of 'Followers'... Getting site users for %1... %1 is a server name Getting list of person lists... The comments for this post cannot be loaded due to missing data on the server. Getting '%1'... %1 is the name of a feed Timeline Messages User timeline Error loading timeline! Error loading minor feed! Unable to verify the address! Bad Gateway HTTP 502 error string Server version: %1 E-mail updated: %1 %1 published successfully. Updating post content... %1 is the type of object: note, image... Untitled post %1 published successfully. %1 is a piece of the post Post %1 published successfully. %1 is the title of the post Untitled post %1 updated successfully. %1 is a piece of the post Post %1 updated successfully. %1 is the title of the post Comment %1 updated successfully. %1 is a piece of the comment Message liked or unliked successfully. Likes received. Received '%1'. %1 is the name of a feed Message deleted successfully. List of 'lists' received. List of %1 users received. %1 is a server name Person list '%1' created successfully. %1 (%2) added to list successfully. 1=contact name, 2=contact ID %1 (%2) removed from list successfully. 1=contact name, 2=contact ID Group %1 created successfully. Group %1 joined successfully. Left the %1 group successfully. Avatar uploaded. SSL errors in connection to %1! Loading external image from %1 regardless of SSL errors, as configured... %1 is a hostname The application is not registered with your server yet. Registering... Getting OAuth token... OAuth support error Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support. You probably need to install the OpenSSL plugin for QCA: %1, %2 or similar. Authorization error There was an OAuth error while trying to get the authorization token. QOAuth error %1 Application authorized successfully. Waiting for proxy password... Still waiting for profile. Trying again... %1 attempts 1 attempt Some initial data was not received. Restarting initialization... Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally. All initial data received. Initialization complete. Ready. Can't follow %1 at this time. %1 is a user ID Trying to follow %1. %1 is a user ID Checking address %1 before following... SiteUsersList You can get a list of the newest users registered on your server by clicking the button below. More resources to find users: Wiki page 'Users by language' PPump user search service at inventati.org List of Followers for the Pump.io Community account Get list of users from your server Close list Loading... %1 users in %2 %1 = user count, %2 = server name 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, for instance: Press <b>F1</b> if you want to open the Help window. 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 over a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Pump.io User Guide 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. Newest Newer Older Requesting... Loading... Page %1 of %2. Showing %1 posts per page. %1 posts in total. Click here or press Control+G to jump to a specific page '%1' cannot be updated because a comment is currently being composed. %1 = feed's name %1 more posts pending for next update. Click here to receive them now. There are no posts Timestamp Invalid timestamp! A minute ago %1 minutes ago An hour ago %1 hours ago Just now In the future Yesterday %1 days ago A month ago %1 months ago A year ago %1 years ago UserPosts Posts by %1 Loading... &Close Received '%1'. %1 posts Error loading the timeline dianara-v1.4.1/translations/dianara_ru.ts0000644000175000017500000062621213212033716016611 0ustar janjan ASActivity Public %1 by %2 1=kind of object: note, comment, etc; 2=author's name ASObject Note Noun, an object type Article Noun, an object type Image Noun, an object type Audio Noun, an object type Video Noun, an object type File Noun, an object type Comment Noun, as in object type: a comment Group Noun, an object type Collection Noun, an object type Other As in: other type of post No detailed location Deleted on %1 and one other and %1 others ASPerson Hometown AccountDialog Your Pump.io address: Get &Verifier Code Verifier code: Enter or paste the verifier code provided by your Pump server here &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 If you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. 1=link to website If you need help: %1 Pump.io User Guide Your address, like username@pumpserver.org 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! &Authorize Application &Cancel Your account is properly configured. Press Unlock if you wish to configure a different account. &Unlock 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 Unable to open web browser! 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. ON THE LEFT should change to ON THE RIGHT in RTL languages Clear &List &Done &Cancel Public Followers Lists People... Selected People AvatarButton Open %1's profile in web browser Open your profile in web browser Send message to %1 Browse messages Stop following Follow Stop following? Are you sure you want to stop following %1? &Yes, stop following &No BannerNotification Timelines were not automatically updated to avoid interruptions. This happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you read Update now Hide this message ColorPicker Change... Choose a color Comment Posted on %1 Modified on %1 Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Edit Modify this comment Delete Erase 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 You can press Control+Enter to send the comment with the keyboard Reload comments Comment Infinitive verb Cancel Press ESC to cancel the comment if there is no text Check for comments Show all %1 comments Comments are not available Error: Already composing You can't edit a comment at this time, because another comment is already being composed. Editing comment Loading comments... Posting comment failed. Try again. An error occurred Sending comment... Updating 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 List Table 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 You can attach only one file. You cannot drop folders here, only a single file. Insert as image? The link you are pasting seems to point to an image. Insert as visible image Insert as link Table Size How many rows (height)? How many columns (width)? Insert a link Type or paste a web address here. You could also select some text first, to turn it into a link. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Invalid link The text you entered does not look like a link. It should start with one of these types: It = the link, from previous sentence &Use it anyway &Enter it again &Cancel link Insert an image from a URL Type or paste the image address here. The link must point to the image file directly. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Yes, but saving a &draft Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog minutes Top Bottom 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 Pro&xy Settings Network configuration Set Up F&ilters Filtering rules Highlighted activities, except mine Any highlighted activity Always Never Comments Left side tabs on left side/west; RTL not affected Right side tabs on right side/east; RTL not affected You are among the recipients of the activity, such as a comment addressed to you. Used also when highlighting posts addressed to you in the timelines. The activity is in reply to something done by you, such as a comment posted in reply to one of your notes. You are the object of the activity, such as someone adding you to a list. The activity is related to one of your objects, such as someone liking one of your posts. Used also when highlighting your own posts in the timelines. Item highlighted due to filtering rules. Item is new. Show snippets in minor feeds Show information for deleted posts No Before avatar Before avatar, subtle After avatar After avatar, subtle Hide duplicated posts Jump to new posts line on update Snippet limit when highlighted Minor feed avatar sizes Show activity icons Avatar size Avatar size in comments Show extended share information Show extra information Highlight post author's comments Highlight your own comments Ignore SSL errors in images Show full size images Show character counter Don't inform followers when following someone Don't inform followers when handling lists As system notifications Using own notifications Don't show notifications seconds Next to a duration, in seconds Notification Style Duration Persistent Notifications Also highlight taskbar entry Notify when receiving: New posts Highlighted posts New activities in minor feed Highlighted activities in minor feed Important errors Default System iconset, if available Show your current avatar Custom icon System Tray Icon &Type S&elect... Custom &Icon Hide window on startup Timelines Posts Composer Privacy Notifications System Tray This is a system notification System notifications are not available! Own notifications will be used. This is a basic notification Select custom icon Post Titles characters This is a suffix, after a number Snippet limit Post Contents Minor Feeds Only for images inserted from web sites. Use with care. Use attachment filename as initial post title Inform only the author when liking things General Options Fonts Colors Dianara stores data in this folder: &Save Configuration &Cancel Image files All files Invalid image The selected image is not valid. ContactCard Hometown Joined: %1 Updated: %1 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 Send Message Browse Messages User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList Type a partial name or ID to find a contact... F&ull List ContactManager username@server.org or https://server.org/username &Enter address to follow: &Follow Reload Followers Reload Following Export Followers Export Following Reload Lists &Neighbors Follo&wers Followin&g &Lists Export list of 'following' to a file Export list of 'followers' to a file Cannot export to this file: Please enter another file name, or choose a different folder. The server seems to be a Pump server, but the account does not exist. %1 doesn't seem to be a Pump server. %1 is a hostname Following this account at this time will probably not work. The %1 server seems unavailable. %1 is a hostname Unknown Refers to server version Error The user address %1 does not exist, or the %2 server is down. Server version Check the address, and keep in mind that usernames are case-sensitive. Do you want to try following this address anyway? (not recommended) Yes, follow anyway No, cancel About to follow %1... DownloadWidget Open Verb, as in: Open the downloaded file Download Save the attached file to your folders Cancel Save File As... All files File not found! Abort download? Do you want to stop downloading the attached file? &Yes, stop &No, continue Download aborted Download completed Attachment downloaded successfully to %1 %1 = filename Open the downloaded attachment with your system's default program for this type of file. Download failed Downloading attachment failed: %1 %1 = filename Downloading %1 KiB... %1 KiB downloaded DraftsManager Draft Manager Load Save Manage drafts... &Delete selected draft &Close Untitled draft Delete draft? Are you sure you want to delete this draft? &Yes, delete it &No EmailChanger Change E-mail Address Change &Cancel E-mail Address: Again: Your Password: E-mail addresses don't match! Password is empty! FDNotifications Show FilterEditor Filter Editor %1 if %2 contains: %3 This explains a filter rule, like: Hide if Author ID contains JohnDoe Here you can set some rules for hiding or highlighting stuff. 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. You could also highlight messages that contain your name. Hide Highlight Post Contents Author ID Application Activity Description Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel if contains &New Filter C&urrent Filters FilterMatchesWidget Content The contents of the post matched Author App Application, short if possible Description FirstRunWizard Welcome Wizard Welcome to Dianara! This wizard will help you get started. You can access this window again at any time from the Help menu. The first step is setting up your account, by using the following button: Configure your &account Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already. &Edit your profile By default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes. Post to &Public by default Open general program &help window &Show this again next time Dianara starts &Close FontPicker Change... Choose a font HelpWidget Basic Help Getting started The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. At this point, your profile, contact lists and timelines will be loaded. You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. Settings You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. Timelines Contents Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. If you're new to Pump.io, take a look at this guide: Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. The main timeline, where you'll see all the stuff posted or shared by the people you follow. Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. Activity timeline, where you'll see your own posts, or posts shared by you. Favorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. Posting New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. It is possible to attach images, audio, video, and general files, like PDF documents, to your post. You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. If you add a specific person to the 'To' list, they will receive your message in their direct messages tab. Choose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. Managing contacts You can see the lists of people you follow, and who follow you from the Contacts tab. There, you can also manage person lists, used mainly to send posts to specific groups of people. You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. Keyboard controls Pump.io User Guide There are seven timelines: The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. LEFT SIDE should change to RIGHT SIDE on RTL languages The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). You can select who will see your post by using the To and Cc buttons. You can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. There is a text field at the top, where you can directly enter addresses of new contacts to follow them. You can also send a direct message (initially private) to that contact from this menu. You can find a list with some Pump.io users and other information here: Users by language Followers of Pump.io Community account The most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. Besides that, you can use: Control+Up/Down/PgUp/PgDown/Home/End to move around the timeline. Control+Left/Right to jump one page in the timeline. Control+G to go to any page in the timeline directly. Control+1/2/3 to switch between the minor feeds. Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. Dianara offers a D-Bus interface that allows some control from other applications. The interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. Command line options Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. Use the --debug parameter to have extra information in your terminal window, about what the program is doing. If your server does not support HTTPS, you can use the --nohttps parameter. If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. &Close ImageViewer Untitled Image &Save As... &Restart Restart animation Fit As in: fit image to window Rotate image to the left RTL: This actually means LEFT, anticlockwise Rotate image to the right RTL: This actually means RIGHT, clockwise &Close Save Image... Close Viewer Downloading full image... Error downloading image! Try again later. 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 LogViewer Log Clear &Log &Close MainWindow Status &Bar &Timeline The main timeline &Activity Your own posts Your favorited posts &Messages Messages sent explicitly to you &Contacts Initializing... Your account is not configured yet. Dianara started. Minor activities done by everyone, such as replying to posts Minor activities addressed to you Minor activities done by you The people you follow, the ones who follow you, and your person lists Press F1 for help Running with Qt v%1. Click here to configure your account &Session Auto-update &Timelines Mark All as Read &Post a Note &Quit &View Locked Panels and Toolbars Side Panel &Toolbar Full &Screen &Log S&ettings Edit &Profile &Account Basic &Help Report a &Bug Pump.io User &Guide Some Pump.io &Tips List of Some Pump.io &Users Pump.io &Network Status Website Toolbar Open the log viewer Auto-updating enabled Auto-updating disabled Proxy password required You have configured a proxy server with authentication, but the password is not set. Enter the password for your proxy server: Starting automatic update of timelines, once every %1 minutes. Stopping automatic update of timelines. Received %1 older posts in '%2'. %1 is a number, %2 = name of a timeline 1 highlighted singular, refers to a post %1 highlighted plural, refers to posts Direct messages By filters 1 more pending to receive. singular, one post %1 more pending to receive. plural, several posts Also: Last update: %1 '%1' updated. %1 is the name of a feed Show Welcome Wizard 1 filtered out. singular, refers to a post %1 filtered out. plural, refers to posts 1 deleted. singular, refers to a post %1 deleted. plural, refers to posts There is 1 new activity. There are %1 new activities. 1 highlighted. singular, refers to an activity %1 highlighted. plural, refers to activities 1 filtered out. singular, refers to one activity %1 filtered out. plural, several activities No new activities. Error storing image! %1 bytes Marking everything as read... Dianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. Closing due to environment shutting down... Quit? You are composing a note or a comment. Do you really want to close Dianara? &Yes, close the program &No Shutting down Dianara... System tray icon is not available. Dianara cannot be hidden in the system tray. Do you want to close the program completely? Timeline updated at %1. Update %1 Your Pump.io account is not configured Link to: %1 With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. English translation by JanKusanagi. TRANSLATORS: Change this with your language and name. If there was another translator before you, add your name after theirs ;) &Configure Dianara &Filters and Highlighting &Help Visit &Website About &Dianara Your biography is empty Click to edit your profile No new posts. Total posts: %1 Favor&ites Received %1 older activities in '%2'. %1 is a number, %2 = name of feed Minor feed updated at %1. 1 more pending to receive. singular, 1 activity %1 more pending to receive. plural, several activities &Hide Window &Show Window There is 1 new post. There are %1 new posts. About Dianara Dianara is a pump.io social networking client. Thanks to all the testers, translators and packagers, who help make Dianara better! MinorFeed Older Activities Get previous minor activities There are no activities to show yet. Get %1 newer As in: Get 3 newer (activities) MinorFeedItem Using %1 Application used to generate this activity To: %1 1=people to whom this activity was sent Cc: %1 1=people to whom this activity was sent as CC Open referenced post MiscHelpers bytes Error: Unable to launch browser The default system web browser could not be executed. You might need to install the XDG utilities. PageSelector Jump to page Page number: &First As in: first page &Last As in: last page Newer As in: newer pages Older As in: older pages &Go &Cancel PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Like Like this post 1 like 1 comment Shared %1 times Shared on %1 Edited: %1 In Using %1 1=Program used for posting or sharing Share Edit Loading image... %1 likes %1 comments Delete Post Noun, not verb Via %1 Posted on %1 1=Date To If you select some text, it will be quoted. Unshare Unshare this post Open post in web browser Click to download the attachment Cc Copy post link to clipboard Normalize text colors &Close Type As in: type of object Modified on %1 Parent As in 'Open the parent post'. Try to use the shortest word! Open the parent post, to which this one replies Comment verb, for the comment button Reply to this post. Share this post with your contacts Modify this post Erase this post Join Group %1 members in the group Image is animated. Click on it to play. Size Image size (resolution) Couldn't load image! Attached Audio Attached Video Attached File %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 Are you sure you want to share your own post? 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 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 &E-mail... Change &Avatar... This is your visible name Changing your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. &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. ProxyDialog Proxy Configuration Do not use a proxy Your proxy username Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. &Save &Cancel Proxy &Type &Hostname &Port Use &Authentication &User Pass&word Publisher Title Select Picture... Find the picture in your folders Add a brief title for the post here (recommended) Remove Cancel the attachment, and go back to a regular note Drafts To... Select who will get a copy of this post Other as in other kinds of files Cancel Cancel the post Picture not set Select Audio File... Find the audio file in your folders Audio file not set Select Video... Find the video in your folders Video not set Select File... Find the file in your folders File not set File not found. Error: Already composing You can't edit a post at this time, because a post is already being composed. Update You can't create a message for %1 at this time, because a post is already being composed. Draft loaded. ERROR: Already composing You can't load a draft at this time, because a post is already being composed. Draft saved. Posting failed. Try again. Warning: You have no followers yet You're trying to post to your followers only, but you don't have any followers yet. If you post like this, no one will be able to see your message. Do you want to make the post public instead of followers-only? &Yes, make it public &Cancel, go back to the post Updating... Post is empty. File not selected. Select one image Image files Select one file Invalid file The file type cannot be detected. All files Since you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. File is too big Dianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. This is a temporary measure, since the servers cannot set their own limits yet. Sorry for the inconvenience. Error The selected file cannot be accessed: It is owned by %1. %1 = a username You might not have the necessary permissions. Resolution Image resolution (size) Type Size %1 KiB of %2 KiB uploaded Invalid image Setting a title helps make the Meanwhile feed more informative Cc... Post verb Note started from another application. Ignoring new note request from another application. Editing post. &No, post to my followers only The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Select one audio file Audio files Invalid audio file The audio format cannot be detected. Select one video file Video files Invalid video file The video format cannot be detected. Posting... Select who will see this post Picture Audio Video Ad&d... Upload media, like pictures or videos 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... Creating group... Joining group... Leaving group... Getting likes... Getting comments... Activity Favorites Meanwhile Mentions Actions Uploading %1 1=filename HTTP error For the following HTTP error codesyou can check http://en.wikipedia.org/wiki/List_of_HTTP_status_codes in your language Gateway Timeout HTTP 504 error string Service Unavailable HTTP 503 error string Not Implemented HTTP 501 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. Followers Following Profile updated. Comment %1 posted successfully. %1 is a piece of the comment Avatar published successfully. 1 comment received. %1 comments received. Post by %1 shared successfully. 1=author of the post we are sharing Adding items... Following %1 (%2) successfully. %1 is a person's name, %2 is the ID Stopped following %1 (%2) successfully. %1 is a person's name, %2 is the ID List of 'following' completely received. Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. Person list deleted successfully. Person list received. File uploaded successfully. Posting message... OAuth error while authorizing application. Authorized to use account %1. Getting initial data. There is no authorized account. Updating profile... Getting list of 'Following'... Getting list of 'Followers'... Getting site users for %1... %1 is a server name Getting list of person lists... The comments for this post cannot be loaded due to missing data on the server. Getting '%1'... %1 is the name of a feed Timeline Messages User timeline Error loading timeline! Error loading minor feed! Unable to verify the address! Bad Gateway HTTP 502 error string Server version: %1 E-mail updated: %1 %1 published successfully. Updating post content... %1 is the type of object: note, image... Untitled post %1 published successfully. %1 is a piece of the post Post %1 published successfully. %1 is the title of the post Untitled post %1 updated successfully. %1 is a piece of the post Post %1 updated successfully. %1 is the title of the post Comment %1 updated successfully. %1 is a piece of the comment Message liked or unliked successfully. Likes received. Received '%1'. %1 is the name of a feed Message deleted successfully. List of 'lists' received. List of %1 users received. %1 is a server name Person list '%1' created successfully. %1 (%2) added to list successfully. 1=contact name, 2=contact ID %1 (%2) removed from list successfully. 1=contact name, 2=contact ID Group %1 created successfully. Group %1 joined successfully. Left the %1 group successfully. Avatar uploaded. SSL errors in connection to %1! Loading external image from %1 regardless of SSL errors, as configured... %1 is a hostname The application is not registered with your server yet. Registering... Getting OAuth token... OAuth support error Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support. You probably need to install the OpenSSL plugin for QCA: %1, %2 or similar. Authorization error There was an OAuth error while trying to get the authorization token. QOAuth error %1 Application authorized successfully. Waiting for proxy password... Still waiting for profile. Trying again... %1 attempts 1 attempt Some initial data was not received. Restarting initialization... Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally. All initial data received. Initialization complete. Ready. Can't follow %1 at this time. %1 is a user ID Trying to follow %1. %1 is a user ID Checking address %1 before following... SiteUsersList You can get a list of the newest users registered on your server by clicking the button below. More resources to find users: Wiki page 'Users by language' PPump user search service at inventati.org List of Followers for the Pump.io Community account Get list of users from your server Close list Loading... %1 users in %2 %1 = user count, %2 = server name 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, for instance: Press <b>F1</b> if you want to open the Help window. 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 over a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Pump.io User Guide 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. Newest Newer Older Requesting... Loading... Page %1 of %2. Showing %1 posts per page. %1 posts in total. Click here or press Control+G to jump to a specific page '%1' cannot be updated because a comment is currently being composed. %1 = feed's name %1 more posts pending for next update. Click here to receive them now. There are no posts Timestamp Invalid timestamp! A minute ago %1 minutes ago An hour ago %1 hours ago Just now In the future Yesterday %1 days ago A month ago %1 months ago A year ago %1 years ago UserPosts Posts by %1 Loading... &Close Received '%1'. %1 posts Error loading the timeline dianara-v1.4.1/translations/dianara_de.qm0000644000175000017500000037701613202447606016555 0ustar janjanvTIvyzV1eW#e %UBSЅM"y(*E*0\+(B+vcv++c+c+2>cs<<+>R~@)DQYBjH>!H:*HHk IamKKPLb~Lbp~MeP/NNOP7P7R;S ATYVTBTU_VTV*BV*QVVxWj]"WLkX,Xƥ)>YU'Zy%>[ %[!\\3j AUkn$q׎awjs(yTBybxzrz>$?nhN-c]X>G4t^&@.w"47-̸gՅSBI5 ҥH~:7[*C{NX_!Abdr ݣYEss$Yk&u-!/0`<#)Z<DIn^M$GMP3Pk0QE:.RS`V80\q.lo@p`N=#Ĕ,8sTrhh>o3^qtd OpKΨ%& 2c *)| Uf*ʑ#%%e3.> KL=ՏQMRxNdsd.-hBhpH>tɥwvZ4np/V$ϠN/q؞bdyM |53T88YS")#J /Q qr }mm vSr$1($1Tt(J.s0c\0c+666'70HyEA9M.XR@*mZN.Zfd'&i6dijC6l#RsCElwh3Fx2zt{e}z7.Qv&Nn}^yBxbרч݋¿lf¿7Ì P^jn˺>3\q(AqQ޷J|?_ #n-E)q`n%? ʳr 1V^ DS bXL  |8#n:W$W[+h ;6U+>"B0P ?`3Ea$fXUr) |ڛ}K::tII7]< FZbU>8]Kp@QI*ISIk6KiU,&,M<33Ľnԁv^`"*c mS4tͣU3g3guրvR@z7F7XW ZF ;V%<#Sp,4>1hKTKTwLܤ RMRAV|<]L.]Wa(wbcfy^h9~hTTjsu_yzߺ{9{|7}̏Ig 0,.{FzVVs[h>4L>T.[tRtG@:4LAúX~\wf0)0WFƨ2Xon:Z<N_أ\Ë*)+D-^i4uV 4u^X4uwj9${/<.Z<.<.u=LAt.GCKGmbLI$M?$mORQM.T$Vхw3ZPZzuZf;< fCgkhCmnfp{8Ir7wuvy`Aa/\ ,NTnc9S>G9NkVQ,1u̐}*DCXV4'ґu ڑڱ!.1[x[Cְ#DkM0UQ1;=;Xl<T<<^<vS/WcmgPjlqNipHrt/A{k_l$]jžo~XoOENxRO nex<ȯB>ҕ>~nNqji)o."nQ% [iJnEFU"i#N l)2K=2?56~~F MJ$'MJMrch\]ee7IkZ*s#{isn5E|2r|Q?UX#'(per#B>eB`/ qF`OFrnť4Yʪ$-]P琊R5QiRvNq !D -Q۶ 0\~ 0u~Q 1`#G 6 q 7w: <.| Mg P3 Sgj ^nт r% s w>sY |  ~?  thg N 4,= .7  ZC I# IL IG I IU IA ICI IY Ib )O ~ I  ) yO Nw , o+w W tM `_ ^ '_M tGL ՁO 3G{ ֓3 N %| T%$ x ؄l ~ 0N, Y4 :,L N ( > ~ň ~ҿ ~2 z c>i gG  "e_B "< ,i4K -֮ 0W 0d 5<s^ 7"N <خ = =/nm @V A# ED' XQ/ Ya0k bX, bXp bK eTg f(6 f*2 mmTG o>5 y[>[w |6f P/ݡ ! q33 SLV #c< DB V" ̕ 7 u h )e'  U+1 ( sd* 5 j{ OC أV y=x U $ CJR m 20 Ғ$f >X (=K2 - UZS / 6@ =x1 Bd2 E! RV YW [c( ]ӡn `uZ d.[ eMf j8Z {j4 {_ mx ?k Y rw V xv  q= #. sɯ ͡< =} (c Б*X ۅy Z < y |SR7 7~| I w= 7 a2> =|j 7 Е3x #pn ) .> Bs Ga Nn V4 _P b e&^' gX h1vg iFC 1 @E |3 LB   >v ^n q ˽+9 7;) = b^ J+  ^ `J RVz3N 2RmTcb)+jMQ/]D[pR/7RT3ZX c]gOIlNlIflpllZljmhn?m e$8T%8T8TC8TGo^[>$Q'iu'N͌Tn'/PZ^U~$A%}-ZP!.UH O7yyx"R/ #uu`^;_^izCI'5%O'5%\'5%>'E) +<,02A BtnTB}~DCE.dF$BJXPd TH_}9`pDbu_\eY.ub`tiRB2^1/~nɣ.g,z:@^V]a~d3؞¹eBZ$We+~P1 *dzZ"^ڨdy)B#{ޝi%1 von %2%1 by %2 ASActivityffentlichPublic ASActivityArtikelArticleASObject AudioAudioASObjectSammlung CollectionASObjectKommentarCommentASObjectGelscht am %1 Deleted on %1ASObject DateiFileASObject GruppeGroupASObjectBildImageASObject kein genauer OrtNo detailed locationASObjectMitteilungNoteASObjectSonstigesOtherASObject VideoVideoASObjectund %1 weitere and %1 othersASObject*und ein(e) Weitere(r) and one otherASObjectHeimatortHometownASPerson.Anwendung &autorisieren&Authorize Application AccountDialog&Abbrechen&Cancel AccountDialog Daten &speichern &Save Details AccountDialog&Entsperren&Unlock AccountDialogEin Browser wird jetzt gestartet, um den Verifizierungskode zu erhaltenAA web browser will start now, where you can get the verifier code AccountDialog&Konto konfigurierenAccount Configuration AccountDialogNachdem Sie diese Schaltflche gedrckt haben, wird sich ein Webbrowser ffnen, um die Autorisierung fr Dianara anzufordernYAfter clicking this button, a web browser will open, requesting authorization for Dianara AccountDialog\Dianara hat autorisierten Zugang zu Ihre Daten)Dianara is authorized to access your data AccountDialogHier den von Ihrem Pump-Server bereitgestellten Verifizierungskode eingeben oder einfgenBEnter or paste the verifier code provided by your Pump server here AccountDialog|Geben Sie zuerst Ihre Webfinger-ID (Ihre Pump.io Adresse) ein.5First, enter your Webfinger ID, your pump.io address. AccountDialog8&Verifizierungskode erhaltenGet &Verifier Code AccountDialogFalls der Browser sich nicht automatisch ffnet, kopieren Sie die folgende Adresse von HandEIf the browser doesn't open automatically, copy this address manually AccountDialogFalls Sie noch kein Konto haben, knnen Sie sich unter %1 anmelden. Dieser Link zeigt auf einen zuflligen ffentlichen Server.sIf you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. AccountDialog:Falls Sie Hilfe bentigen: %1If you need help: %1 AccountDialogFr ein Profil auf 'https://pump.beispiel/benutzer', ist Ihre zugehrige Adresse 'benutzer@pump.beispiel'_If your profile is at https://pump.example/yourname, then your address is yourname@pump.example AccountDialogSobald Sie Dianara von der Oberflche Ihres Pump-Servers aus autorisiert haben, werden Sie einen Code namens 'VERIFIER' erhalten. Kopieren Sie diesen und fgen Sie ihn in das Feld unterhalb ein.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. AccountDialogKlicken Sie 'Entsperren', falls Sie ein anderes Konto konfigurieren mchten.:Press Unlock if you wish to configure a different account. AccountDialog"Pump.io LeitfadenPump.io User Guide AccountDialog6Verifizierungskode ist leerVerifier code is empty AccountDialog&Verifizierungskode:Verifier code: AccountDialog<Ihre Pump-Adresse ist ungltigYour Pump address is invalid AccountDialog*Ihre Pump.io Adresse:Your Pump.io address: AccountDialogFIhr Konto ist korrekt konfiguriert.$Your account is properly configured. AccountDialogIhre Adresse sieht aus wie 'Benutzer@pumpeserver.org'. Sie knnen diese ber die Web-Oberflche in Ihrem Profil finden.kYour address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. AccountDialogRIhre Adresse, als benutzer@pumpserver.org*Your address, like username@pumpserver.org AccountDialog.Zur Auswahl &hinzufgen&Add to SelectedAudienceSelector&Abbrechen&CancelAudienceSelector&Fertig&DoneAudienceSelector$CC-Empfnger Liste 'Cc' ListAudienceSelectorEmpfnger Liste 'To' ListAudienceSelectorAlle Kontakte All ContactsAudienceSelector&Liste leeren Clear &ListAudienceSelectorAnhngerschaft FollowersAudienceSelector ListenListsAudienceSelectorLeute... People...AudienceSelectorffentlichPublicAudienceSelectorTWhlen Sie Kontakte aus der Liste links aus. Sie knnen diese mit der Maus ziehen, einfach oder doppelt anklicken oder sie auswhlen und die Schaltflche unten verwenden.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(Ausgewhlte KontakteSelected PeopleAudienceSelector &Nein&No AvatarButton,&Ja, nicht mehr folgen&Yes, stop following AvatarButtonlSind Sie sicher, dass Sie %1 nicht mehr folgen wollen?+Are you sure you want to stop following %1? AvatarButton.Nachrichten durchsuchenBrowse messages AvatarButton FolgenFollow AvatarButton>Profil von %1 im Browser ffnen Open %1's profile in web browser AvatarButton<Ihres Profil im Browser ffnen Open your profile in web browser AvatarButton4Sende eine Nachricht an %1Send message to %1 AvatarButton"Nicht mehr folgenStop following AvatarButton$Nicht mehr folgen?Stop following? AvatarButton0Diesen Hinweis verbergenHide this messageBannerNotificationbDies geschieht, wenn eine automatische Zeitleistenaktualisierung ansteht und Sie sich gerade nicht oben auf der ersten Seite befinden, um Unterbrechungen beim Lesen zu vermeidenThis happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you readBannerNotificationDie Zeitleisten wurden nicht automatisch aktualisiert, um Unterbrechungen zu verhindern.@Timelines were not automatically updated to avoid interruptions.BannerNotification&Jetzt aktualisieren Update nowBannerNotificationWechseln... Change... ColorPicker*Whlen Sie eine FarbeChoose a color ColorPicker6%1 gefllt dieser Kommentar%1 like this commentComment6%1 gefllt dieser Kommentar%1 likes this commentComment &Nein&NoComment&Ja, lschen&Yes, delete itCommenttSind Sie sicher, dass Sie diesen Kommentar lschen wollen?-Are you sure you want to delete this comment?CommentLschenDeleteCommentBearbeitenEditComment0Diesen Kommentar lschenErase this commentCommentFavorisierenLikeComment\Diesen Kommentar favorisieren / defavorisierenLike or unlike this commentCommentAm %1 verndertModified on %1Comment6Diesen Kommentar bearbeitenModify this commentComment(Am %1 verffentlicht Posted on %1CommentZitierenQuoteCommentFAntwort mit Zitat dieses KommentarsReply quoting this commentCommentDefavorisierenUnlikeComment6WARNUNG: Kommentar lschen?WARNING: Delete comment?CommentAbbrechenCancelCommenterBlock2Auf Kommentare berprfenCheck for commentsCommenterBlockKommentierenCommentCommenterBlock&Kommentar ist leer.Comment is empty.CommenterBlock>Kommentare sind nicht verfgbarComments are not availableCommenterBlock(Kommentar bearbeitenEditing commentCommenterBlock>Fehler: Bereits am KommentierenError: Already composingCommenterBlock*Rufe Kommentare ab...Loading comments...CommenterBlockVerffentlichung des Kommentars schlug fehl. Versuchen Sie es noch einmal.#Posting comment failed. Try again.CommenterBlockDrcken Sie ESC um den Kommentar abzubrechen, wenn das Textfeld leer ist3Press ESC to cancel the comment if there is no textCommenterBlock(Kommentare neu ladenReload commentsCommenterBlock4Kommentar wird gesendet...Sending comment...CommenterBlock0Zeige alle %1 KommentareShow all %1 commentsCommenterBlock<Kommentar wird aktualisiert...Updating comment...CommenterBlockSie knnen Strg+Eingabe auf der Tastatur drcken, um den Kommentar abzusendenAYou can press Control+Enter to send the comment with the keyboardCommenterBlockSie knnen jetzt keinen Kommentar bearbeiten, weil bereits ein anderer Kommentar erstellt wird.YYou can't edit a comment at this time, because another comment is already being composed.CommenterBlockLink &verwerfen &Cancel linkComposer$Nochmals &eingeben&Enter it againComposer&Format&FormatComposer &Nein&NoComposer&&Trotzdem verwenden&Use it anywayComposer&Ja, abbrechen&Yes, cancel itComposertSind Sie sicher, dass Sie diesen Beitrag verwerfen wollen?-Are you sure you want to cancel this message?ComposerFettBoldComposer$Beitrag verwerfen?Cancel message?ComposerKlicken Sie hier oder drcken Sie Strg+N um eine Mitteilung zu schreiben.../Click here or press Control+N to post a note...Composer*Fehler: Ungltige URLError: Invalid URLComposer Format FormattingComposerVorspannHeaderComposer6Wie viele Spalten (Breite)?How many columns (width)?Composer0Wie viele Zeilen (Hhe)?How many rows (height)?ComposerLink hinzufgen Insert a linkComposerHFgen Sie ein Bild aus einer URL einInsert an image from a URLComposerDBild von einer Webseite hinzufgenInsert an image from a web siteComposer$Als Bild einfgen?Insert as image?Composer"Als Link einfgenInsert as linkComposer8Als sichtbares Bild einfgenInsert as visible imageComposer Linie hinzufgen Insert lineComposerUngltiger Link Invalid linkComposerXEr sollte mit einem dieser Lettern beginnen:(It should start with one of these types:Composer KursivItalicComposer ListeListComposerLink hinzufgen Make a linkComposerHLink aus ausgewhltem Text erstellenMake a link from selected textComposer NormalNormalComposer>Text ohne Formatierung einfgenPaste Text Without FormattingComposer0Vorformattierter BereichPreformatted blockComposer Zitat Quote blockComposerDurchgestrichen StrikethroughComposerSymboleSymbolsComposerTabelleTableComposerTabellengre Table SizeComposer.TextformateinstellungenText Formatting OptionsComposerDie Adresse, dass Sie hinzugefgt haben (%1) ist nicht gltig. Adressen sollen mit http:// oder https:// anfangen`The address you entered (%1) is not valid. Image addresses should begin with http:// or https://ComposerDer Link den Sie gerade einfgen scheint auf eine Bilddatei zu zeigen.4The link you are pasting seems to point to an image.ComposerdDer eingegebene Text sieht nicht wie ein Link aus./The text you entered does not look like a link.ComposerDGeben Sie hier einen Kommentar einType a comment hereComposer@Schreiben Sie hier Ihren BeitragType a message here to post itComposerTippen oder fgen Sie hier eine Webadresse ein. Der ausgewhlte Text (%1) wird in einen Link umgewandelt.UType or paste a web address here. The selected text (%1) will be converted to a link.ComposerTippen oder fgen Sie hier eine Webadresse ein. Sie knnen auch zuerst Text auswhlen, um diesen in einen Link umzuwandeln.`Type or paste a web address here. You could also select some text first, to turn it into a link.ComposerTippen oder fgen Sie hier die Adresse fr ein Bild ein. Dieser Link muss direkt auf die Bilddatei verweisen.UType or paste the image address here. The link must point to the image file directly.ComposerUnterstrichen UnderlineComposer&Abbrechen&Cancel ConfigDialog*Be&wegliche Tableiste &Movable tabs ConfigDialogJBeitrge &pro Seite, Haupt-Zeitleiste&Posts per page, main timeline ConfigDialog0Konfiguration &speichern&Save Configuration ConfigDialog*Stelle fr &Tableiste&Tabs position ConfigDialogAlle Dateien All files ConfigDialog ImmerAlways ConfigDialog@Alle hervorgehobenen AktivittenAny highlighted activity ConfigDialog8Als SystembenachrichtigungenAs system notifications ConfigDialogAvatargre Avatar size ConfigDialog4Avatargre in KommentarenAvatar size in comments ConfigDialog UntenBottom ConfigDialog FarbenColors ConfigDialogKommentareComments ConfigDialogBeitragseditorComposer ConfigDialog6Benutzerdef&iniertes Symbol Custom &Icon ConfigDialog4Benutzerdefiniertes Symbol Custom icon ConfigDialogStandardDefault ConfigDialogRDianara speichert Daten in diesen Ordner:#Dianara stores data in this folder: ConfigDialogv'Anhngerschaft' nicht informieren, wenn ich jemandem folge-Don't inform followers when following someone ConfigDialogz'Anhngerschaft' nicht informieren, wenn ich Listen bearbeite*Don't inform followers when handling lists ConfigDialog>Keine Benachrichtigungen zeigenDon't show notifications ConfigDialogFilterregelnFiltering rules ConfigDialogSchriftartenFonts ConfigDialog&Allgemeine OptionenGeneral Options ConfigDialog6Doppelte Beitrge verbergenHide duplicated posts ConfigDialog>Fenster beim Starten versteckenHide window on startup ConfigDialogRKommentare des Beitragsautors hervorheben Highlight post author's comments ConfigDialog:Eigene Kommentare hervorhebenHighlight your own comments ConfigDialogPhervorgehobenen Aktivitten im Nebenfeed$Highlighted activities in minor feed ConfigDialogNHervorgehobene Aktivitten, auer meine#Highlighted activities, except mine ConfigDialog2hervorgehobenen BeitrgenHighlighted posts ConfigDialogHSSL-Fehler in Bilddateien ignorierenIgnore SSL errors in images ConfigDialogBilddateien Image files ConfigDialogWichtige FehlerImportant errors ConfigDialogrNur den Autor benachrichtigen, wenn ich Dinge favorisiere)Inform only the author when liking things ConfigDialog&Ungltige Bilddatei Invalid image ConfigDialoghElement wurde wegen einer Filterregel hervorgehoben.(Item highlighted due to filtering rules. ConfigDialog Element ist neu. Item is new. ConfigDialogpBeim Aktualisieren zur Zeile des neuen Beitrags springen Jump to new posts line on update ConfigDialogLinke Seite Left side ConfigDialogNebenfeeds Minor Feeds ConfigDialog2Avatargre in NebenfeedsMinor feed avatar sizes ConfigDialog*NetzwerkkonfigurationNetwork configuration ConfigDialogNieNever ConfigDialog<neuen Aktivitten im NebenfeedNew activities in minor feed ConfigDialogneuen Beitrgen New posts ConfigDialog6Stil der BenachrichtigungenNotification Style ConfigDialog$Benachrichtigungen Notifications ConfigDialogBBenachrichtigen beim Empfang von:Notify when receiving: ConfigDialog:Nur fr Bilder von Webseiten.(Only for images inserted from web sites. ConfigDialogVEigene Benachrichtigungen werden verwendet.Own notifications will be used. ConfigDialogBeitragsinhalte Post Contents ConfigDialogBeitragstitel Post Titles ConfigDialogBeitrgePosts ConfigDialogNBeitrge pr&o Seite, andere Zeitleisten Posts per page, &other timelines ConfigDialogPrivatsphrePrivacy ConfigDialog(Pro&xy-EinstellungenPro&xy Settings ConfigDialog*ProgrammeinstellungenProgram Configuration ConfigDialogDffentliche Beitrge als Stan&dardPublic posts as &default ConfigDialogRechte Seite Right side ConfigDialogAus&whlen... S&elect... ConfigDialogHBenutzerdefiniertes Symbol auswhlenSelect custom icon ConfigDialog$F&ilter einrichtenSet Up F&ilters ConfigDialog,Zeichenanzahl anzeigenShow character counter ConfigDialogXAusfhrliche 'Teilen'-Informationen anzeigenShow extended share information ConfigDialogDZustzliche Informationen anzeigenShow extra information ConfigDialog>Bilder in voller Gre anzeigenShow full size images ConfigDialog\Informationen ber gelschte Beitrge anzeigen"Show information for deleted posts ConfigDialogBSchnipsel aus Nebenfeeds anzeigenShow snippets in minor feeds ConfigDialog>Ihren derzeitigen Avatar zeigenShow your current avatar ConfigDialog0Maximale Schnipsel-Lnge Snippet limit ConfigDialogXMaximale Schnipsel-Lnge, wenn hervorgehobenSnippet limit when highlighted ConfigDialog8System-Benachrichtigungsfeld System Tray ConfigDialogXSymbol&typ frs System-BenachrichtigungsfeldSystem Tray Icon &Type ConfigDialogLSystemeigenes Iconset, falls verfgbarSystem iconset, if available ConfigDialog\Systembenachrichtigungen sind nicht verfgbar!'System notifications are not available! ConfigDialogDie Aktivitt ist eine Reaktion auf eine Ihrer Aktionen, z.B. ein Kommentar, der als Antwort auf eine Ihrer Mitteilungen verffentlicht wurde.jThe activity is in reply to something done by you, such as a comment posted in reply to one of your notes. ConfigDialogDie Aktivitt bezieht sich auf eines Ihrer Objekte, z.B., wenn einer Ihrer Beitrge favorisiert wurde.YThe activity is related to one of your objects, such as someone liking one of your posts. ConfigDialogVDie ausgewhlte Bilddatei ist nicht gltig. The selected image is not valid. ConfigDialogNDies ist eine einfache BenachrichtigungThis is a basic notification ConfigDialogHDies ist eine SystembenachrichtigungThis is a system notification ConfigDialogPAkt&ualisierungsintervall der ZeitleisteTimeline &update interval ConfigDialogZeitleisten Timelines ConfigDialogObenTop ConfigDialogdDateinamen des Anhangs als Beitragstitel verwenden-Use attachment filename as initial post title ConfigDialog6Bitte vorsichtig einsetzen.Use with care. ConfigDialogWird auch verwendet, um an Sie gerichtete Beitrge in den Zeitleisten hervorzuheben.DUsed also when highlighting posts addressed to you in the timelines. ConfigDialogWird auch verwendet, um Ihre eigenen Beitrge in den Zeitleisten hervorzuheben.Standardmig wird Dianara Beitrge nur an ihre 'Anhngerschaft' senden, aber es empfiehlt sich auch an die ffentlichkeit zu senden, zumindest hin und wieder.wBy default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes.FirstRunWizard(&Konto konfigurierenConfigure your &accountFirstRunWizardHSobald Sie Ihr Konto konfiguriert haben, empfiehlt es sich Ihr Profil zu bearbeiten und einen Avatar und einige Angaben hinzuzufgen, falls nicht bereits geschehen.Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already.FirstRunWizard\Fenster mit allgemeiner Programm-&Hilfe ffnen!Open general program &help windowFirstRunWizardNStandardmig an &ffentlichkeit sendenPost to &Public by defaultFirstRunWizardDer Erste Schritt ist Ihre Kontoeinrichtung ber die folgende Schaltflche:IThe first step is setting up your account, by using the following button:FirstRunWizardtDieser Assistent wir Sie durch die ersten Schritte fhren.&This wizard will help you get started.FirstRunWizard*Willkommens-AssistentWelcome WizardFirstRunWizard.Willkommen bei Dianara!Welcome to Dianara!FirstRunWizardSie knnen dieses Fenster jederzeit ber das 'Hilfe'-Men aufrufen.@You can access this window again at any time from the Help menu.FirstRunWizardWechseln... Change... FontPicker"Schriftart whlen Choose a font FontPickerS&chlieen&Close HelpWidgetDie 'Aktivitt'-Zeitleiste, in der Sie Ihre eigenen oder von Ihnen geteilte Beitrge sehen werden.KActivity timeline, where you'll see your own posts, or posts shared by you. HelpWidgetZu diesem Zeitpunkt werden Ihr Profil, Ihre Kontaklisten und Ihre Zeitleisten geladen werden.HAt this point, your profile, contact lists and timelines will be loaded. HelpWidget&Hilfe zu Grundlagen Basic Help HelpWidgetNAuerdem knnen Sie Folgendes benutzen:Besides that, you can use: HelpWidget4Whlen Sie mit den Pfeiltasten aus und drcken Sie die Eingabe-Taste um den Namen zu vervollstndigen. Das wird jene Person zur Empfngerliste hinzufgen.vChoose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. HelpWidget,KommandozeilenoptionenCommand line options HelpWidget InhaltContents HelpWidgetfStrg+1/2/3, um zwischen den Nebenfeeds zu wechseln.0Control+1/2/3 to switch between the minor feeds. HelpWidgetStrg+Eingabe um die Empfngerliste im 'An' oder 'CC'-Feld fr einen Beitrag abzuschlieen.\Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. HelpWidgetStrg+Eingabe, um zu verffentlichen, wenn Sie das Erstellen einer Mitteilung oder eines Kommentars abgeschlossen haben. Falls die Mitteilung leer ist, knnen Sie diese mit der Taste ESC verwerfen.Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. HelpWidgetStr+G, um direkt zu einer bestimmten Seite der Zeitleiste zu springen.5Control+G to go to any page in the timeline directly. HelpWidget~Strg+Links/Rechts, um eine Seite in der Zeitleiste zu springen.4Control+Left/Right to jump one page in the timeline. HelpWidgetStrg+Hoch/Runter/BildAuf/BildAb/Pos1/Ende, um sich ber die Zeitleiste zu bewegen.AControl+Up/Down/PgUp/PgDown/Home/End to move around the timeline. HelpWidgetDianara bietet eine D-Bus-Schnittstelle, die teilweise Kontrolle von anderen Anwendungen aus ermglicht.RDianara offers a D-Bus interface that allows some control from other applications. HelpWidgetDie Favoriten-Zeitleiste, in der Sie die Beitrge und Kommentare sehen werden, die Sie favorisiert haben (indem Sie "Gefllt mir" gedrckt haben). Dieses kann als Lesezeichensystem verwendet werden.pFavorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. HelpWidgetb'Anhngerschaft' des Community-Kontos von Pump.io&Followers of Pump.io Community account HelpWidgetLoslegenGetting started HelpWidgetBDort knnen Sie auch die Option, immer 'ffentlich' zu verffentlichen, aktivieren. Ansonsten knnen Sie dies immer im Augenblick des Verffentlichens bestimmen.Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. HelpWidgetWenn Sie der 'An'-Liste eine bestimmte Person beifgen, wird diese Ihren Beitrag ber ihr Direktnachrichten-Tab empfangen.kIf you add a specific person to the 'To' list, they will receive your message in their direct messages tab. HelpWidgetDFalls Sie eine alternative Konfiguration verwenden, beispielsweise mit '--config otherconf', befindet sich die Schnittstelle unter 'org.nongnu.dianara_otherconf'.If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. HelpWidgetFalls Sie neu bei Pump.io sind, schauen Sie sich diesen Leitfaden an:4If you're new to Pump.io, take a look at this guide: HelpWidgetFalls Ihr Server HTTPS nicht untersttzt, knnen Sie den Parameter '--nohttps' verwenden.KIf your server does not support HTTPS, you can use the --nohttps parameter. HelpWidgetEs ist mglich Bilder, Audio- und Videodateien sowie allgemeine Dateien (wie PDF-Dokumente) an Ihren Beitrag anzuhngen.cIt is possible to attach images, audio, video, and general files, like PDF documents, to your post. HelpWidgetVergessen Sie nicht, dass es in Dianara viele Stellen gibt, wo Sie zustzliche Informationen erhalten knnen, indem Sie Textbereiche oder Schaltflchen mit Ihrer Maus berfahren und auf das Erscheinen eines Tooltips achten.Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. HelpWidgetTastaturbefehleKeyboard controls HelpWidget$Kontakte verwaltenManaging contacts HelpWidgetLDie Nachrichten-Zeitleiste, in der Sie Nachrichten sehen werden, die direkt an Sie gerichtet sind. Diese Nachrichten knnen auch an andere Leute gesendet worden sein.Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. HelpWidgetVNeue Beitrge erscheinen andersfarbig hervorgehoben. Sie knnen sie einfach als 'gelesen' markieren, indem Sie auf irgendeine freie Stelle innerhalb des Beitrags klicken. New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. HelpWidgetVerffentlichenPosting HelpWidget"Pump.io LeitfadenPump.io User Guide HelpWidgetEinstellungenSettings HelpWidgetdDie fnfte Zeitleiste ist der Nebenfeed, auch als 'Inzwischen' bezeichnet. Dieser ist auf der linken Seite zu sehen, doch kann er auch versteckt werden. Dort werden Sie beilufige Aktivitten von allen, denen Sie folgen, sehen, darunter Kommentar-Aktionen, Favorisieren von Beitrgen oder Leuten zu folgen.The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. HelpWidgetWenn Sie Dianara zum ersten Mal starten, sollten Sie den Konten-Konfigurationsdialog sehen. Geben Sie dort Ihre Pump.io-Adresse in der Form 'Name@Server' ein und drcken Sie die Schaltflche 'Verifizierungkode erhalten'.The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. HelpWidgetDas Interface befindet sich bei %1, und Sie knnen darauf mit Werkzeugen wie %2 oder %3 zugreifen. Es bietet Methoden wie %4 und %5.lThe interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. HelpWidgetDie Haupt-Zeitleiste, in der Sie all das Zeug sehen werden, das von den Leuten, denen Sie folgen verffentlicht oder geteilt wird.\The main timeline, where you'll see all the stuff posted or shared by the people you follow. HelpWidgetDie gelufigsten Befehle in den Mens sind durch Tastaturkrzel, wie 'F5' oder 'Strg+N', ergnzt.nThe most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. HelpWidgetDie sechste und siebte Zeitleiste sind ebenfalls Nebenfeeds, hnlich wie 'Inzwischen', enthalten aber nur Aktivitten, die direkt Sie betreffen (Erwhnungen), und Aktionen durch Sie (Aktionen).The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). HelpWidgetZDann sollte Ihr blicher Browser die Autorisierungsseite Ihres Pump.io-Servers laden. Dort werden Sie den ganzen 'VERIFIER'-Code kopieren und diesen dann in das zweite Feld bei Dianara einfgen mssen. Drcken Sie daraufhin 'Anwendung autorisieren' und - sobald dies besttigt ist - 'Daten speichern'.Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. HelpWidget6Es gibt sieben Zeitleisten:There are seven timelines: HelpWidgetEs gibt oben ein Textfeld, in das Sie direkt die Adressen von Kontakten eingeben knnen, um ihnen zu folgen.hThere is a text field at the top, where you can directly enter addresses of new contacts to follow them. HelpWidgetDort knnen Sie auch Personenlisten verwalten, die in erster Linie dazu verwendet werden, Beitrge an bestimmte Personenkreise zu senden.`There, you can also manage person lists, used mainly to send posts to specific groups of people. HelpWidgetDiese Aktivitten knnen eine '+'-Schaltflche enthalten. Drcken Sie darauf, um den erwhnten Beitrag zu ffnen. Auerdem knnen Sie, wie an vielen anderen Stellen, durch berfahren mit der Maus relevante Informationen sehen.These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. HelpWidgetZeitleisten Timelines HelpWidgetHIm 'Nachbarn'-Tab sehen Sie einige Ressourcen, um Leute zu finden, und haben die Mglichkeit die zuletz registrierten Benutzer ihres Servers direkt zu durchstbern.Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. HelpWidgetVerwenden Sie den Parameter '--debug', um zustzliche Informationen ber das Verhalten des Programms in Ihrem Terminal anzuzeigen.mUse the --debug parameter to have extra information in your terminal window, about what the program is doing. HelpWidget*Benutzer nach SpracheUsers by language HelpWidgetWhrend des Erstellens einer Mitteilung knnen Sie mithilfe der Eingabetaste vom Titel- ins Text-Feld springen. Umgekehrt knnen Sie mithilfe der Pfeiltaste aufwrts vom Text- zurck ins Titel-Feld springen, wenn sich der Cursor ganz am Anfang des Feldes befindet.While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. HelpWidgetSie knnen diesem Kontakt aus dem selben Men auch eine (anfangs private) Direktnachricht senden.VYou can also send a direct message (initially private) to that contact from this menu. HelpWidgetSie knnen auch '@' gefolgt vom ersten Zeichen eines Kontaktnamens eingeben, um ein Pop-Up-Men mit passenden Mglichkeiten anzuzeigen.wYou can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. HelpWidgetSie knnen auf alle Avatare in Beitrgen, Kommentaren und der 'Inzwischen'-Spalte klicken, woraufhin ein Men mit mehreren Optionen erscheint; eine davon das Folgen bzw. Nicht-mehr-folgen der betreffenden Person.You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. HelpWidgetvIn den Einstellungen knnen Sie viele Dinge nach Ihren Wnschen anpassen, darunter das Zeitintervall zwischen Zeitleisten-Aktualisierungen, die Anzahl Beitrge, die Sie pro Seite angezeigt bekommen, die Farben fr Hervorhebungen, die Benachrichtigungen oder das Aussehen des Symbols im System-Benachrichtigungsfeld.You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. HelpWidget>Sie knnen private Nachrichten erstellen, indem Sie bestimmte Personen zu diesen Listen hinzufgen und die Optionen 'Anhngerschaft' und 'ffentlich' abwhlen.~You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. HelpWidgetSie finden eine Liste mit einigen Pump.io-Nutzern und anderen Informationen hier:GYou can find a list with some Pump.io users and other information here: HelpWidget~Sie knnen Mitteilungen verffentlichen, indem Sie in das Eingabefeld im oberen Fensterbereich klicken oder Strg+N drcken. Ihrem Beitrag einen Titel zu geben ist optional, jedoch sehr empfehlenswert, da es das Wiedererkennen von Verweisen auf Ihren Beitrag im Nebenfeed, in E-Mail-Benachrichtigungen usw., vereinfacht.You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. HelpWidgetSie knnen die Listen der Leute, denen Sie folgen, und derjenigen, die Ihnen folgen, im Kontakte-Tab anzeigen lassen.UYou can see the lists of people you follow, and who follow you from the Contacts tab. HelpWidgetIndem Sie die 'An'- und die 'CC'-Schaltflche verwenden, bestimmen Sie wer Ihren Beitrag sehen wird.EYou can select who will see your post by using the To and Cc buttons. HelpWidgetSie knnen den Parameter '--config' verwenden um das Programm mit einer anderen Konfiguration auszufhren. Das kann ntzlich sein, um zwei oder mehr Konten zu verwenden. Sie knnen sogar zwei Instanzen Dianaras gleichzeitig ausfhren.You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. HelpWidgetFMittels der 'Format' Schaltflche knnen Sie Ihrem Text Formatierungen wie Fett- oder Kursivdruck verleihen. Manche dieser Optionen setzen eine Textauswahl voraus.You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. HelpWidgetjSie sollten einen Blick in das Fenster 'Programmeinstellungen' werfen, erreichbar ber das Men 'Einstellungen' - 'Dianara einrichten'. Dort finden Sie einige interessante Optionen.You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. HelpWidget&Schlieen&Close ImageViewerWiede&rholen&Restart ImageViewer&&Speichern unter... &Save As... ImageViewerAlle Dateien All files ImageViewer(Betrachter schlieen Close Viewer ImageViewer2Vollbild herunterladen...Downloading full image... ImageViewerJFehler beim Herunterladen des Bildes!Error downloading image! ImageViewer@Fehler beim speichern des BildesError saving image ImageViewerEinpassenFit ImageViewerBildImage ImageViewerBilddateien Image files ImageViewer,Bild linksherum drehenRotate image to the left ImageViewer.Bild rechtsherum drehenRotate image to the right ImageViewer.Bild speichern unter...Save Image As... ImageViewer"Bild speichern... Save Image... ImageViewerEin Fehler ist aufgetreten beim Speichern von %1. Der Dateiname sollte mit den Erweiterungen '.jpg' oder '.png' enden.UThere was a problem while saving %1. Filename should end in .jpg or .png extensions. ImageViewer>Versuchen Sie es spter wieder.Try again later. ImageViewerUnbenanntUntitled ImageViewer*Zur Liste hin&zufgen &Add to List ListsManager4Ausgewhlte Li&ste lschen&Delete Selected List ListsManager &Nein&No ListsManager"Mitglied &lschen&Remove Member ListsManager&Ja&Yes ListsManager&Ja, lschen&Yes, delete it ListsManager(Mitglied &hinzufgen Add Mem&ber ListsManager,&Neue Liste hinzufgen Add New &List ListsManagerbSind Sie sich sicher, dass Sie %1 lschen wollen?#Are you sure you want to delete %1? ListsManagerSind Sie sich sicher, dass Sie %1 aus der Liste %2 lschen wollen?4Are you sure you want to remove %1 from the %2 list? ListsManager Liste &erstellen Create L&ist ListsManagerMitgliederMembers ListsManagerNameName ListsManager:Person aus der Liste lschen?Remove person from list? ListsManager^Geben Sie einen Namen fr die neue Liste ein...Type a name for the new list... ListsManager\Geben Sie eine optionale Beschreibung hier ein!Type an optional description here ListsManager.WARNUNG: Liste lschen?WARNING: Delete list? ListsManager&Schlieen&Close LogViewer"Log-Datei &leeren Clear &Log LogViewerLog-DateiLog LogViewer%1 Bytes%1 bytes MainWindow%1 gelscht. %1 deleted. MainWindow&%1 herausgefiltert.%1 filtered out. MainWindow&%1 herausgefiltert.plural, several activities%1 filtered out. MainWindow"%1 hervorgehobene%1 highlighted MainWindow$%1 hervorgehobene.%1 highlighted. MainWindow*Noch %1 zu empfangen.%1 more pending to receive. MainWindow*Noch %1 zu empfangen.plural, several activities%1 more pending to receive. MainWindow &Konto&Account MainWindow&Aktivitt &Activity MainWindow&Dianara &einrichten&Configure Dianara MainWindow&Kontakte &Contacts MainWindow.&Filter und Hervorheben&Filters and Highlighting MainWindow &Hilfe&Help MainWindow&Fenster &verstecken &Hide Window MainWindow&Log-Datei&Log MainWindow&Nachrichten &Messages MainWindow &Nein&No MainWindow6&Mitteilung verffentlichen &Post a Note MainWindow&Verlassen&Quit MainWindow&Sitzung&Session MainWindowFen&ster zeigen &Show Window MainWindowZeitleis&te &Timeline MainWindowWerkzeugleis&te&Toolbar MainWindow&Ansicht&View MainWindow.&Ja, Programm schlieen&Yes, close the program MainWindow$'%1' aktualisiert. '%1' updated. MainWindow1 gelscht. 1 deleted. MainWindow$1 herausgefiltert.1 filtered out. MainWindow$1 herausgefiltert. singular, refers to one activity1 filtered out. MainWindow1 hervorgehoben 1 highlighted MainWindow"1 hervorgehobene.1 highlighted. MainWindow(Noch 1 zu empfangen.1 more pending to receive. MainWindow(Noch 1 zu empfangen.singular, 1 activity1 more pending to receive. MainWindowber &DianaraAbout &Dianara MainWindowber Dianara About Dianara MainWindowAuerdem:Also: MainWindowLZei&tleisten automatisch aktualisierenAuto-update &Timelines MainWindowFAutomatisches Aktualisieren inaktivAuto-updating disabled MainWindowBAutomatisches Aktualisieren aktivAuto-updating enabled MainWindow(&Hilfe zu Grundlagen Basic &Help MainWindowNach Filtern By filters MainWindow^Klicken Sie hier, um Ihr Konto zu konfigurieren$Click here to configure your account MainWindowNKlicken Sie um Ihr Profil zu bearbeitenClick to edit your profile MainWindowTSchliee wegen Abschaltens der Umgebung...+Closing due to environment shutting down... MainWindowDianara kann nicht im System-Benachrichtigungsfeld versteckt werden.,Dianara cannot be hidden in the system tray. MainWindowDianara ist freie Software, lizenziert unter der GNU-GPL-Lizenz, und verwendet einige Oxygen-Icons unter der LGPL-Lizenz.lDianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. MainWindowpDianara ist ein Client fr das soziale Netzwerk Pump.io..Dianara is a pump.io social networking client. MainWindow$Dianara gestartet.Dianara started. MainWindow"DirektnachrichtenDirect messages MainWindowLWollen Sie Dianara wirklich schlieen?$Do you really want to close Dianara? MainWindowNWollen Sie das Programm ganz schlieen?,Do you want to close the program completely? MainWindow$&Profil bearbeiten Edit &Profile MainWindowxDeutsche bersetzung durch Eugenio M. Vigo und mightyscoopa.#English translation by JanKusanagi. MainWindowdGeben Sie das Passwort fr Ihren Proxy-Server ein:)Enter the password for your proxy server: MainWindowBFehler beim Speichern des Bildes!Error storing image! MainWindowFavor&iten Favor&ites MainWindowVoll&bild Full &Screen MainWindowStarten...Initializing... MainWindow0Zuletzt aktualisiert: %1Last update: %1 MainWindowLink zu: %1 Link to: %1 MainWindow>Liste einiger P&ump.io-BenutzerList of Some Pump.io &Users MainWindow:Alles als 'gelesen' markierenMark All as Read MainWindow:Markiere alles als gelesen...Marking everything as read... MainWindow2Nachrichten direkt an SieMessages sent explicitly to you MainWindowFAn Sie gerichtete Neben-Aktivitten!Minor activities addressed to you MainWindowpNeben-Aktivitten von allen, z.B. Antworten auf BeitrgeStarting automatic update of timelines, once every %1 minutes. MainWindow&Statusleiste Status &Bar MainWindowfBeende automatische Aktualisierung der Zeitleisten.'Stopping automatic update of timelines. MainWindowvIcon frs System-Benachrichtigungsfeld ist nicht verfgbar."System tray icon is not available. MainWindowGedankt sei allen Testern, bersetzern und Paketerstellern, die dabei helfen, Dianara zu verbessern!SThanks to all the testers, translators and packagers, who help make Dianara better! MainWindow(Die Haupt-ZeitleisteThe main timeline MainWindowDie Leute denen Sie folgen, diejenigen, die Ihnen folgen, und Ihre KontaklistenEThe people you follow, the ones who follow you, and your person lists MainWindow8Es gibt %1 neue Aktivitten.There are %1 new activities. MainWindow2Es gibt %1 neue Beitrge.There are %1 new posts. MainWindow2Es gibt 1 neue Aktivitt.There is 1 new activity. MainWindow0Es gibt 1 neuen Beitrag.There is 1 new post. MainWindow<Zeitleiste aktualisiert um %1.Timeline updated at %1. MainWindowWerkzeugleisteToolbar MainWindow&Beitrge gesamt: %1Total posts: %1 MainWindow %1 aktualisieren Update %1 MainWindow$&Webseite besuchenVisit &Website MainWindowMit Dianara knnen Sie Ihre Zeitleisten betrachten, neue Mitteilungen schreiben, Bilder und andere Medien hochladen, mit Beitrgen interagieren, Ihre Kontakte verwalten und neuen Leuten folgen.With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. MainWindowtSie schreiben gerade eine Mitteilung oder einen Kommentar.&You are composing a note or a comment. MainWindowSie haben einen Proxy-Server mit Authentifizierung konfiguriert, aber das Passwort ist nicht eingestellt.TYou have configured a proxy server with authentication, but the password is not set. MainWindowPIhr Pump.io-Konto ist nicht konfiguriert&Your Pump.io account is not configured MainWindowLIhr Konto ist noch nicht konfiguriert.#Your account is not configured yet. MainWindow0Ihre Biographie ist leerYour biography is empty MainWindow6Ihre favorisierten BeitrgeYour favorited posts MainWindow*Ihre eigenen BeitrgeYour own posts MainWindow$Empfange %1 neuere Get %1 newer MinorFeedDVorherige Nebenaktivitten abrufenGet previous minor activities MinorFeed$ltere AktivittenOlder Activities MinorFeedREs gibt noch keine Aktivitten zu zeigen.$There are no activities to show yet. MinorFeed CC: %1Cc: %1 MinorFeedItem0Erwhnten Beitrag ffnenOpen referenced post MinorFeedItem An: %1To: %1 MinorFeedItem Mit %1Using %1 MinorFeedItem Bytesbytes MiscHelpers&Abbrechen&Cancel PageSelector &Erste&First PageSelector&Los&Go PageSelector&Letzte&Last PageSelector"Zu Seite springen Jump to page PageSelector NeuereNewer PageSelector ltereOlder PageSelectorSeitenzahl: Page number: PageSelector&Abbrechen&Cancel PeopleWidget&Suche:&Search: PeopleWidgetNEinen Kontakt zu einer Liste hinzufgenAdd a contact to a list PeopleWidgetfGeben Sie hier einen Namen ein, um danach zu suchen"Enter a name here to search for it PeopleWidget%1 Kommentare %1 commentsPost%1 gefllt dies %1 like thisPost%1x favorisiert%1 likesPost%1 gefllt dies %1 likes thisPost6%1 Mitglieder in der Gruppe%1 members in the groupPost&%1 hat dies geteilt%1 shared thisPost*%1 haben dies geteilt#%1 = Names for more than one person%1 shared thisPostS&chlieen&ClosePost &Nein&NoPost&Ja, lschen&Yes, delete itPost&Ja, teilen&Yes, share itPost,&Ja, nicht mehr teilen&Yes, unshare itPost1 Kommentar 1 commentPost1x favorisiert1 likePostpSind Sie sicher, dass Sie diesen Beitrag lschen wollen?*Are you sure you want to delete this post?Post~Sind Sie sicher, dass Sie Ihren eigenen Beitrag teilen mchten?-Are you sure you want to share your own post?Post*Angehngte AudiodateiAttached AudioPost Angehngte Datei Attached FilePost*Angehngte VideodateiAttached VideoPostCCCcPostpKlicken Sie auf das Bild, um es in voller Gre zu sehen&Click the image to see it in full sizePostLKlicken, um den Anhang herunterzuladen Click to download the attachmentPostKommentierenCommentPost\Kopiere den Beitragslink in die ZwischenablageCopy post link to clipboardPost0Konnte Bild nicht laden!Couldn't load image!PostLschenDeletePost<Wollen Sie %1s Beitrag teilen?Do you want to share %1's post?PostpWollen Sie das Teilen von %1s Beitrag rckgngig machen?!Do you want to unshare %1's post?PostBearbeitenEditPostBearbeitet: %1 Edited: %1Post,Diesen Beitrag lschenErase this postPost>Ausgewhlter Text wird zitiert.+If you select some text, it will be quoted.PosthBild ist animiert. Darauf klicken um es abzuspielen.'Image is animated. Click on it to play.PostInInPost Gruppe beitreten Join GroupPostGefllt mirLikePost8Diese Nachricht favorisierenLike this postPostLade Bild...Loading image...Post"Am %1 modifiziertModified on %1Post2Diesen Beitrag bearbeitenModify this postPost4Normalisiere SchriftfarbenNormalize text colorsPost@Diesen Beitrag im Browser ffnenOpen post in web browserPost`ffne Vorluferbeitrag, auf den dieser antwortet/Open the parent post, to which this one repliesPostVorluferParentPostBeitragPostPost(Am %1 verffentlicht Posted on %1Post:Auf diesen Beitrag antworten.Reply to this post.Post TeilenSharePostBeitrag teilen? Share post?PostRDiesen Beitrag mit Ihren Kontakten teilen"Share this post with your contactsPost%1x geteiltShared %1 timesPostGeteilt am %1 Shared on %1Post1x geteilt Shared oncePostBildgreSizePostAnToPostTypTypePost"Gefllt mir nichtUnlikePost"Nicht mehr teilenUnsharePost4Beitrag nicht mehr teilen? Unshare post?Post@Diesen Beitrag nicht mehr teilenUnshare this postPost Mit %1Using %1Post Via %1Via %1Post2WARNUNG: Beitrag lschen?WARNING: Delete post?Post0Du hast dies favorisiert You like thisPost&Biographie&Bio ProfileEditor&Abbrechen&Cancel ProfileEditor&Heimatort &Hometown ProfileEditor"Profil &speichern &Save Profile ProfileEditorAlle Dateien All files ProfileEditor AvatarAvatar ProfileEditor"&Avatar ndern...Change &Avatar... ProfileEditor4Wechsle &E-Mail-Adresse...Change &E-mail... ProfileEditor*Wenn Sie Ihren Avatar wechseln, wird ein Beitrag in Ihrer Zeitleiste daraus erstellt. Falls Sie jenen Beitrag lschen, wird auch Ihr Avatar gelscht.zChanging your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. ProfileEditor E-MailE-mail ProfileEditorGanzer &Name Full &Name ProfileEditorBilddateien Image files ProfileEditor&Ungltige Bilddatei Invalid image ProfileEditor"Nicht eingestelltNot set ProfileEditorProfil-EditorProfile Editor ProfileEditor2Bild fr Avatar auswhlenSelect avatar image ProfileEditorVDie ausgewhlte Bilddatei ist nicht gltig. The selected image is not valid. ProfileEditorDies ist die mit Ihrem Konto verknpfte E-Mail-Adresse, um Benachrichtigungen zu erhalten und fr die PasswortwiederherstellungoThis is the e-mail address associated with your account, for things such as notifications and password recovery ProfileEditor:Dies ist Ihre Pump.io-AdresseThis is your Pump address ProfileEditor8Dies ist Ihr sichtbarer NameThis is your visible name ProfileEditorWebfinger-ID Webfinger ID ProfileEditor&Abbrechen&Cancel ProxyDialog&Host-Name &Hostname ProxyDialog &Port&Port ProxyDialog&Speichern&Save ProxyDialogBen&utzer&User ProxyDialog,Keinen Proxy verwendenDo not use a proxy ProxyDialog6Hinweis: Das Passwort wird nicht sicher hinterlegt. Wenn Sie wollen, knnen Sie das Feld leer lassen, dann werden Sie beim Start nach dem Passwort gefragt.Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. ProxyDialogPass&wort Pass&word ProxyDialogProxy-&Typ Proxy &Type ProxyDialog&Proxy-KonfigurationProxy Configuration ProxyDialog8&Authentifizierung verwendenUse &Authentication ProxyDialog,Ihr Proxy-BenutzernameYour proxy username ProxyDialog:%1 KiB von %2 KiB hochgeladen%1 KiB of %2 KiB uploaded Publisher<&Abbrechen, zurck zum Beitrag&Cancel, go back to the post Publisher\&Nein, nur an 'Anhngerschaft' verffentlichen&No, post to my followers only Publisher,&Ja, ffentlich machen&Yes, make it public PublisherHin&zufgen...Ad&d... Publisher~Fgen Sie dem Beitrag hier einen kurzen Titel hinzu (empfohlen)1Add a brief title for the post here (recommended) PublisherAlle Dateien All files Publisher AudioAudio Publisher0Audiodatei nicht gewhltAudio file not set PublisherAudiodateien Audio files PublisherAbbrechenCancel PublisherDen Anhang verwerfen und zurck zu einer einfachen Mitteilung gehen4Cancel the attachment, and go back to a regular note Publisher"Beitrag verwerfenCancel the post Publisher CC...Cc... Publisher$Dianara begrenzt das Hochladen von Dateien z.Zt. auf 10 MiB pro Beitrag, um mgliche Speicherplatz- oder Netzwerkprobleme der Server zu vermeiden.yDianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. PublisherWollen Sie den Beitrag ffentlich, anstatt nur fr Ihre 'Anhngerschaft' machen?>Do you want to make the post public instead of followers-only? Publisher$Bearbeite Beitrag. Editing post. Publisher FehlerError Publisher8Fehler: Bereits am ErstellenError: Already composing Publisher*Die Datei ist zu groFile is too big Publisher.Datei nicht ausgewhlt.File not selected. Publisher&Datei nicht gewhlt File not set PublisherDAudiodatei in Ihren Ordnern finden#Find the audio file in your folders Publisher:Datei in Ihren Ordnern findenFind the file in your folders Publisher8Bild in Ihren Ordnern finden Find the picture in your folders Publisher:Video in Ihren Ordnern findenFind the video in your folders PublisherDrcken Sie Strg+Eingabe, um mit der Tastatut zu verffentlichen+Hit Control+Enter to post with the keyboard PublisherWenn Sie so verffentlichen, wird niemand Ihren Beitrag sehen knnen.?If you post like this, no one will be able to see your message. PublisherIgnoriere Anfrage fr neue Mitteilung durch eine andere Anwendung.3Ignoring new note request from another application. PublisherBilddateien Image files Publisher(Ungltige AudiodateiInvalid audio file PublisherUngltige Datei Invalid file Publisher&Ungltige Bilddatei Invalid image Publisher(Ungltige VideodateiInvalid video file PublisherpMitteilung wurde von einer anderen Anwendung angefangen.&Note started from another application. PublisherSonstigesOther PublisherBildPicture Publisher$Bild nicht gewhltPicture not set PublisherVerffentlichenPost Publisher*Der Beitrag ist leer.Post is empty. PublisherfVerffentlichung fehlgeschlagen. Erneut versuchen.Posting failed. Try again. Publisher"Verffentliche... Posting... PublisherEntfernenRemove PublisherBildgre Resolution Publisher.Audiodatei auswhlen...Select Audio File... Publisher$Datei auswhlen...Select File... Publisher"Bild auswhlen...Select Picture... Publisher$Video auswhlen...Select Video... Publisher<Whlen Sie eine Audiodatei ausSelect one audio file Publisher2Whlen Sie eine Datei ausSelect one file Publisher.Whlen Sie ein Bild ausSelect one image Publisher@Whlen Sie _eine_ Videodatei ausSelect one video file PublishernAuswhlen, wer eine Kopie dieses Beitrags erhalten soll'Select who will get a copy of this post PublisherRAuswhlen, wer diesen Beitrags sehen sollSelect who will see this post PublisherEinen Titel einzustellen hilft dabei die 'Inzwischen'-Zeitleiste informativer zu machen>Setting a title helps make the Meanwhile feed more informative PublisherDa Sie ein Bild hochladen wollen, knnten Sie es ein wenig verkleinern oder es in einem komprimierten Format wie JPG speichern.sSince you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. Publisher GreSize PublisherREntschuldigen Sie die Unannehmlichkeiten.Sorry for the inconvenience. PublisherZDas Format der Audiodatei wird nicht erkannt.$The audio format cannot be detected. Publisher@Der Dateityp wird nicht erkannt.!The file type cannot be detected. Publisher(Das Format der Bilddatei wird nicht erkannt. Vielleicht ist deren Erweiterung falsch, bspw. eine GIF-Datei die nach 'image.jpg' umbenannt wurde o..tThe image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. PublisherZDas Format der Videodatei wird nicht erkannt.$The video format cannot be detected. PublisherDies ist eine vorlufige Manahme, da die Server noch nicht ihre eigenen Begrenzungen aufstellen knnen.OThis is a temporary measure, since the servers cannot set their own limits yet. Publisher TitelTitle Publisher An...To... PublisherTypType PublisherAktualisierenUpdate PublisherAktualisiere... Updating... PublisherRMedien, wie Bilder oder Videos, hochladen%Upload media, like pictures or videos Publisher VideoVideo PublisherVideodateien Video files Publisher&Video nicht gewhlt Video not set PublisherXWarnung: Sie haben noch keine Anhngerschaft"Warning: You have no followers yet PublisherSie knnen jetzt keine Nachricht an %1 erstellen, weil bereits ein anderer Beitrag erstellt wird.YYou can't create a message for %1 at this time, because a post is already being composed. PublisherSie knnen jetzt keinen Beitrag bearbeiten, weil bereits ein anderer Beitrag erstellt wird.MYou can't edit a post at this time, because a post is already being composed. PublisherSie versuchen nur an Ihre Anhngerschaft zu verffentlichen, aber Sie haben noch keine.SYou're trying to post to your followers only, but you don't have any followers yet. PublisherR%1 (%2) erfolgreich zu Liste hinzugefgt.#%1 (%2) added to list successfully.PumpControllerN%1 (%2) erfolgreich von Liste entfernt.'%1 (%2) removed from list successfully.PumpController%1 Versuche %1 attemptsPumpController0%1 Kommentare empfangen.%1 comments received.PumpControllerz%1 erfolgreich verffentlicht. Aktualisiere Beitragsinhalt...3%1 published successfully. Updating post content...PumpController1 Versuch 1 attemptPumpController,1 Kommentar empfangen.1 comment received.PumpControllerAktionenActionsPumpControllerAktivittActivityPumpController,Fge Elemente hinzu...Adding items...PumpController:Fge Person zu Liste hinzu...Adding person to list...PumpControllerAlle einleitenden Daten empfangen. Initialisierung abgeschlossen.3All initial data received. Initialization complete.PumpControllerDAnwendung erfolgreich autorisiert.$Application authorized successfully.PumpController(AutorisierungsfehlerAuthorization errorPumpControllerhAutorisiert fr Konto %1. Rufe einleitende Daten ab.3Authorized to use account %1. Getting initial data.PumpControllerDAvatar erfolgreich verffentlicht.Avatar published successfully.PumpController&Avatar hochgeladen.Avatar uploaded.PumpControllerFalsche Anfrage Bad RequestPumpController<Kann %1 momentan nicht folgen.Can't follow %1 at this time.PumpControllerTberprfe Adresse %1 bevor gefolgt wird...'Checking address %1 before following...PumpControllerPKommentar %1 erfolgreich verffentlicht.Comment %1 posted successfully.PumpControllerLKommentar %1 erfolgreich aktualisiert. Comment %1 updated successfully.PumpController$Erstelle Gruppe...Creating group...PumpController2Erstelle Personenliste...Creating person list...PumpController.Lsche Personenliste...Deleting person list...PumpController>E-Mail-Adresse aktualisiert: %1E-mail updated: %1PumpController8Fehler beim verbinden mit %1Error connecting to %1PumpControllerBFehler beim Laden des Nebenfeeds!Error loading minor feed!PumpControllerBFehler beim Laden der Zeitleiste!Error loading timeline!PumpControllerFavoriten FavoritesPumpControllerpDatei erfolgreich hochgeladen. Verffentliche Beitrag....File uploaded successfully. Posting message...PumpControllerAnhngerschaft FollowersPumpControllerVerfolgte FollowingPumpController>Folgen von %1 (%2) erfolgreich.Following %1 (%2) successfully.PumpControllerVerboten ForbiddenPumpController4Gateway-ZeitberschreitungGateway TimeoutPumpControllerRufe '%1' ab...Getting '%1'...PumpController,Rufe Oauth-Token ab...Getting OAuth token...PumpController:Rufe eine Personenliste ab...Getting a person list...PumpController*Rufe Kommentare ab...Getting comments...PumpController4Rufe Favorisierungen ab...Getting likes...PumpControllerBRufe Liste 'Anhngerschaft' ab...Getting list of 'Followers'...PumpController8Rufe Liste 'Verfolgte' ab...Getting list of 'Following'...PumpControllerFRufe Liste der Personenlisten ab...Getting list of person lists...PumpControllerDRufe Benutzer des Servers %1 ab...Getting site users for %1...PumpControllerVerschwundenGonePumpController>Gruppe %1 erfolgreich erstellt.Group %1 created successfully.PumpControllerDGruppe %1 erfolgreich beigetreten.Group %1 joined successfully.PumpControllerHTTP-Fehler HTTP errorPumpController*Interner ServerfehlerInternal Server ErrorPumpController&Trete Gruppe bei...Joining group...PumpController$Verlasse Gruppe...Leaving group...PumpController@Gruppe %1 erfolgreich verlassen.Left the %1 group successfully.PumpController4Favorisierungen empfangen.Likes received.PumpControllerBListe mit %1 Benutzern empfangen.List of %1 users received.PumpControllerZListe 'Anhngerschaft' vollstndig empfangen.(List of 'followers' completely received.PumpControllerPListe 'Verfolgte' vollstndig empfangen.(List of 'following' completely received.PumpController:Liste der 'Listen' empfangen.List of 'lists' received.PumpControllerLade externe Bilddatei von %1 ohne Rcksicht auf SSL-Fehler (wie konfiguriert)...ILoading external image from %1 regardless of SSL errors, as configured...PumpControllerInzwischen MeanwhilePumpControllerErwhnungenMentionsPumpController<Beitrge erfolgreich gelscht.Message deleted successfully.PumpControllerHBeitrag erfolgreich (de)favorisiert.&Message liked or unliked successfully.PumpControllerNachrichtenMessagesPumpController(Permanent verschobenMoved PermanentlyPumpController&Temporr verschobenMoved TemporarilyPumpControllerNicht gefunden Not FoundPumpController&Nicht implementiertNot ImplementedPumpControllerjOAuth-Fehler whrend des Autorisierens der Anwendung.*OAuth error while authorizing application.PumpController(OAuth-Support-FehlerOAuth support errorPumpControllerVListe 'Anhngerschaft' teilweise empfangen.%Partial list of 'followers' received.PumpControllerLListe 'Verfolgte' teilweise empfangen.%Partial list of 'following' received.PumpControllerPPersonenliste '%1' erfolgreich erstellt.&Person list '%1' created successfully.PumpControllerFPersonenliste erfolgreich gelscht.!Person list deleted successfully.PumpController0Personenliste empfangen.Person list received.PumpControllerLBeitrag %1 erfolgreich verffentlicht.Post %1 published successfully.PumpControllerHBeitrag %1 erfolgreich aktualisiert.Post %1 updated successfully.PumpControllerFBeitrag von %1 erfolgreich geteilt.Post by %1 shared successfully.PumpController"Profil empfangen.Profile received.PumpController(Profil aktualisiert.Profile updated.PumpController QOAuth-Fehler %1QOAuth error %1PumpControllerBereit.Ready.PumpController*'%1' wurde empfangen.Received '%1'.PumpController8Entferne Person von Liste...Removing person from list...PumpController@SSL-Fehler bei Verbindung zu %1!SSL errors in connection to %1!PumpController,Dienst nicht verfgbarService UnavailablePumpController|Einige einleitenden Daten wurden nach mehreren Versuchen nicht empfangen. Vielleicht stimmt etwas mit Ihrem Server nicht. Dennoch ist es mglich, dass Sie den Dienst normal verwenden knnen.Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally.PumpControllerEinige einleitenden Daten wurden nicht empfangen. Beginne erneute Initialisierung...@Some initial data was not received. Restarting initialization...PumpControllerbWarte weiterhin auf Profil. Versuche es erneut...*Still waiting for profile. Trying again...PumpControllerNFolgen von %1 (%2) erfolgreich beendet.'Stopped following %1 (%2) successfully.PumpControllerDie Anwendung ist noch nicht bei Ihrem Server registriert. Registriere...FThe application is not registered with your server yet. Registering...PumpControllerDie Kommentare fr diesen Beitrag knnen nicht geladen werden, da die Daten auf dem Server fehlen.NThe comments for this post cannot be loaded due to missing data on the server.PumpControllerBEs gibt kein autorisiertes Konto.There is no authorized account.PumpControllerEin OAuth-Fehler trat auf, whrend versucht wurde das Autorisierungs-Token abzurufen.EThere was an OAuth error while trying to get the authorization token.PumpControllerZeitleisteTimelinePumpController,Versuche %1 zu folgen.Trying to follow %1.PumpControllerDKann die Adresse nicht berprfen!Unable to verify the address!PumpController"Nicht autorisiert UnauthorizedPumpControllerBUnbehandelter HTTP-Fehler-Code %1Unhandled HTTP error code %1PumpControllerdUnbenannter Beitrag %1 erfolgreich verffentlicht.(Untitled post %1 published successfully.PumpController`Unbenannter Beitrag %1 erfolgreich aktualisiert.&Untitled post %1 updated successfully.PumpControllerLade %1 hoch Uploading %1PumpController$Benutzerzeitleiste User timelinePumpController6Warte auf Proxy-Passwort...Waiting for proxy password...PumpControllerSie mssen wahrscheinlich das OpenSSL-Plugin fr QCA installieren: %1, %2 o..KYou probably need to install the OpenSSL plugin for QCA: %1, %2 or similar.PumpControllerIhre Installation von QOAuth, einer von Dianara verwendeten Bibliothek, scheint HMAC-SHA1 nicht zu untersttzen._Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support.PumpController$%1 Benutzer auf %2%1 users in %2 SiteUsersListListe schlieen Close list SiteUsersListPRufe Liste der Benutzer ihres Servers ab"Get list of users from your server SiteUsersListvListe der 'Anhngerschaft' des Community-Kontos von Pump.io3List of Followers for the Pump.io Community account SiteUsersListLade... Loading... SiteUsersListRWeitere Ressourcen um Benutzer zu finden:More resources to find users: SiteUsersListLPPump-Benutzer-Suche auf inventati.org*PPump user search service at inventati.org SiteUsersListDWiki-Seite 'Benutzer nach Sprache'Wiki page 'Users by language' SiteUsersListSie knnen eine Liste der zuletzt auf ihrem Server registrierten Benutzer abrufen, indem Sie die Schaltflche unten anklicken.^You can get a list of the newest users registered on your server by clicking the button below. SiteUsersListvNoch %1 weitere Beitrge warten auf nchste Aktualisierung.&%1 more posts pending for next update.TimeLine,%1 Beitrge insgesamt.%1 posts in total.TimeLine'%1' kann nicht aktualisiert werde, da gerade ein Kommentar erstellt wird.E'%1' cannot be updated because a comment is currently being composed.TimeLine(Aktivitt-ZeitleisteActivity TimelineTimeLineNachdem der Vorgang abgeschlossen ist, sollten sich Ihr Profil und Ihre Zeitleisten automatisch aktualisieren.RAfter the process is done, your profile and timelines should update automatically.TimeLineKlicken Sie hier oder drcken Sie Str+G, um zu einer bestimmten Seite zu springen8Click here or press Control+G to jump to a specific pageTimeLineTKlicken Sie hier um diese jetzt abzurufen.Click here to receive them now.TimeLineLDianara ist ein <b>Pump.io</b>-Client.#Dianara is a Pump.io client.TimeLineDianaras BlogDianara's blogTimeLine8Direktnachrichten-ZeitleisteDirect Messages TimelineTimeLine(Favoriten ZeitleisteFavorites TimelineTimeLineAls Erstes sollten Sie Ihr Konto ber das Men <b>Einstellungen - Konto</b> einrichten.FFirst, configure your account from the Settings - Account menu.TimeLinetHier werden Sie speziell an Sie gerichtete Beitrge sehen.4Here, you'll see posts specifically directed to you.TimeLineFalls Sie noch kein Pump.io-Konto haben, knnen Sie z.B. bei folgenden Adressen eines bekommen:]If you don't have a Pump account yet, you can get one at the following address, for instance:TimeLineLade... Loading...TimeLine NeuerNewerTimeLineNeuesteNewestTimeLine lterOlderTimeLine Seite %1 von %2.Page %1 of %2.TimeLinefBeitrge und Kommentare, die Sie favorisiert haben. Posts and comments you've liked.TimeLineDrcken Sie <b>F1</b>, falls Sie das Hilfe-Fenster ffnen wollen.4Press F1 if you want to open the Help window.TimeLine"Pump.io-LeitfadenPump.io User GuideTimeLineAbfrage... Requesting...TimeLine8Zeige %1 Beitrge pro Seite.Showing %1 posts per page.TimeLineNehmen sich einen Moment, um sich in den Mens und im Konfigurationsfenster umzuschauen.DTake a moment to look around the menus and the Configuration window.TimeLine,Es gibt keine BeitrgeThere are no postsTimeLine Es gibt berall Tooltips, sodass Sie beim berfahren einer Schaltflche oder eines Eingabefeldes wahrscheinlich zustzliche Informationen sehen.There are tooltips everywhere, so if you hover over a button or a text field with your mouse, you'll probably see some extra information.TimeLine,Willkommen bei DianaraWelcome to DianaraTimeLineSie knnen auch Profilinformationen und -bild ber das Men <b>Einstellungen - Profil bearbeiten</b> einstellen.\You can also set your profile data and picture from the Settings - Edit Profile menu.TimeLineXHier werden Sie Ihre eigenen Beitrge sehen.You'll see your own posts here.TimeLineVor %1 Tagen %1 days ago TimestampVor %1 Stunden %1 hours ago TimestampVor %1 Minuten%1 minutes ago TimestampVor %1 Monaten %1 months ago TimestampVor %1 Jahren %1 years ago Timestamp Vor einer Minute A minute ago TimestampVor einem Monat A month ago TimestampVor einem Jahr A year ago Timestamp Vor einer Stunde An hour ago TimestampIn der Zukunft In the future Timestamp.Ungltiger Zeitstempel!Invalid timestamp! TimestampGerade jetztJust now TimestampGestern Yesterday Timestamp%1 Beitrge%1 posts UserPostsS&chlieen&Close UserPosts@Fehler beim Laden der ZeitleisteError loading the timeline UserPostsLade... Loading... UserPostsBeitrge von %1 Posts by %1 UserPosts*'%1' wurde empfangen.Received '%1'. UserPostsdianara-v1.4.1/translations/dianara_gl.qm0000664000175000017500000040031513221212314016541 0ustar janjancs<H<+>R@)DOBjH=h5N,>c_.>G2,ta^".y"44̸eՅTI5 /ҥH~o 7)[*B"{NZ_X`dqݣWss/$Yi&-!@0a<#&b<DIn`M$FMP3PkQE85RS4V8.Q\qNl o=p`N=#ƭ,6 BTrkfh> mW^qtbWx PЗ%# 2f (| Uh*23!ʑ6E=m%%h53*>KL=tQ$RxNbd.+hBLpH<tɥwv:kw|S`&J3fՂpi^3 TjtB66]6j66 v6E6 @Q }!H nMb!0ƭ>Z4rnoV ϠN. q؞d{|&Hw5W3U8YPN>d#|J , q* o}mp vSr n$1'y$1R(J.s׺0c^0c6h6t6'70G_EA9M.lXR@'ZN+Zed#i6ijC !l#gsCDjwh3x2z{e}z5.Q&Nni^{B{Cb{xކ¿n¿<Ì.P^jn˺>10\q&qPE޷I$?_#n*D!qAn%V?~ʳuL 1T SY  ~L#n7$X+h:6U(;~>B/JP `3Da$fXr) W|۪}::vJQ7[<K EYKbU>5]L=SI)\IMIj4'6Mi˜,%e,K<31,nԁx}^g}n7c%ZpmUJ4tͣS3g 3gxրvR@z557WUW Fҩ 9"K<#Q,(^]4>1KTKTz!Lܤ RMR-V|; ]L.]X\a%EbcBfy]h9~hTRXjsku_ny]z-{9{|7}̏H/.m,.{FzVVvYh>4CL>T,tT t%G=:`4N?ú|X\w0(\0Uƨ,2Won:Nbf&أ_**Ċ+C-^W4uWe4u\4uw69${<.Xz<.f<.xI=KAt+CMOGmbMI!LyIM?$O˿RQKT$GVхyZPZzZVf;:fgbhC mnNp{64r7uvy^a/p]5I \ ?NRna{S;Q9NmR,1̐},DCYV4&Eґwڑdڱ߳15xtSC#Diu%70S1;;;XoF<V<<\<v5S,VWcxgjlqNgpGrgt-{k8jh`ej žo~NrqENx?ROBx9ȯB;ҕ,>tnNh6i'.!nQ!  c#iKiIn=EE="#N )2K;2?36~~vF MJ"MJMrc\_]ee7]kZ)pFs#{Ssn2|2u |O1UVt'޳%"elN"B>BBc?z>/8qCa`Mrnť2ʪ$:]N|琊P3Q$RyNe?m A !E -Qg 0\~ 0u~ 1`#G 6 pl 7w8 <.~ Mg P1@ Sgm= ^n r%t s w>s[ | ~= < tf V 49S P C~ ~*  r  ` ! a B o$ dv b ] fZ ʞ U p & ~L Ԭ0 [C t* SV hC Nt A>' AV - ~} n$4 os ` !/ # (O ?m) @=c @Ccw E| Hk Tu TGy TG [d5 ]d} cv d 4G% d< d<* d<J dk8 e pO7 q\4 v'r z3.Ѽ _>)s .4  \* I I IF I IH I@ IBk IX? IaB )M ~ I 9  yM[ N , o*< d tNj b ^ '_L tE }^nE ՁQ5 3G} ֓1 N %| T# z ؄k S 0L Y2? :+ + + > ~ǿ ~Ԛ ~ z c3 y[>]2 |6d P/4 ! q3 SM #f CR V ζ  uv g )g  U(] ' sd' 5 j}( OC\ أTd y:x 2  CJ  X 2. Ғ#+ > (=I - UX / 6> =x/ Bg E RV YU [c ]ӡO `uY d.] eMi? j8 zO {l {^' P 4S k  L.  " >Q » ^qt q ˽+7 790 : b]h H ( G `Ij VDz3N42QK(TWceݦ+jN/\71L-DYR/5RrT3kXcgOJlNlIrlpllXlhmhn<mU$P8T$o8T8T8TFG^ZIz">c>#wQK'g$@Nϑ3Tɽ' Z^U~A%}*R!.UGjO5jyzrR- #u"E`^|_^i>zAF'5%''5%+'5%'E)1+<o,. 0*A BtnDB}~BpCUE.F JZPdTH_}7`pDbuaeY.6ue{`wiP2^0&/~n˾.edz^Wa~ld؞ۤ¹e@U$We*~P1 ')dz\"a]ڨdy-B#~ޝil%1 por %2%1 by %2 ASActivityPblicoPublic ASActivity ArtigoArticleASObject AudioAudioASObjectColeccin CollectionASObjectComentarioCommentASObjectBorrado en %1 Deleted on %1ASObjectFicheiroFileASObject GrupoGroupASObject ImaxeImageASObject6Non hai ubicacin detalladaNo detailed locationASObjectNotaNoteASObject OutroOtherASObject VdeoVideoASObjecte %1 mis and %1 othersASObjecte un mis and one otherASObject CidadeHometownASPerson*&Autorizar Aplicacin&Authorize Application AccountDialog&Cancelar&Cancel AccountDialog &Gardar Detalles &Save Details AccountDialog&Desbloquear&Unlock AccountDialogAgora arrancar un navegador, onde poder obter o cdigo de verificacinAA web browser will start now, where you can get the verifier code AccountDialog,Configuracin da ContaAccount Configuration AccountDialogDespois de premer neste botn, abrirase un navegador, solicitando autorizacin para DianaraYAfter clicking this button, a web browser will open, requesting authorization for Dianara AccountDialogfDianara est autorizada para acceder aos seus datos)Dianara is authorized to access your data AccountDialogIntroduza ou pegue eiqu o cdigo de verificacin que lle proporcionou o seu servidor PumpBEnter or paste the verifier code provided by your Pump server here AccountDialog~Primeiro, introduza o seu ID Webfinger, o seu enderezo pump.io.5First, enter your Webfinger ID, your pump.io address. AccountDialog:Obter Cdigo de &VerificacinGet &Verifier Code AccountDialogSe o navegador non se abre automticamente, copie este enderezo manualmenteEIf the browser doesn't open automatically, copy this address manually AccountDialogSe anda non ten unha conta, pode rexistrar unha e %1. Esta ligazn levaralle aleatoriamente a un servidor pblico.sIf you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. AccountDialog*Se necesita axuda: %1If you need help: %1 AccountDialogSe o seu perfil est en https://pump.exemplo/seunome, entn o seu enderezo seunome@pump.exemplo_If your profile is at https://pump.example/yourname, then your address is yourname@pump.example AccountDialog(Unha vez tea autorizado a Dianara dene a interfaz web do seu servidor Pump, recibir un cdigo chamdo VERIFIER. Copie e pgueo no campo de embaixo.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. AccountDialogxPrema Desbloquear se desexa configurar unha conta diferente.:Press Unlock if you wish to configure a different account. AccountDialog.Gua de Usuario Pump.ioPump.io User Guide AccountDialog<No se puido abrir o navegador!Unable to open web browser! AccountDialogJO cdigo de verificacin est baleiroVerifier code is empty AccountDialog.Cdigo de verificacin:Verifier code: AccountDialog@O seu enderezo Pump non vlidoYour Pump address is invalid AccountDialog.O seu enderezo Pump.io:Your Pump.io address: AccountDialogVA sa conta est configurada correctamente.$Your account is properly configured. AccountDialogO seu enderezo algo como nomedeusuario@servidor.org, e pdeo atopar no seu perfil, no interfaz web.kYour address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. AccountDialognO seu enderezo, tal como nomedeusuario@servidorpump.org*Your address, like username@pumpserver.org AccountDialog(&Engadir Seleccin&Add to SelectedAudienceSelector&Cancelar&CancelAudienceSelector &Feito&DoneAudienceSelectorLista 'Cc' 'Cc' ListAudienceSelectorLista 'Para' 'To' ListAudienceSelector"Tdolos Contactos All ContactsAudienceSelectorLimpar &Lista Clear &ListAudienceSelectorSeguidores FollowersAudienceSelector ListasListsAudienceSelectorXente... People...AudienceSelectorPblicoPublicAudienceSelectorSeleccione xente da lista da esquerda. Mode arrastralos con rato, facer click ou doble-click neles, ou seleccionalos e usar o botn de embaixo.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*Persoas SeleccionadasSelected PeopleAudienceSelector&Non&No AvatarButton*&S, deixar de seguir&Yes, stop following AvatarButtonZEsta vostede seguro de deixar de seguir a %1?+Are you sure you want to stop following %1? AvatarButton Revisar mensaxesBrowse messages AvatarButton SeguirFollow AvatarButtonBAbrir o perfil de %1 no navegador Open %1's profile in web browser AvatarButton>Abrir o seu perfil no navegador Open your profile in web browser AvatarButton&Enviar mensaxe a %1Send message to %1 AvatarButton Deixar de seguirStop following AvatarButton"Deixar de seguir?Stop following? AvatarButton(Ocultar esta mensaxeHide this messageBannerNotificationFIsto ocorre cando o momento de auto actualizar as lias temporais, pero non est enrriba de todo na primeira pxina, para evitar interrupcins mentres vostede leThis happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you readBannerNotificationAs lias temporais non se actualizaron automticamente para evitar interrupcins.@Timelines were not automatically updated to avoid interruptions.BannerNotification Actualizar agora Update nowBannerNotificationCambiar... Change... ColorPickerElixir unha corChoose a color ColorPicker:%1 lles gosta este comentario%1 like this commentComment6%1 gstalle este comentario%1 likes this commentComment&Non&NoComment&S, borralo&Yes, delete itCommentjEst vostede seguro de querer borrar este comentario?-Are you sure you want to delete this comment?Comment BorrarDeleteComment EditarEditComment,Borrar este comentarioErase this commentComment GostarLikeCommentTGostar ou deixar de gostar este comentarioLike or unlike this commentCommentModificado o %1Modified on %1Comment2Modificar este comentarioModify this commentCommentPublicado o %1 Posted on %1Comment CitarQuoteComment>Respostar citar este comentarioReply quoting this commentComment Deixar de gostarUnlikeComment8ATENCIN: Borrar comentario?WARNING: Delete comment?CommentOcurru un erroAn error occurredCommenterBlockCancelarCancelCommenterBlock8Comprobar se hai comentariosCheck for commentsCommenterBlockComentarCommentCommenterBlock4O comentario est baleiro.Comment is empty.CommenterBlock6Comentarios non dispoiblesComments are not availableCommenterBlock&Editando comentarioEditing commentCommenterBlock0Erro: Xa est redactandoError: Already composingCommenterBlock.Cargando comentarios...Loading comments...CommenterBlocknFalllou o comentario da publicacin. Tnteo outra vez.#Posting comment failed. Try again.CommenterBlockjPrema ESC para cancelar o comentario se non ten texto3Press ESC to cancel the comment if there is no textCommenterBlock(Recargar comentariosReload commentsCommenterBlock,Enviando comentario...Sending comment...CommenterBlock<Amosar todos os %1 comentariosShow all %1 commentsCommenterBlock4Actualizando comentario...Updating comment...CommenterBlockxPode premer Control+Enter para enviar o comentario c tecladoAYou can press Control+Enter to send the comment with the keyboardCommenterBlockNon pode editar un comentario agora porque xa est redactando outro comentario.YYou can't edit a comment at this time, because another comment is already being composed.CommenterBlock"&Cancelar ligazn &Cancel linkComposer,&Introducilo outra vez&Enter it againComposer&Formato&FormatComposer&Non&NoComposer"&Usalo igualmente&Use it anywayComposer&S, cancelalo&Yes, cancel itComposerXEst seguro de querer cancelar esta mensaxe?-Are you sure you want to cancel this message?ComposerNegritaBoldComposer"Cancelar mensaxe?Cancel message?ComposerjPrema eiqu o pulse Ctrl+N para escribir unha nota.../Click here or press Control+N to post a note...Composer(Erro: URL non vlidaError: Invalid URLComposerFormato FormattingComposerCabeceiroHeaderComposer4Cantas columnas (anchura)?How many columns (width)?Composer,Cantas filas (altura)?How many rows (height)?Composer(Inserte unha ligazn Insert a linkComposerDInsertar unha imaxe dende unha URLInsert an image from a URLComposerLInsertar unha imaxe dende un sitio webInsert an image from a web siteComposer*Insertar como imaxen?Insert as image?Composer*Insertar como ligaznInsert as linkComposer8Insertar como imaxen visibleInsert as visible imageComposerInsertar lia Insert lineComposer$Ligazn non vlida Invalid linkComposerNEsta debera comenzar cun destes tipos:(It should start with one of these types:ComposerItlicaItalicComposer ListaListComposer$Facer unha ligazn Make a linkComposerfFacer unha ligazn a partires do texto seleccionadoMake a link from selected textComposer NormalNormalComposer2Pegar Texto Sen FormatearPaste Text Without FormattingComposer(Bloque preformateadoPreformatted blockComposerBloque de Cita Quote blockComposerTachado StrikethroughComposerSmbolosSymbolsComposer TboaTableComposerTamao de Tboa Table SizeComposer8Opcins de Formateo de TextoText Formatting OptionsComposerA direccin que introducu (%1) non vlida. As direccins das imaxes deberan comenzar con http:// ou https://`The address you entered (%1) is not valid. Image addresses should begin with http:// or https://ComposerpA ligazn que est pegando semella apuntar a unha imaxe.4The link you are pasting seems to point to an image.Composer`O texto que introducu non semella unha ligazn./The text you entered does not look like a link.Composer4Teclee un comentario eiquType a comment hereComposerREscriba unha mensaxe eiqu para publicalaType a message here to post itComposerTeclee ou copie un enderezo web eiqu. O texto seleccionado (%1) converterase nunha ligazn.UType or paste a web address here. The selected text (%1) will be converted to a link.ComposerTeclee ou copie o enderezo web eiqu. Tamn pode seleccionar antes algo de texto, para convertelo nunha ligazn.`Type or paste a web address here. You could also select some text first, to turn it into a link.ComposerTeclee ou pegue a direccin da imaxe eiqu. A ligazn debe apuntar directamente ao ficheiro da imaxe.UType or paste the image address here. The link must point to the image file directly.ComposerSuliar UnderlineComposer<Si, mais gardan&do un borradorYes, but saving a &draftComposer6S pode mandar un ficheiro.You can attach only one file.ComposervVostede non pode arrastrar cartafoles aqu; s un ficheiro.1You cannot drop folders here, only a single file.Composer&Cancelar&Cancel ConfigDialog Tboas &Movibeis &Movable tabs ConfigDialogZ&Mensaxes por pxina, lia temporal principal&Posts per page, main timeline ConfigDialog*&Gardar Configuracin&Save Configuration ConfigDialog.Poisicin das Pes&taas&Tabs position ConfigDialog"Despois do avatar After avatar ConfigDialog0Despois do avatar, sutilAfter avatar, subtle ConfigDialog"Tdolos ficheiros All files ConfigDialogVSuliar tamn a entrada na barra de tarefasAlso highlight taskbar entry ConfigDialog SempreAlways ConfigDialog:Calquera actividade destacadaAny highlighted activity ConfigDialog:Como notificacins do sistemaAs system notifications ConfigDialog Tamao do avatar Avatar size ConfigDialog@Tamao do avatar nos comentariosAvatar size in comments ConfigDialogAntes do avatar Before avatar ConfigDialog,Antes do avatar, sutilBefore avatar, subtle ConfigDialogAbaixo de todoBottom ConfigDialog CoresColors ConfigDialogComentariosComments ConfigDialog EditorComposer ConfigDialog(&Icona Personalizada Custom &Icon ConfigDialog&Icona personalizada Custom icon ConfigDialogPor defectoDefault ConfigDialogLDianara garda os datos neste cartafol:#Dianara stores data in this folder: ConfigDialog^Non informar aos seguidores cando sigo a algun-Don't inform followers when following someone ConfigDialogdNon informar aos seguidores cando manexo as listas*Don't inform followers when handling lists ConfigDialog0Non amosar notificacinsDon't show notifications ConfigDialogDuracinDuration ConfigDialog$Reglas de filtradoFiltering rules ConfigDialog FontesFonts ConfigDialogOpcins XeraisGeneral Options ConfigDialog>Ocultar publicacins duplicadasHide duplicated posts ConfigDialog6Ocultar fiestra no arranqueHide window on startup ConfigDialog^Destacar os comentarios do autor da publicacin Highlight post author's comments ConfigDialogJDestacara os seus propios comentariosHighlight your own comments ConfigDialogdActividades destacadas en lia temporal secundaria$Highlighted activities in minor feed ConfigDialogPActividades destacadas, excepto as mias#Highlighted activities, except mine ConfigDialog.Publicacins destacadasHighlighted posts ConfigDialog8Ignorar erros SSL nas imaxesIgnore SSL errors in images ConfigDialog&Ficheiros de imaxen Image files ConfigDialog"Erros importantesImportant errors ConfigDialogXInformar s ao autor cando lle gosten cousas)Inform only the author when liking things ConfigDialog Imaxe non vlida Invalid image ConfigDialog`Elemento destacado debido s regras de filtrado.(Item highlighted due to filtering rules. ConfigDialog$O elemento novo. Item is new. ConfigDialogbSaltar lia de novas publicacins ao actualizar Jump to new posts line on update ConfigDialogLado esquerdo Left side ConfigDialog6Lias Temporais Secundarias Minor Feeds ConfigDialog`Tamaos dos avatares nas lias temporais menoresMinor feed avatar sizes ConfigDialog*Configuracin de redeNetwork configuration ConfigDialog NuncaNever ConfigDialogZNovas actividades en lia temporal secundariaNew activities in minor feed ConfigDialog"Novas publicains New posts ConfigDialogNonNo ConfigDialog,Estilo de NotificacinNotification Style ConfigDialogNotificacins Notifications ConfigDialog*Notificar ao recibir:Notify when receiving: ConfigDialogVS para imaxes insertadas dende sitios web.(Only for images inserted from web sites. ConfigDialogLEmpregaranse as notificacins propias.Own notifications will be used. ConfigDialog4Notificacins PersistentesPersistent Notifications ConfigDialog2Contidos das Publicacins Post Contents ConfigDialog0Ttulos das Publicacins Post Titles ConfigDialogPublicacinsPosts ConfigDialog`Publicacins por pxina; &outras lias temporais Posts per page, &other timelines ConfigDialogPrivacidadePrivacy ConfigDialog"Axustes de Pro&xyPro&xy Settings ConfigDialog2Configuracin do ProgramaProgram Configuration ConfigDialogDPublicacins pblicas por &defectoPublic posts as &default ConfigDialogLado dereito Right side ConfigDialogS&eleccionar... S&elect... ConfigDialog4Elixir icona personalizadaSelect custom icon ConfigDialog&Configurar F&iltrosSet Up F&ilters ConfigDialog6Amosar iconas de actividadeShow activity icons ConfigDialog:Amosar contador de caracteresShow character counter ConfigDialogXAmosar informacin de comparticin extendidaShow extended share information ConfigDialog8Amosar informacin adicionalShow extra information ConfigDialog@Amosar imaxes en tamao completoShow full size images ConfigDialogZAmosar informacin para publicacins borradas"Show information for deleted posts ConfigDialogPAmosar anacos en lias temporais menoresShow snippets in minor feeds ConfigDialog4Amosar o seu avatar actualShow your current avatar ConfigDialogLmite de anaco Snippet limit ConfigDialogHLmite de anaco cando sexa destacadoSnippet limit when highlighted ConfigDialog$Bandexa do Sistema System Tray ConfigDialogH&Tipo de Icona de Bandexa do SistemaSystem Tray Icon &Type ConfigDialogNConxunto de iconas do sistema, se o haiSystem iconset, if available ConfigDialogdNon estn dispoibeis as notificacins do sistema!'System notifications are not available! ConfigDialogA actividade en resposta a algo feito por vostede, tal como un comentario publicado en resposta a unha das sas notas.jThe activity is in reply to something done by you, such as a comment posted in reply to one of your notes. ConfigDialogA actividade est relacionada cun dos seus obxectos, como cando algun lle gosta unha das sas publicacins.YThe activity is related to one of your objects, such as someone liking one of your posts. ConfigDialogDA imaxe seleccionada non vlida. The selected image is not valid. ConfigDialog<Esta una notificacin bsicaThis is a basic notification ConfigDialogFEsta unha notificacin do sistemaThis is a system notification ConfigDialogXIntervalo de act&ualizacin da Lia TemporalTimeline &update interval ConfigDialogLias temporais Timelines ConfigDialogEnrribaTop ConfigDialogUsar o nome do ficheiro adxunto como ttulo inicial da publicacin-Use attachment filename as initial post title ConfigDialog"Usar con coidado.Use with care. ConfigDialogUsado tamn cando se resaltan publicacins dirixidas a vostede nas lias temporais.DUsed also when highlighting posts addressed to you in the timelines. ConfigDialogUsado tamn cando se resaltan as sas propias publicacins nas lias temmporais.Debaixo da pestaa 'Vecios' ver algns recursos para atopar xente, e ten a opcin de visualizar directamente os ltimos usuarios rexistrados no seu servidor.Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. HelpWidgetUse o parmetro --debug para obter informacin extra na sa fiestra de terminal, acerca de qu est a facer o programa.mUse the --debug parameter to have extra information in your terminal window, about what the program is doing. HelpWidget&Usuarios por idiomaUsers by language HelpWidgethMentras compn unha nota, prema Enter para saltar dende o ttulo ao corpo da mensaxe. Tamn, premendo a tecla Arriba mentras est no comenzo da mensaxe, lvalle de volta ao ttulo.While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. HelpWidgetTamn pode enviar unha mensaxe directa (inicialmente privada) ao contacto dende este men.VYou can also send a direct message (initially private) to that contact from this menu. HelpWidgetTamn pode teclear '@' e os primeiros caracteres do nome dun contacto para sacar un men emerxente con opcins axeitadas.wYou can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. HelpWidgetzPode premer en calquer avatar nas publicacins, nos comentarios e na columna Mentras tanto, e sairalle un men con varias opcins, unha das cales seguir ou deixar de seguir a esa persona.You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. HelpWidgetPode configurar varias cousas ao seu xeito nos axustes, como o intervalo de tempo entre actualizacins das lias temporais, cntas publicacins por pxina quere, cores de suliado, notificiacins ou a apariencia da icona da bandexa do sistema.You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. HelpWidgetPode crear mesaxes privadas engadindo xente concreta a estas listas, e desmarcando as opcins Seguidores ou Pblico.~You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. HelpWidgetPode atopar unha lista con algns usuarios Pump.io e outra informacin eiqu:GYou can find a list with some Pump.io users and other information here: HelpWidgetPode publicar notas premendo no campo de texto na parte superior da fiestra ou premendo Control+N. Poerlle un ttulo s sas publicacins opcional, mais altamente recomendable, xa que axudar a identificar mellor as referencias sa publicacin na lia temporal secundaria, notificacins por correo electrnico, etc.You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. HelpWidgetPode ver as listas de xente que segue, e qun lle segue na pestaa de Contactos.UYou can see the lists of people you follow, and who follow you from the Contacts tab. HelpWidgetPode seleccionar qun ver a sa publicacin usando os botns Para e Cc.EYou can select who will see your post by using the To and Cc buttons. HelpWidgetPode usar o parmetro --config para executar o programa cunha configuracin diferente. Isto poder ser til para usar das ou mis contas diferentes. Incluso pode executar das instancias de Dianara ao mesmo tempo.You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. HelpWidgetVPode usar o botn Formato para engadir formato ao seu texto, tal como negrita ou itlica. Algunhas destas opcins requiren que haxa texto seleccionado antes de empregalas.You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. HelpWidgetDebera botarlle un vistazo fiestra de Configuracin do Programa, no men Axustes - Configurar Dianara. Hai varias opcins interesantes ah.You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. HelpWidget&Pechar&Close ImageViewer&Recomenzar&Restart ImageViewer&Gardar Como... &Save As... ImageViewer"Tdolos ficheiros All files ImageViewerPechar Visor Close Viewer ImageViewer:Descargando imaxe completa...Downloading full image... ImageViewer.Erro descargando imaxe!Error downloading image! ImageViewer&Erro gardando imaxeError saving image ImageViewerEncaixarFit ImageViewer ImaxeImage ImageViewer$Ficheiros de imaxe Image files ImageViewer,Rotar imxe esquerdaRotate image to the left ImageViewer*Rotar imaxe dereitaRotate image to the right ImageViewer*Gardar Imaxen Como...Save Image As... ImageViewer Gardar Imaxen... Save Image... ImageViewerHoubo un problema gardando %1. O nome do ficheiro debera rematar en extensins .jpg ou .png.UThere was a problem while saving %1. Filename should end in .jpg or .png extensions. ImageViewer$Tnteo mis tarde.Try again later. ImageViewerSen TtuloUntitled ImageViewer &Engadir a Lista &Add to List ListsManager4Borrar Lista &Seleccionada&Delete Selected List ListsManager&Non&No ListsManager &Eliminar Membro&Remove Member ListsManager&S&Yes ListsManager&S, borralo&Yes, delete it ListsManagerEngadir Mem&bro Add Mem&ber ListsManager&Engadir Nova &Lista Add New &List ListsManagerPEsta vostede seguro de querer borrar %1?#Are you sure you want to delete %1? ListsManager\Esta seguro de querer quitar a %1 da lista %2?4Are you sure you want to remove %1 from the %2 list? ListsManagerCrear L&ista Create L&ist ListsManagerMembrosMembers ListsManagerNomeName ListsManager0Quitar persona da lista?Remove person from list? ListsManagerHEscriba un nome para a nova lista...Type a name for the new list... ListsManagerNEscriba eiqu unha descripcin opcional!Type an optional description here ListsManager.ATENCIN: Borrar lista?WARNING: Delete list? ListsManagerPe&char&Close LogViewer Limpar &Rexistro Clear &Log LogViewerRexistroLog LogViewer%1 bytes%1 bytes MainWindow%1 borrado. %1 deleted. MainWindow%1 filtrado.%1 filtered out. MainWindow%1 filtradas.plural, several activities%1 filtered out. MainWindow%1 destacada%1 highlighted MainWindow%1 destacadas.%1 highlighted. MainWindow8%1 mis pendente de recibir.%1 more pending to receive. MainWindow:%1 mis pendentes de recibir.plural, several activities%1 more pending to receive. MainWindow &Conta&Account MainWindow&Actividade &Activity MainWindow&&Configurar Dianara&Configure Dianara MainWindow&Contactos &Contacts MainWindow*&Filtros e Destacados&Filters and Highlighting MainWindow &Axuda&Help MainWindow Agoc&har Fiestra &Hide Window MainWindow&Rexistro&Log MainWindow&Mensaxes &Messages MainWindow&Non&No MainWindow&&Publicar unha Nota &Post a Note MainWindow &Sair&Quit MainWindow&Sesin&Session MainWindowAmo&sar Fiestra &Show Window MainWindowLia &Temporal &Timeline MainWindow*Barra de &Ferramentas&Toolbar MainWindow &Vista&View MainWindow,&S, pechar o programa&Yes, close the program MainWindow"'%1' actualizada. '%1' updated. MainWindow1 borrado. 1 deleted. MainWindow1 filtrado.1 filtered out. MainWindow1 filtrada. singular, refers to one activity1 filtered out. MainWindow1 destacada 1 highlighted MainWindow1 destacada.1 highlighted. MainWindow61 mis pendente de recibir.1 more pending to receive. MainWindow61 mis pendente de recibir.singular, 1 activity1 more pending to receive. MainWindow$Acerca de &DianaraAbout &Dianara MainWindow"Acerca de Dianara About Dianara MainWindow Tamn:Also: MainWindow@Auto actualizar &Lias TemporaisAuto-update &Timelines MainWindow@Auto-actualizacin deshabilitadaAuto-updating disabled MainWindow:Auto-actualizacin habilitadaAuto-updating enabled MainWindow&Axuda Bsica Basic &Help MainWindowPor filtros By filters MainWindowNPrema eiqu para configurar a sa conta$Click here to configure your account MainWindow<Prema para editar o seu perfilClick to edit your profile MainWindowXPechando porque o entorno estase a apagar...+Closing due to environment shutting down... MainWindowdNon se pode agochar Dianara na bandexa do sistema.,Dianara cannot be hidden in the system tray. MainWindowDianara Software Libre, licenciada baixo a licenza GNU-GPL, e usa algunas iconas Oxygen, baixo a licenza LGPL.lDianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. MainWindow`Dianara un cliente para a rede social pump.io..Dianara is a pump.io social networking client. MainWindow$Dianara arrancada.Dianara started. MainWindow"Mensaxes directasDirect messages MainWindow>Realmente quere pechar Dianara?$Do you really want to close Dianara? MainWindowLQuere pechar o programa completamente?,Do you want to close the program completely? MainWindowEditar &Perfil Edit &Profile MainWindowHTraducin ao galego por EVAnaRkISTO.#English translation by JanKusanagi. MainWindowdIntroduza o contrasinal para o seu servidor proxy:)Enter the password for your proxy server: MainWindow.Erro almacenando imaxe!Error storing image! MainWindowFavor&itos Favor&ites MainWindow$Pantalla C&ompleta Full &Screen MainWindow Inicializando...Initializing... MainWindow.ltima actualizain: %1Last update: %1 MainWindowEnlazar a: %1 Link to: %1 MainWindow>Lista dalgns &Usuarios Pump.ioList of Some Pump.io &Users MainWindowRPaneis e Barras de Ferramentas bloqueadasLocked Panels and Toolbars MainWindow,Marcar Todo Como LedoMark All as Read MainWindow6Marcando todo como ledo...Marking everything as read... MainWindowTMensaxes enviadas explcitamente a vostedeMessages sent explicitly to you MainWindowVActividades secundarias dirixidas a vostede!Minor activities addressed to you MainWindowActividades secundarias feitas por todo o mundo, tales como respostar s publicacinsStarting automatic update of timelines, once every %1 minutes. MainWindow &Barra de Estado Status &Bar MainWindowjParando actualizacin automtica das lias temporais.'Stopping automatic update of timelines. MainWindowdA icona da bandexa do sistema non est dispoible."System tray icon is not available. MainWindowGrazas a todos os probadores, tradutores e empaquetadores, que axudan a mellorar Dianara!SThanks to all the testers, translators and packagers, who help make Dianara better! MainWindow2A lia temporal principalThe main timeline MainWindowA xente que segue, os que lle seguen a vostede, e as sas listas de personasEThe people you follow, the ones who follow you, and your person lists MainWindow2Hai %1 novas actividades.There are %1 new activities. MainWindow4Hai %1 novas publicacions.There are %1 new posts. MainWindow,Hai 1 nova actividade.There is 1 new activity. MainWindow.Hai 1 nova publicacin.There is 1 new post. MainWindowBLila temporal actualizada s %1.Timeline updated at %1. MainWindow(Barra de ferramentasToolbar MainWindow.Publicacins totais: %1Total posts: %1 MainWindow Actualizacin %1 Update %1 MainWindow$Visitar Sitio &WebVisit &Website MainWindowlCon Dianara pode ver as sas lias temporais, crear novas publicacins, subir imaxes ou outros medios, interactuar coas publicacins, xestionar os seus contactos e seguir nova xente.With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. MainWindowVEst escrebendo unha nota ou un comentario.&You are composing a note or a comment. MainWindowConfigurou un servidor proxy con autentificacin, pero non puxo o contrasinal.TYou have configured a proxy server with authentication, but the password is not set. MainWindowPA sa conta Pump.io non est configurada&Your Pump.io account is not configured MainWindowNA sa conta anda non est configurada.#Your account is not configured yet. MainWindow8A sa biografa est baleiraYour biography is empty MainWindow8A ss publicacins favoritasYour favorited posts MainWindow(As sas publicacinsYour own posts MainWindowObter %1 novos Get %1 newer MinorFeedBObter actividades menores previasGet previous minor activities MinorFeed0Actividades Mis AntigasOlder Activities MinorFeedJAnda non hai actividades que amosar.$There are no activities to show yet. MinorFeed Cc: %1Cc: %1 MinorFeedItem8Abrir a mensaxe referenciadaOpen referenced post MinorFeedItem A: %1To: %1 MinorFeedItemUsando %1Using %1 MinorFeedItemNErro: Non se puido arrancar o navegadorError: Unable to launch browser MiscHelpersrNon se puido executar o navegador por defecto do sistema.5The default system web browser could not be executed. MiscHelpersTPode necesitar instalar as utilidades XDG.,You might need to install the XDG utilities. MiscHelpers bytesbytes MiscHelpers&Cancelar&Cancel PageSelector&Primeira&First PageSelector&Ir&Go PageSelector&Derradeira&Last PageSelectorSaltar pxina Jump to page PageSelectorMis novasNewer PageSelectorMis antigasOlder PageSelectorPxina nmero: Page number: PageSelector&Cancelar&Cancel PeopleWidget&Procurar:&Search: PeopleWidget8Engadir lista de contactosAdd a contact to a list PeopleWidgetLIntroduza eiqu un nome para procuralo"Enter a name here to search for it PeopleWidget%1 comentarios %1 commentsPost$%1 lles gusta isto %1 like thisPost%1 Gstame%1 likesPost"%1 lle gusta isto %1 likes thisPost&%1 membros no grupo%1 members in the groupPost"%1 compartu isto%1 shared thisPost&%1 compartiron isto#%1 = Names for more than one person%1 shared thisPost&Pechar&ClosePost&Non&NoPost&S, brraa&Yes, delete itPost&S, compartila&Yes, share itPost2&S, deixar de compartila&Yes, unshare itPost1 comentario 1 commentPost1 Gstame1 likePost\Esta seguro de querer borrar esta publicacin?*Are you sure you want to delete this post?PostnEst certo de quere compartir a sa propia publicacin?-Are you sure you want to share your own post?PostAudio AdxuntoAttached AudioPost Ficheiro Adxunto Attached FilePostVdeo AdxuntoAttached VideoPostCcCcPostTPrema na imaxe para vela a tamao completo&Click the image to see it in full sizePost<Prema para descargar o adxunto Click to download the attachmentPostComentarCommentPostXCopiar ligazn da publicacin ao portapapeisCopy post link to clipboardPost6Non puiden cargar a imaxen!Couldn't load image!Post BorrarDeletePostHQuere compartir a publicacin de %1?Do you want to share %1's post?Post\Quere deixar de compartir a publicacin de %1?!Do you want to unshare %1's post?Post EditarEditPostEditado: %1 Edited: %1Post.Borrar esta publicacinErase this postPostNSe selecciona algn texto, ser citado.+If you select some text, it will be quoted.Post^A imaxe animada. Prema nela para reproducila.'Image is animated. Click on it to play.PostEnInPostUnirse a Grupo Join GroupPostGstameLikePost0Gstame esta publicacinLike this postPost"Cargando imaxe...Loading image...PostModificado o %1Modified on %1Post4Modificar esta publicacinModify this postPost6Normalizar colores do textoNormalize text colorsPost<Abrir publicacin no navegadorOpen post in web browserPostXAbrir a publicacin nai, que sta resposta/Open the parent post, to which this one repliesPostNaiParentPostMensaxePostPostPublicado o %1 Posted on %1Post:Respostar a esta publicacin.Reply to this post.PostCompartirSharePost,Compartir publicacin? Share post?PostZCompartir esta publicacin cos seus contactos"Share this post with your contactsPost&Compartido %1 vecesShared %1 timesPost Compartido en %1 Shared on %1Post&Compratido unha vez Shared oncePost TamaoSizePostParaToPostTipoTypePostNon gostarUnlikePostDescompartirUnsharePost@Deixar de compartir publicacin? Unshare post?Post:Descompartir esta publicacinUnshare this postPostUsando %1Using %1PostA travs de %1Via %1Post:ATENCIN: Borrar publicacin?WARNING: Delete post?Post.A vostede gstalle isto You like thisPost&Bio&Bio ProfileEditor&Cancelar&Cancel ProfileEditor&Cidade &Hometown ProfileEditor&Gardar Perfil &Save Profile ProfileEditor"Tdolos ficheiros All files ProfileEditor AvatarAvatar ProfileEditor$Cambiar &Avatar...Change &Avatar... ProfileEditor@Cambiar &Enderezo electrnico...Change &E-mail... ProfileEditorCambiar o seu avatar crear unha publicacin na sa lia temporal con el. Se borra esa publicacin tamn se borrar a seu avatar.zChanging your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. ProfileEditor(Enderezo electrnicoE-mail ProfileEditor&Nome Completo Full &Name ProfileEditor$Ficheiros de imaxe Image files ProfileEditor Imaxe non vlida Invalid image ProfileEditor Non especificadoNot set ProfileEditor Editor do PerfilProfile Editor ProfileEditor:Seleccione imaxen para avatarSelect avatar image ProfileEditorDA imaxe seleccionada non vlida. The selected image is not valid. ProfileEditorEste o enderezo electrnico asociado sa conta, para cousas como notificacins e recuperacin de contrasinaloThis is the e-mail address associated with your account, for things such as notifications and password recovery ProfileEditor4Este o seu enderezo PumpThis is your Pump address ProfileEditor4Este o seu nombe visibleThis is your visible name ProfileEditorWebfinger ID Webfinger ID ProfileEditor&Cancelar&Cancel ProxyDialogNome de &Host &Hostname ProxyDialog &Porto&Port ProxyDialog&Gardar&Save ProxyDialog&Usuario&User ProxyDialog"Non usar un proxyDo not use a proxy ProxyDialog"Nota: O contrasinal non est gardado de xeito seguro. Se o desexa, pode deixar o campo baleiro, e se lle preguntar polo contrasinal no arranque.Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. ProxyDialogContrasina&l Pass&word ProxyDialog&Tipo de Proxy Proxy &Type ProxyDialog,Configuracin do ProxyProxy Configuration ProxyDialog2Empregar &AutentificacinUse &Authentication ProxyDialog:O seu nom de usuario no proxyYour proxy username ProxyDialog0%1 KiB de %2 KiB subidos%1 KiB of %2 KiB uploaded Publisher>&Cancelar, voltar publicacin&Cancel, go back to the post PublisherR&Non, publicar s para os meus seguidores&No, post to my followers only Publisher&&S, facelo pblico&Yes, make it public PublisherEnga&dir...Ad&d... PublisherzEngada eiqu un ttulo breve para a publicacin (recomendado)1Add a brief title for the post here (recommended) Publisher"Tdolos ficheiros All files Publisher AudioAudio PublisherBFicheiro de audio non establecidoAudio file not set Publisher$Ficheiros de audio Audio files PublisherCancelarCancel Publisher^Cancelar o adxunto, e voltar a unha nota normal4Cancel the attachment, and go back to a regular note Publisher$Cancelar a mensaxeCancel the post Publisher Cc...Cc... Publisher*Actualmente Dianara limita a subida de ficheiros a 10 MiB por publicacin, para previr posibles problemas de almacenamento ou de rede nos servidores.yDianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. PublisherQuere facer pblica a publicacin en lugar de s para os seus seguidores?>Do you want to make the post public instead of followers-only? Publisher"Borrador cargado. Draft loaded. Publisher"Borrador gardado. Draft saved. PublisherBorradoresDrafts Publisher:ERRO: Anda estase a compoerERROR: Already composing Publisher*Editando publicacin. Editing post. PublisherErroError Publisher0Erro: Xa est a compoerError: Already composing Publisher:O ficheiro demasiado grandeFile is too big Publisher*Ficheiro non atopado.File not found. Publisher4Ficheiro non seleccionado.File not selected. Publisher2Ficheiro non seleccionado File not set PublisherZAtopar o ficheiro de audio nos seus cartafois#Find the audio file in your folders PublisherHAtopar o ficheiro nos seus cartafoisFind the file in your folders PublisherBAtopar a imaxe nos seus cartafois Find the picture in your folders PublisherBAtopar o vdeo nos seus cartafoisFind the video in your folders PublisherXPrema Control+nter para publicar co teclado+Hit Control+Enter to post with the keyboard PublisherrSe publica deste xeito, ningun poder ver a sa mensaxe.?If you post like this, no one will be able to see your message. PublishernIgnorando peticin de nova nota dende outra aplicacin.3Ignoring new note request from another application. Publisher$Ficheiros de Imaxe Image files Publisher8Ficheiro de audio non vlidoInvalid audio file Publisher"Ficheiro invlido Invalid file Publisher"Imaxen non vlida Invalid image Publisher8Ficheiro de vdeo non vlidoInvalid video file Publisher$ propiedade de %1It is owned by %1. PublisherLNota arrancada dende outra aplicacin.&Note started from another application. Publisher OutrosOther Publisher ImaxePicture Publisher,Imaxen non establecidaPicture not set PublisherPublicarPost Publisher6A publicacin est baleira.Post is empty. PublisherNPublicacin fallida. Tnteo outra vez.Posting failed. Try again. PublisherPublicando... Posting... PublisherEliminarRemove PublisherResolucin Resolution Publisher@Seleccionar Ficheiro de Audio...Select Audio File... Publisher,Seleccione Ficheiro...Select File... PublisherElixa Imaxe...Select Picture... Publisher&Seleccione Vdeo...Select Video... Publisher>Seleccione un ficheiro de audioSelect one audio file Publisher,Seleccione un ficheiroSelect one file Publisher*Seleccione unha imaxeSelect one image Publisher>Seleccione un ficheiro de vdeoSelect one video file PublisherjSeleccione qun recibir unha copia desta publicacin'Select who will get a copy of this post PublisherDSeleccionar quen ver esta mensaxeSelect who will see this post PublisherEscreber un ttulo axuda a que a lista Mentras tanto sexa mis informativa>Setting a title helps make the Meanwhile feed more informative PublisherXa que est a subir unha imaxe, podera reducirlle a escala ou gardala nun formato mis comprimido, como JPG.sSince you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. Publisher TamaoSize PublisherLPregmoslle disculpas polas molestias.Sorry for the inconvenience. PublisherPNon se pode detectar o formato do audio.$The audio format cannot be detected. PublisherPNon se pode detectar o tipo de ficheiro.!The file type cannot be detected. PublisherNon se pode detectar o formato da imaxe. A extensin pode ser incorrecta, como unha imaxe GIF renomeada a imaxen.jpg ou similar.tThe image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. PublisherXNo se pode acceder ao ficheiro seleccionado:%The selected file cannot be accessed: PublisherPNon se pode detectar o formato do vdeo.$The video format cannot be detected. PublisherEst unha medida temporal, xa que os servidores anda non poden establecer os seus propios lmites.OThis is a temporary measure, since the servers cannot set their own limits yet. Publisher TtuloTitle PublisherPara...To... PublisherTipoType PublisherActualizarUpdate PublisherActualizando... Updating... PublisherFSubir medios, como imaxes ou vdeos%Upload media, like pictures or videos Publisher VdeoVideo Publisher$Ficheiros de vdeo Video files Publisher,Vdeo non seleccionado Video not set PublisherTAtencin: Vostede anda non ten seguidores"Warning: You have no followers yet PublisherNon pode crear unha mensaxe para %1 neste intre, porque xa se est a compoer unha mensaxe.YYou can't create a message for %1 at this time, because a post is already being composed. PublisherNon pode editar a mensaxe neste intre porque xa se est compoendo outra mensaxe.MYou can't edit a post at this time, because a post is already being composed. PublisherNon pode cargar un borrador neste intre porque agora estase compoendo outra mensaxe.NYou can't load a draft at this time, because a post is already being composed. PublisherdPoida que non tea vostede os permisos necesarios.-You might not have the necessary permissions. PublisherEst tentando escribirlle s aos seus seguidores, pero anda non ten seguidores.SYou're trying to post to your followers only, but you don't have any followers yet. PublisherN%1 (%2) engadido lista correctamente.#%1 (%2) added to list successfully.PumpControllerR%1 (%2) eliminado da lista correctamente.'%1 (%2) removed from list successfully.PumpController%1 intentos %1 attemptsPumpController2%1 comentarios recibidos.%1 comments received.PumpController%1 publicado correctamente. Actualizando contido da publicacin...3%1 published successfully. Updating post content...PumpController1 intento 1 attemptPumpController,1 comentario recibido.1 comment received.PumpControllerAccinsActionsPumpControllerActividadeActivityPumpController,Engadindo elementos...Adding items...PumpController8Engadindo persona lista...Adding person to list...PumpControllerzRecibidos todos os datos iniciais. Inicializacin completada.3All initial data received. Initialization complete.PumpControllerHAplicacin autorizada correctamente.$Application authorized successfully.PumpController(Erro de autorizacinAuthorization errorPumpControllerhAutorizad a usar a conta %1. Obtendo datos iniciais.3Authorized to use account %1. Getting initial data.PumpController>Avatar publicado correctamente.Avatar published successfully.PumpControllerAvatar subido.Avatar uploaded.PumpController&Pasarela incorrecta Bad GatewayPumpControllerMala Peticin Bad RequestPumpControllerBNon pode seguir a %1 neste intre.Can't follow %1 at this time.PumpControllerVComprobando direccin %1 antes de seguir...'Checking address %1 before following...PumpControllerDComentario %1 publicado con xito.Comment %1 posted successfully.PumpControllerPComentario %1 actualizado correctamente. Comment %1 updated successfully.PumpController Creando grupo...Creating group...PumpController6Creando lista de persoas...Creating person list...PumpController8Borrando lista de persoas...Deleting person list...PumpControllerHEnderezo electrnico actualizado: %1E-mail updated: %1PumpController,Erro conectando con %1Error connecting to %1PumpControllerNErro cargando lia temporal secundaria!Error loading minor feed!PumpController8Erro cargando lia temporal!Error loading timeline!PumpControllerFavoritos FavoritesPumpControllerhFicheiro subido correctamente. Publicando mensaxe....File uploaded successfully. Posting message...PumpControllerSeguidores FollowersPumpControllerSeguindo FollowingPumpControllerBSeguindo a %1 (%2) correctamente.Following %1 (%2) successfully.PumpControllerProhibido ForbiddenPumpControllerFTempo de espera da pasarela agotadoGateway TimeoutPumpControllerObtend %1...Getting '%1'...PumpController0Obtendo o token OAuth...Getting OAuth token...PumpControllerBObtendo unha lista de personas...Getting a person list...PumpController,Obtendo comentarios...Getting comments...PumpController,Recuperando Gstame...Getting likes...PumpController@Obtendo lista de 'Seguidores'...Getting list of 'Followers'...PumpController<Obtendo lista de 'Seguindo'...Getting list of 'Following'...PumpControllerLObtendo lista de listas de personas...Getting list of person lists...PumpController>Obtendo usuarios do sitio %1...Getting site users for %1...PumpController FoiseGonePumpController<Grupo %1 creado correctamente.Group %1 created successfully.PumpController:Grupo %1 unido correctamente.Group %1 joined successfully.PumpControllerErro HTTP HTTP errorPumpController0Erro Interno do ServidorInternal Server ErrorPumpController(Unndose ao grupo...Joining group...PumpController(Abandonando grupo...Leaving group...PumpControllerFAbandonou o grupo %1 correctamente.Left the %1 group successfully.PumpController$Gstame recibidos.Likes received.PumpControllerBLista de usuarios de %1 recibida.List of %1 users received.PumpControllerZLista de 'seguidores' recibida completamente.(List of 'followers' completely received.PumpControllerVLista de 'seguindo' recibida completamente.(List of 'following' completely received.PumpController6Recibida lista de 'listas'.List of 'lists' received.PumpControllerCargando imaxen externa dende %1 a pesares dos erros SSL, tal como est configurado...ILoading external image from %1 regardless of SSL errors, as configured...PumpControllerMentras tanto MeanwhilePumpControllerMencinsMentionsPumpController4Mensaxe borrada con xito.Message deleted successfully.PumpControllerAccin de gostar ou deixar de gostar mensaxe completouse con xito.&Message liked or unliked successfully.PumpControllerMensaxesMessagesPumpController,Movido PermanentementeMoved PermanentlyPumpController(Movido TemporalmenteMoved TemporarilyPumpControllerNon Atopado Not FoundPumpController Non ImplementadoNot ImplementedPumpControllerJErro OAuth ao autorizar a aplicacin.*OAuth error while authorizing application.PumpController*Erro de soporte OAuthOAuth support errorPumpControllerNRecibida lista parcial de 'seguidores'.%Partial list of 'followers' received.PumpControllerJRecibida lista parcial de 'seguindo'.%Partial list of 'following' received.PumpControllerXLista de personas '%1' creada correctamente.&Person list '%1' created successfully.PumpControllerPLista de personas borrada correctamente.!Person list deleted successfully.PumpController6Lista de personas recibida.Person list received.PumpControllerNPublicacin %1 publicada correctamente.Post %1 published successfully.PumpControllerRPublicacin %1 actualizada correctamente.Post %1 updated successfully.PumpControllerVPublicacin de %1 compartida correctamente.Post by %1 shared successfully.PumpController Perfil recibido.Profile received.PumpController&Perfil actualizado.Profile updated.PumpControllerQOAuth erro %1QOAuth error %1PumpController Listo.Ready.PumpControllerRecibido '%1'.Received '%1'.PumpController8Borrando persona da lista...Removing person from list...PumpController:Erros SSL na conexin con %1!SSL errors in connection to %1!PumpController.Versin do servidor: %1Server version: %1PumpController,Servizo Non DispoibleService UnavailablePumpController4Algns datos iniciais non se recibiron tras varios intentos. Algo podera ir mal no seu servidor. Anda as debera poder usar o servicio con normalidade.Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally.PumpControllerNon se recibiron alguns datos iniciais. Reiniciando inicializacin...@Some initial data was not received. Restarting initialization...PumpControllerfAnda esperando polo perfil. Tentndoo outra vez...*Still waiting for profile. Trying again...PumpControllerRDeixou de seguir a %1 (%2) correctamente.'Stopped following %1 (%2) successfully.PumpControllerA aplicacin anda non est rexistrada co seu servidor. Rexistrando...FThe application is not registered with your server yet. Registering...PumpControllerOs comentarios para esta publicacin non se poden cargar porque hai datos que faltan no servidor.NThe comments for this post cannot be loaded due to missing data on the server.PumpController<Non hay.unha conta autorizada.There is no authorized account.PumpControllerHoubo un erro OAuth mentres se tentaba obter o token de autorizacin.EThere was an OAuth error while trying to get the authorization token.PumpControllerLia temporalTimelinePumpController*Tentando seguir a %1.Trying to follow %1.PumpController@Imposible verificar a direccin!Unable to verify the address!PumpControllerNon Autorizado UnauthorizedPumpControllerFCdigo de erro HTTP %1 non manexadoUnhandled HTTP error code %1PumpControllerfPublicacin sen titular %1 publicada correctamente.(Untitled post %1 published successfully.PumpControllerhPublicacin sen ttulo %1 actualizada correctamente.&Untitled post %1 updated successfully.PumpController,Actualizando perfil...Updating profile...PumpControllerSubindo %1 Uploading %1PumpController0Lia temporal do usuario User timelinePumpControllerJAgardando por contrasinal do proxy...Waiting for proxy password...PumpControllerProbablemente necesite instalar o plugin OpenSSL para QCA: %1, %2 ou similar.KYou probably need to install the OpenSSL plugin for QCA: %1, %2 or similar.PumpControllerA sa instalacin de QOAuth, a librara empregada por Dianara, non semella ter soporte HMAC-SHA1._Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support.PumpController"%1 usuarios en %2%1 users in %2 SiteUsersListPechar lista Close list SiteUsersListVObter lista de usuario dende o seu servidor"Get list of users from your server SiteUsersListlLista de Seguidores para a conta de Comunidade Pump.io3List of Followers for the Pump.io Community account SiteUsersListCargando... Loading... SiteUsersListFMis recursos para atopar usuarios:More resources to find users: SiteUsersListjServicio PPump de procura de usuario en inventati.org*PPump user search service at inventati.org SiteUsersListBPxina Wiki 'Usuarios por idioma'Wiki page 'Users by language' SiteUsersListPode obter unha lista dos novos usuarios rexistrados no seu servidor premendo no botn de embaixo.^You can get a list of the newest users registered on your server by clicking the button below. SiteUsersListz%1 mis publicacins pendentes para a seguinte actualizacin.&%1 more posts pending for next update.TimeLine2%1 publicacins en total.%1 posts in total.TimeLineNon se pode actualizar %1 porque estase a compoer unha mensaxe neste intre.E'%1' cannot be updated because a comment is currently being composed.TimeLine6Lia Temporal de ActividadeActivity TimelineTimeLineUnha vez rematado o proceso, o seu perfil e as lias temporais deberan actualizarse automticamente.RAfter the process is done, your profile and timelines should update automatically.TimeLinePrema eiqu ou pulse Control+G para saltar a unha pxina especfica8Click here or press Control+G to jump to a specific pageTimeLineBPrema eiqu para recibilos agora.Click here to receive them now.TimeLineHDianara un cliente <b>Pump.io</b>.#Dianara is a Pump.io client.TimeLineBlog de DianaraDianara's blogTimeLineDLia Temporal de Mensaxes DirectasDirect Messages TimelineTimeLine4Lia Temporal de FavoritosFavorites TimelineTimeLinePrimeiro, configure a sa conta dende o men <b> Conta - Axustes</b>.FFirst, configure your account from the Settings - Account menu.TimeLine~Eiqu ver as publicacins dirixidas especficamente a vostede.4Here, you'll see posts specifically directed to you.TimeLineSe anda non ten unha conta Pump, pode obter unha no seguinte enderezo, por exemplo:]If you don't have a Pump account yet, you can get one at the following address, for instance:TimeLineCargando... Loading...TimeLineMis novoNewerTimeLineAs mis novasNewestTimeLineMis antigasOlderTimeLine Pxina %1 de %2.Page %1 of %2.TimeLineXPublicacins e comentarios que lle gustaron. Posts and comments you've liked.TimeLinedPrema <b>F1</b> se quere abrir a fiestra de Axuda.4Press F1 if you want to open the Help window.TimeLine.Gua de Usuario Pump.ioPump.io User GuideTimeLineSolicitando... Requesting...TimeLineHAmosando %1 publicacins por pxina.Showing %1 posts per page.TimeLineTome uns momentos para revisar os mens e a fiestra de Configuracin.DTake a moment to look around the menus and the Configuration window.TimeLine(Non hai publicacinsThere are no postsTimeLine.Hai pistas emerxentes por todas partes, de xeito que se move o ratn sobre un botn ou un campo de texto, probablemente ver algo de informacin extra.There are tooltips everywhere, so if you hover over a button or a text field with your mouse, you'll probably see some extra information.TimeLine&Benvido/a a DianaraWelcome to DianaraTimeLineTamn pode establecer os datos do seu perfil e a imaxe no men <b>Axustes - Edictar Perfil</b>.\You can also set your profile data and picture from the Settings - Edit Profile menu.TimeLinePEiqu ver as sas propias publicacins.You'll see your own posts here.TimeLineHai %1 das %1 days ago TimestampHai %1 horas %1 hours ago TimestampHai %1 minutos%1 minutes ago TimestampHai %1 meses %1 months ago TimestampHai %1 anos %1 years ago TimestampHai un minuto A minute ago TimestampHai un mes A month ago TimestampHai un ano A year ago TimestampHai unha hora An hour ago TimestampNo futuro In the future Timestamp4Marca de tempo non vlida!Invalid timestamp! TimestampAgora mesmoJust now TimestampOnte Yesterday Timestamp%1 mensaxes%1 posts UserPosts&Pechar&Close UserPosts:Erro cargando a lia temporalError loading the timeline UserPostsCargando... Loading... UserPostsMensaxes de %1 Posts by %1 UserPostsRecibido: '%1'.Received '%1'. UserPostsdianara-v1.4.1/translations/dianara_it.ts0000644000175000017500000066223713212033716016606 0ustar janjan ASActivity Public Pubblico %1 by %2 1=kind of object: note, comment, etc; 2=author's name %1 di %2 ASObject Note Noun, an object type Nota Article Noun, an object type Articolo Image Noun, an object type Immagine Audio Noun, an object type Audio Video Noun, an object type Video File Noun, an object type File Comment Noun, as in object type: a comment Commento Group Noun, an object type Gruppo Collection Noun, an object type Collezione Other As in: other type of post Altro No detailed location Nessuna località dettagliata Deleted on %1 Eliminato il %1 and one other e nessun altro and %1 others e %1 altri ASPerson Hometown Città AccountDialog Your Pump.io address: Il tuo indirizzo Pump.io: Get &Verifier Code Ottieni il Codice di &Verifica Verifier code: Codice di Verifica: Enter or paste the verifier code provided by your Pump server here Inserisci o incolla il codice di verifica fornito dal tuo server Pump qui &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 If you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. 1=link to website Se non hai ancora un account, puoi iscriverti su %1. Questo link ti porterà su un server pubblico casuale. If you need help: %1 Se hai bisogno di aiuto: %1 Pump.io User Guide Guida Utente Pump.io Your address, like username@pumpserver.org Il tuo indirizzo, tipo nomeutente@serverpump.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. &Authorize Application &Autorizza Applicazione &Cancel &Cancella Your account is properly configured. Il tuo account è configurato correttamente. Press Unlock if you wish to configure a different account. Premi Sblocca se vuoi configurare un altro account. &Unlock &Sblocca 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 Unable to open web browser! AudienceSelector 'To' List Lista 'A' 'Cc' List Lista 'Cc' &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. ON THE LEFT should change to ON THE RIGHT in RTL languages 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 Public Pubblico Followers Followers Lists Liste People... Persone... Selected People Persone Selezionate AvatarButton Open %1's profile in web browser Apri il profilo di %1 nel browser web Open your profile in web browser Apri il tuo profilo nel browser web Send message to %1 Invia messaggio a %1 Browse messages Stop following Smetti di seguire Follow Segui 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 BannerNotification Timelines were not automatically updated to avoid interruptions. This happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you read Update now Hide this message ColorPicker Change... Cambia... Choose a color Comment Posted on %1 Pubblicato il %1 Modified on %1 Modificato il %1 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 Edit Modifica Modify this comment Modifica questo commento Delete Elimina Erase 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 You can press Control+Enter to send the comment with the keyboard Premi Control+Invio per inviare il commento con la tastiera Reload comments Ricarica commenti Comment Infinitive verb Commentare Cancel Cancella Press ESC to cancel the comment if there is no text Premi ESC per annullare il commento, se non c'è testo Check for comments Show all %1 comments Mostra tutti i %1 commenti Comments are not available Error: Already composing Errore: stai già scrivendo You can't edit a comment at this time, because another comment is already being composed. Non puoi modificare ora il commento, perchè ne stai già scrivendo un altro. Editing comment Modificando il commento Loading comments... Posting comment failed. Try again. Invio del commento fallito. Prova di nuovo. An error occurred Sending comment... Inviando il commento... Updating comment... Aggiornando 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 List Lista Table Tabella 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 You can attach only one file. You cannot drop folders here, only a single file. Insert as image? Inserire come immagine? The link you are pasting seems to point to an image. Il collegamento che stai incollando sembra puntare ad un'immagine. Insert as visible image Inserisci come immagine visibile Insert as link Inserisci come collegamento Table Size Dimensioni Tabella How many rows (height)? Quante righe (altezza)? How many columns (width)? Quante colonne (larghezza)? Type or paste a web address here. You could also select some text first, to turn it into a link. Scrivi o incolla un indirizzo web qui. Puoi anche selezionare del testo, per convertirlo poi in un link. Invalid link The text you entered does not look like a link. It should start with one of these types: It = the link, from previous sentence &Use it anyway &Enter it again &Cancel link Type or paste the image address here. The link must point to the image file directly. Scrivi o incolla l'indirizzo dell'immagine qui. Il link deve puntare direttamente all'immagine. Yes, but saving a &draft &Format Button for text formatting and related options &Formato 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 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 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 Top In alto Bottom In basso Program Configuration Configurazione del programma Timeline &update interval Intervallo di &aggiornamento della timeline &Tabs position Posizione delle &tab &Movable tabs &Tab mobili Minor Feeds Feed Minori 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 Ignore SSL errors in images Public posts as &default Messaggi pubblici come &default Pro&xy Settings Impostazioni del Pro&xy Network configuration Configurazione di Rete Set Up F&ilters Imposta F&iltri Filtering rules Regole di filtraggio Highlighted activities, except mine Attività selezionate, escluse le mie Any highlighted activity Qualsiasi attività selezionata Always Sempre Never Mai characters This is a suffix, after a number caratteri Snippet limit Limite Snippet Post Titles Titoli dei Post Post Contents Contenuto dei post Comments Commenti You are among the recipients of the activity, such as a comment addressed to you. Sei tra i destinatari dell'attività, per esempio un commento indirizzato a te. Used also when highlighting posts addressed to you in the timelines. Usato anche quando i post in evidenza sono indirizzati a te nelle timeline. The activity is in reply to something done by you, such as a comment posted in reply to one of your notes. L'attività è in risposta a qualcosa fatto da te, come un commento postato in risposta a una delle tue note. You are the object of the activity, such as someone adding you to a list. Sei l'oggetto dell'attività, per esempio qualcuno ti ha aggiunto ad una lista. The activity is related to one of your objects, such as someone liking one of your posts. L'attività è legata a uno dei tuoi oggetti, per esempio a qualcuno piace un tuo post. Used also when highlighting your own posts in the timelines. Usato anche per evidenziare i tuoi post nelle timeline. Item highlighted due to filtering rules. Oggetto evidenziato in base alle regole dei filtri. Item is new. Nuovo Elemento. Show snippets in minor feeds Mostra snippets nei feed minori Hide duplicated posts Nascondi post duplicati No Before avatar Before avatar, subtle After avatar After avatar, subtle Snippet limit when highlighted Show activity icons Avatar size Dimensione avatar Show extended share information Mostra informazioni di condivisione estese Show extra information Mostra informazioni extra Highlight post author's comments Evidenzia i commenti dell'autore del post Highlight your own comments Evidenza i tuoi commenti Show character counter Mostra contatore di caratteri Don't inform followers when following someone Don't inform followers when handling lists As system notifications Come notifiche di sistema Using own notifications Usando proprie notifiche Don't show notifications Non mostrare notifiche seconds Next to a duration, in seconds Notification Style Stile di notifica Duration Persistent Notifications Also highlight taskbar entry Notify when receiving: Notifica quando ricevi: New posts Nuovi post Highlighted posts Post in evidenza New activities in minor feed Nuove attività nel feed secondario Highlighted activities in minor feed Attività in evidenza nel feed secondario Important errors Default Default System iconset, if available Icone di sistema, se disponibili Show your current avatar Mostra il tuo avatar attuale Custom icon Icona personalizzata System Tray Icon &Type &Tipo di Icona di Sistema Hide window on startup General Options Opzioni Generali Fonts Font Colors Colori Timelines Timeline Posts Post Composer Componi Privacy Notifications Notifiche System Tray Icone di Sistema Minor feed avatar sizes Avatar size in comments S&elect... S&eleziona... Custom &Icon &Icona Personalizzata Select custom icon Seleziona un'icona personalizzata Dianara stores data in this folder: Dianara salva i dati in questa cartella: Left side tabs on left side/west; RTL not affected A sinistra Right side tabs on right side/east; RTL not affected A destra Show information for deleted posts Jump to new posts line on update Only for images inserted from web sites. Use with care. Show full size images Use attachment filename as initial post title Usa il nome del file dell'allegato come titolo iniziale del post Inform only the author when liking things &Save Configuration &Salva Configurazione &Cancel &Cancella This is a system notification System notifications are not available! Own notifications will be used. This is a basic notification Image files File immagine All files Tutti i files Invalid image Immagine non valida The selected image is not valid. L'immagine selezionata non è valida. ContactCard Hometown Città Joined: %1 Membro dal: %1 Updated: %1 Aggiornato: %1 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 Send Message Invia Messaggio Browse Messages 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 Type a partial name or ID to find a contact... Scrivi parzialmente un nome o l'ID per trovare un contatto... F&ull List Lista Com&pleta ContactManager 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: &Follow &Segui Reload Followers Ricarica Followers Reload Following Ricarica Following Export Followers Esporta Followers Export Following Esporta Following Reload Lists Ricarica Liste &Neighbors Follo&wers Follo&wers Followin&g Foll&owing &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 Cannot export to this file: Please enter another file name, or choose a different folder. The server seems to be a Pump server, but the account does not exist. %1 doesn't seem to be a Pump server. %1 is a hostname Following this account at this time will probably not work. The %1 server seems unavailable. %1 is a hostname Unknown Refers to server version Error Errore The user address %1 does not exist, or the %2 server is down. Server version Check the address, and keep in mind that usernames are case-sensitive. Do you want to try following this address anyway? (not recommended) Yes, follow anyway No, cancel About to follow %1... DownloadWidget Open Verb, as in: Open the downloaded file Download Scarica Save the attached file to your folders Salva il file allegato nelle tue cartelle Cancel Annulla Save File As... Salva File Come... All files Tutti i files File not found! Abort download? Fermare download? Do you want to stop downloading the attached file? Vuoi fermare il download del file allegato? &Yes, stop &Si, ferma &No, continue &No, continua Download aborted Download fermato Download completed Download completato Attachment downloaded successfully to %1 %1 = filename Open the downloaded attachment with your system's default program for this type of file. Download failed Download fallito Downloading attachment failed: %1 %1 = filename Downloading %1 KiB... Scaricando %1 KiB... %1 KiB downloaded %1 KiB scaricati DraftsManager Draft Manager Load Save Manage drafts... &Delete selected draft &Close &Chiudi Untitled draft Delete draft? Are you sure you want to delete this draft? &Yes, delete it &No &No EmailChanger Change E-mail Address Change Cambia &Cancel &Cancella E-mail Address: Again: Your Password: E-mail addresses don't match! Password is empty! FDNotifications Show FilterEditor Filter Editor Modifica Filtri Application Applicazione %1 if %2 contains: %3 This explains a filter rule, like: Hide if Author ID contains JohnDoe %1 se %2 contiene: %3 Here you can set some rules for hiding or highlighting stuff. 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. You could also highlight messages that contain your name. Qui puoi impostare regole per nascondere o mettere in evidenza le attività. Puoi filtrare per contenuto, autore o applicazione. Per esempio, puoi filtrare messaggi postati dall'applicazione Open Farm Game, o quelli che contengono la parola NSFW nel messaggio. Puoi anche evidenziare i messaggi che contengono il tuo nome. Hide Nascondi Highlight Evidenzia Post Contents Contenuto del post Author ID ID dell'autore Activity Description Descrizione dell'attività 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 if se contains contiene &New Filter &Nuovo Filtro C&urrent Filters Filt&ri Salvati FilterMatchesWidget Content The contents of the post matched Author App Application, short if possible Description FirstRunWizard Welcome Wizard Welcome to Dianara! This wizard will help you get started. You can access this window again at any time from the Help menu. The first step is setting up your account, by using the following button: Configure your &account Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already. &Edit your profile By default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes. Post to &Public by default Open general program &help window &Show this again next time Dianara starts &Close &Chiudi FontPicker Change... Cambia... Choose a font HelpWidget Basic Help Aiuto di Base Getting started Per iniziare The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. La prima volta che fai partire Dianara, dovresti vedere la finestra di Configurazione dell'Account. Inserisci il tuo indirizzo Pump.io come nome@server e premi Ottieni Codice di Verifica. Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. Poi, il tuo browser predefinito dovrebbe caricare la pagina di autorizzazione nel tuo server Pump.io. Qui, dovrai copiare il VERIFIER code, e incollarlo nel secondo campo di Dianara. Quindi premi Autorizza Applicazione e, una volta confermato, premi Salva Dettagli. At this point, your profile, contact lists and timelines will be loaded. A questo punto il tuo profilo, le liste contatti e le timeline saranno caricate. You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. Dovresti dare uno sguardo alla finestra di Configurazione del Programma, sotto Impostazioni - Configura Dianara. Ci sono diverse opzioni interessanti là. Settings Impostazioni You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. Puoi configurare diverse cose secondo le tue preferenze, come il tempo di aggiornamento delle timeline, quanti post per pagina vuoi vedere, colori di evidenziazione, notifiche o come visualizzare l'icona di sistema. Timelines Timeline Contents Contenuti Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. Tieni presente che ci sono molti posti in Dianara dove puoi ottenere più informazioni fermandoti sopra alcuni testi o bottoni col mouse, e aspettare che il suggerimento appaia. Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. Qui puoi anche attivare l'opzione di pubblicare sempre i tuoi post pubblici per default. Puoi sempre cambiare questa preferenza al momento di postare. The main timeline, where you'll see all the stuff posted or shared by the people you follow. La timeline principale, dove puoi vedere tutti gli elementi postati o condivisi da persone che segui. Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. La timeline dei messaggi, dove vedrai messaggi inviati a te specificatamente. Questi messaggi potrebbero essere stati inviati anche ad altre persone. Activity timeline, where you'll see your own posts, or posts shared by you. La timeli delle attività, dove vedrai i tuoi post e i post che hai condiviso. Favorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. La timeline dei preferiti, dove vedrai i post e i commenti che ti piacciono. Può essere usata come sistema di bookmark. These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. Queste attività potrebbero avere un '+' vicino. Premilo per aprire il post al quale sono riferite. Anche qui potrai avere ulteriori informazioni se rimani fermo col mouse sul pulsante. Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. Dianara offers a D-Bus interface that allows some control from other applications. The interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. Posting Postare If you're new to Pump.io, take a look at this guide: Se sei nuovo su Pump.io, dai uno sguardo a questa guida: New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. Nuovi messaggi appaiono evidenziati in un colore diffeerente. Puoi segnarli come già letti cliccando in una parte vuota del messaggio. You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. Puoi postare note cliccando il campo di testo in alto oppure premendo Control+N. Impostare un titolo per il post è opzionale, ma caldamente consigliato, perchè aiuta gli altri a identificarlo nella timeline laterale, nelle notifiche mail, ecc. It is possible to attach images, audio, video, and general files, like PDF documents, to your post. Allegare immagini, video o audio ed altri file come PDF è ora possibile. You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. Puoi usare il pulsante Formattazione per formattare il tuo testo, per esempio renderlo grassetto o corsivo. Alcune di queste opzioni richiedono che del testo sia selezionato prima che siano usate. If you add a specific person to the 'To' list, they will receive your message in their direct messages tab. Se aggiungi specifiche persone alla lista A, riceveranno una notifica del tuo messaggio nella tab dei messaggi diretti. You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. Puoi creare messaggi privati aggiungendo specifiche persone a queste liste e rimuovendo Followers e Pubblico. Managing contacts Gestire i contatti You can see the lists of people you follow, and who follow you from the Contacts tab. Puoi vedere le liste di persone che segui e che ti seguono dalla tab Contatti. There, you can also manage person lists, used mainly to send posts to specific groups of people. Qui puoi anche gestire liste di persone, usate principalmente per inviare post a specifici gruppi. You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. Puoi cliccare su qualsiasi avatar nei post, nei commenti, e nella timeline laterale e comparirà un menù con diverse opzioni, una di queste serve per seguire o smettere di seguire quella persona. Keyboard controls Controlli da Tastiera Pump.io User Guide Guida Utente Pump.io There are seven timelines: Ci sono sette timeline: The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). La sesta e la settima timeline sono timeline minori o secondarie, simili a "Nel Frattempo", ma contiene solo attività indirizzate direttamente a te (Menzioni) e attività create da te (Azioni). You can select who will see your post by using the To and Cc buttons. Puoi selezionare chi vedrà il tuo post usando i pulsanti A e Cc. You can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. Puoi anche digitare '@' e il primo carattere del nome di un contatto per far apparire un popup con le scelte corrispondenti. You can also send a direct message (initially private) to that contact from this menu. Puoi anche inviare un messaggio diretto (inizialmente privato) al contatto da questo menu. You can find a list with some Pump.io users and other information here: Puoi trovare una lista con alcuni utenti Pump.io ed altre informazioni qui: Users by language Followers of Pump.io Community account The most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. Le azioni più comuni che si trovano nei menù hanno scorciatorie da tastiera scritte vicino a loro, come F5 o Control+N. Besides that, you can use: Oltre a quello, puoi usare: Control+Up/Down/PgUp/PgDown/Home/End to move around the timeline. Control+Su/Giù/PgSu/PgGiù/Home/Fine per muoverti nelle timeline. Control+Left/Right to jump one page in the timeline. Control+Sinistra/Destra per saltare da una pagina all'altra nella timeline. Control+G to go to any page in the timeline directly. Control+G va in qualsiasi pagina della timeline direttamente. Control+1/2/3 to switch between the minor feeds. Control+1/2/3 per spostarti tra i feed minori. Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. Control+Invio per inviare il post quando hai terminato di comporre la nota o il commento. Se la nota è vuota, puoi cancellarla premendo ESC. Command line options Opzioni da riga di comando The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. LEFT SIDE should change to RIGHT SIDE on RTL languages La quinta timeline è la timeline minore, conosciuta anche come Meanwhile (nel frattempo). Questa è visibile a sinistra, ma può anche essere nascosta. Qui vedrai attività di tutte le persone che segui, come commenti, mi piace e segui persone. Choose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. Scegline uno con i tasti freccia e premi Invio per completare un nome. Questo aggiungerà quella persona alla lista di destinatari. There is a text field at the top, where you can directly enter addresses of new contacts to follow them. While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. Mentre componi una nota, premi Invio per saltare dal titolo al messaggio. Premendo la freccia Su mentre sei all'inizio del messaggio, torni al titolo. Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. Control+Invio per terminare la creazione della lista di destinatari per il post, per le liste 'A' o 'Cc'. You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. Puoi usare il parametro --config per avviare il programma con una configurazione differente. Questo può essere utile per usare due o più account. Puoi anche avviare due istanze di Dianara contemporaneamente. Use the --debug parameter to have extra information in your terminal window, about what the program is doing. Usa il parametro --debug per avere informazioni extra nel tuo terminale, su quello che il programma sta facendo. If your server does not support HTTPS, you can use the --nohttps parameter. If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. &Close &Chiudi ImageViewer Image Immagine Untitled &Save As... &Salva come... &Restart Restart animation &Replay Fit As in: fit image to window Rotate image to the left RTL: This actually means LEFT, anticlockwise Rotate image to the right RTL: This actually means RIGHT, clockwise &Close &Chiudi Save Image... Salvataggio Immagine... Close Viewer Chiudi Visualizzatore Downloading full image... Error downloading image! Try again later. 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 Type a name for the new list... Digita un nome per la nuova lista... Type an optional description here Digita una descrizione opzionale qui WARNING: Delete list? ATTENZIONE: Cancellare la lista? &Yes, delete it &Si, cancellala &No &No LogViewer Log Log Clear &Log Pulisci &Log &Close &Chiudi MainWindow Side &Panel Pannello &laterale Status &Bar &Barra di stato Total posts: %1 Post totali: %1 &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 Initializing... Inizializzando... Your account is not configured yet. Il tuo account non è ancora stato configurato. Dianara started. Dianara è partito. The people you follow, the ones who follow you, and your person lists Le persone che segui, quelle che ti seguono, e le tue liste di persone Running with Qt v%1. In funzione con Qt v%1. &Session &Sessione Auto-update &Timelines Auto-aggiorna &Timeline Mark All as Read Segna tutti come letti &Post a Note &Pubblica una nota &Quit &E&sci &View &Visualizzazione Locked Panels and Toolbars Side Panel Pannello laterale Full &Screen Sc&hermo Intero &Log &Log S&ettings Configura&zione Edit &Profile Modifica &profilo &Account A&ccount Basic &Help Ai&uto di Base Report a &Bug Riporta un &Bug Pump.io User &Guide &Guida Utente Pump.io List of Some Pump.io &Users Elenco di alcuni &Utenti Pump.io Pump.io &Network Status Website Auto-updating enabled Auto-aggiornamenti abilitati Auto-updating disabled Autp-aggiornamenti disabilitati Proxy password required Password Proxy Richiesta You have configured a proxy server with authentication, but the password is not set. Hai configurato un server proxy con autenticazione, ma la password non è impostata. Enter the password for your proxy server: Inserisci la password del tuo server proxy: Your biography is empty La tua biografia è vuota Click to edit your profile Clicca per modificare il tuo profilo Starting automatic update of timelines, once every %1 minutes. Attiva l'aggiornamento automatico delle timeline, ogni %1 minuti. Stopping automatic update of timelines. Ferma l'aggiornamento automatico delle timeline. Received %1 older posts in '%2'. %1 is a number, %2 = name of a timeline 1 highlighted singular, refers to a post 1 evidenziato %1 highlighted plural, refers to posts %1 evidenziati Direct messages By filters 1 more pending to receive. singular, one post 1 in attesa di ricezione. %1 more pending to receive. plural, several posts %1 in attesa di ricezione. Also: Last update: %1 Ultimo aggiornamento: %1 '%1' updated. %1 is the name of a feed '%1' aggiornato. Received %1 older activities in '%2'. %1 is a number, %2 = name of feed 1 more pending to receive. singular, 1 activity 1 in attesa di ricezione. %1 more pending to receive. plural, several activities %1 in attesa di ricezione. Dianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. Shutting down Dianara... System tray icon is not available. L'icona di sistema non è disponibile. Dianara cannot be hidden in the system tray. Dianara non può essere nascosto nella barra delle notifiche di sistema. Do you want to close the program completely? Vuoi chiudere completamente il programma? Timeline updated at %1. Timeline aggiornata alle %1. Press F1 for help Premi F1 per aiuto Click here to configure your account Update %1 Aggiorna %1 No new posts. Nessun nuovo post. Your Pump.io account is not configured Il tuo account Pump.io non è configurato There is 1 new activity. C'è una nuova attività. There are %1 new activities. Ci sono %1 nuove attività. 1 highlighted. singular, refers to an activity 1 evidenziato. %1 highlighted. plural, refers to activities %1 evidenziati. 1 filtered out. singular, refers to one activity 1 filtrato. %1 filtered out. plural, several activities %1 filtrati. No new activities. Nessuna nuova attività. Error storing image! %1 bytes Link to: %1 Link a: %1 Marking everything as read... Closing due to environment shutting down... Quit? Chiudere? You are composing a note or a comment. Stai componendo una nota o un commento. Do you really want to close Dianara? Vuoi veramente chiudere Dianara? &Yes, close the program &Si, chiudi il programma &No &No &Configure Dianara Configura &Dianara Minor activities done by everyone, such as replying to posts Attività minori fatte da chiunque, come rispondere ai post Minor activities addressed to you Attività minori indirizzate a te Minor activities done by you Attività minori create da te &Toolbar Barra degli Strumen&ti &Filters and Highlighting &Filtri e Evidenziazione &Help Ai&uto Show Welcome Wizard Visit &Website Visita il sito &web Some Pump.io &Tips Alcuni consigli per &Pump.io About &Dianara Informazioni su &Dianara Toolbar Barra degli Strumenti Open the log viewer Apri il visualizzatore del log 1 filtered out. singular, refers to a post 1 filtrato. %1 filtered out. plural, refers to posts %1 filtrati. 1 deleted. singular, refers to a post 1 cancellato. %1 deleted. plural, refers to posts %1 cancellati. Favor&ites Prefer&iti Minor feed updated at %1. Timeline laterale aggiornata %1. With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. Con Dianara puoi vedere le tue timeline, creare nuovi post, caricare immagini e altri media, interagire con post, organizzare i tuoi contatti e seguire nuove persone. English translation by JanKusanagi. TRANSLATORS: Change this with your language and name. If there was another translator before you, add your name after theirs ;) Traduzione italiana a cura di Howcanuhavemyusername (or Metal Biker) (howcanuhavemyusername@microca.st). &Hide Window &Nascondi Finestra &Show Window &Mostra la finestra There is 1 new post. C'è 1 nuovo messaggio. There are %1 new posts. Ci sono %1 nuovi messaggi. About Dianara Informazioni su Dianara Dianara is a pump.io social networking client. Dianara è un client per il social network pump.io. 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! MinorFeed Older Activities Attività più vecchie Get previous minor activities Ottieni attività precedenti There are no activities to show yet. Non ci sono ancora attività da mostrare. Get %1 newer As in: Get 3 newer (activities) Scarica %1 nuove MinorFeedItem Using %1 Application used to generate this activity Via %1 To: %1 1=people to whom this activity was sent A: %1 Cc: %1 1=people to whom this activity was sent as CC Cc: %1 Open referenced post Apri l'elemento relativo MiscHelpers bytes bytes Error: Unable to launch browser The default system web browser could not be executed. You might need to install the XDG utilities. PageSelector Jump to page Salta alla pagina Page number: Pagina numero: &First As in: first page &Last As in: last page Newer As in: newer pages Più recenti Older As in: older pages Più vecchi &Go &Vai &Cancel &Cancella 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 Like Mi piace Like this post Dì che ti piace questo messaggio Click to download the attachment Clicca pe rscaricare l'allegato Post Noun, not verb Pubblica Using %1 1=Program used for posting or sharing Via %1 &Close &Chiudi Type As in: type of object Tipo Modified on %1 Modificato il %1 Parent As in 'Open the parent post'. Try to use the shortest word! Padre Open the parent post, to which this one replies Apri il post padre, quello al quale questo post risponde Modify this post Modifica questo post Join Group Unisciti al Gruppo %1 members in the group %1 membri nel gruppo Image is animated. Click on it to play. L'immagine è animata. Clicca per riprodurre. Loading image... Caricando l'immagine... 1 like 1 mi piace 1 comment 1 commento Shared %1 times Condiviso %1 volte Shared on %1 Condiviso su %1 Edited: %1 Modificato: %1 In In Share Condividi Edit Modifica %1 likes %1 mi piace %1 comments %1 commenti Delete Elimina Via %1 Via %1 Posted on %1 1=Date Pubblicato il %1 To A 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 Open post in web browser Apri il messaggio nel browser web Cc Cc Copy post link to clipboard Copia il link al messaggio negli appunti Normalize text colors Normalizza i colori del testo Comment verb, for the comment button Commenta Reply to this post. Rispondi a questo post. Share this post with your contacts Condividi questo post con i tuoi contatti Erase this post Cancella questo post Size Image size (resolution) Dimensioni Couldn't load image! Attached Audio Audio allegato Attached Video Video allegato Attached File File allegato %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ù Are you sure you want to share your own post? 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? 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 &E-mail... Cambia &e-mail... Change &Avatar... Cambia &avatar... This is your visible name Questo è il tuo nome visibile Changing your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. &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. ProxyDialog Proxy Configuration Configurazione Proxy Do not use a proxy Non usare un proxy Your proxy username Il tuo username del proxy Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. Nota: la password non è salvata in modo sicuro. Se lo desideri, puoi lasciare questo campo vuoto e ti verrà chiesta la password all'avvio. &Save &Salva &Cancel &Annulla Proxy &Type &Tipo di Proxy &Hostname &Hostname &Port &Porta Use &Authentication Usa Autenti&cazione &User &Utente Pass&word Pass&word Publisher Title Titolo Add a brief title for the post here (recommended) (Jan) This one should be updated with the "here" Aggiungi un breve titolo per il post (raccomandato) Picture Immagine Audio Audio Video Video Ad&d... Aggiun&gi... Upload media, like pictures or videos Carica media, come immagini o video Select Picture... Selezionare l'immagine... Find the picture in your folders Trova l'immagine nelle tue cartelle Public Pubblico Drafts Followers Followers Lists Liste To... A... Select who will get a copy of this post Scegli chi riceverà una copia di questo messaggio Other as in other kinds of files Altro Cancel Cancella Cancel the post Cancella il messaggio Picture not set Immagine non selezionata Select Audio File... Seleziona File Audio... Find the audio file in your folders Cerca il file audio nelle tue cartelle Audio file not set Il file audio non è impostato Select Video... Seleziona Video... Find the video in your folders Cerca il file video nelle tue cartelle Video not set Video non impostato Select File... Seleziona File... Find the file in your folders Trova il file nelle tue cartelle File not set File non impostato 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 You can't create a message for %1 at this time, because a post is already being composed. Non puoi creare un messaggio per %1 al momento, perche un post è già in fase di composizione. Draft loaded. ERROR: Already composing You can't load a draft at this time, because a post is already being composed. Draft saved. Posting failed. Try again. Invio fallito. Prova di nuovo. Warning: You have no followers yet Avviso: non hai ancora followers You're trying to post to your followers only, but you don't have any followers yet. Stai cercando di postare solo per i tuoi followers, ma non ne hai ancora. If you post like this, no one will be able to see your message. Se posti con queste impostazioni, nessuno sarà in grado di leggere il tuo messaggio. Do you want to make the post public instead of followers-only? Vuoi rendere il post pubblico invece che renderlo visibile solo ai tuoi followers? &Yes, make it public &Si, rendilo pubblico &Cancel, go back to the post &Annulla e torna al post Updating... Aggiornando... Post is empty. Il messaggio è vuoto. File not selected. File non selezionato. Select one image Scegli un'immagine Image files Files immagine Select one file Seleziona un file Invalid file File non valido The file type cannot be detected. Il tipo di file non può essere identificato. All files Tutti i files Since you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. Visto che stai caricando un'immagine, potresti ridurre la sua risoluzione o salvarla in un formato più compresso, come JPG. File is too big File troppo grande Dianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. Dianara limita la dimensione dei file caricati a 10MiB per post, in modo da prevenire problemi di spazio e rete dei server. This is a temporary measure, since the servers cannot set their own limits yet. Questa è una misura temporanea, in quanto i server non possono ancora impostare i loro limiti. Sorry for the inconvenience. Ci scusiamo per il disagio. File not found. Error Errore The selected file cannot be accessed: It is owned by %1. %1 = a username You might not have the necessary permissions. Resolution Image resolution (size) Risoluzione Type Tipo Size Dimensioni %1 KiB of %2 KiB uploaded %1 KiB di %2 KiB caricati Invalid image Immagine non valida Setting a title helps make the Meanwhile feed more informative Impostare un titolo rende il feed laterale più informativo Remove Rimuovi Cancel the attachment, and go back to a regular note Rimuovi l'allegato e torna al post regolare Cc... Cc... Post verb Pubblica Note started from another application. Ignoring new note request from another application. Editing post. Modifica il messaggio. &No, post to my followers only &No, rendilo visibile solo ai miei follower 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. Select one audio file Seleziona un file audio Audio files Files audio Invalid audio file File audio non valido The audio format cannot be detected. Il formato del file audio non può essere rilevato. Select one video file Seleziona un file video Video files Files Video Invalid video file File video non valido The video format cannot be detected. Il formato del file video non può essere rilevato. Posting... Pubblicando... 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... Creating group... Creando un gruppo... Joining group... Unendosi al gruppo... Leaving group... Lasciando il gruppo... Getting likes... Ricevendo i "mi piace"... Getting comments... Ricevendo i commenti... Getting '%1'... %1 is the name of a feed Ottenendo '%1'... Timeline Timeline Messages Messaggi Uploading %1 1=filename Caricando %1 HTTP error For the following HTTP error codesyou can check http://en.wikipedia.org/wiki/List_of_HTTP_status_codes in your language Errore HTTP Gateway Timeout HTTP 504 error string Gateway Timeout Service Unavailable HTTP 503 error string Servizio non Disponibile Not Implemented HTTP 501 error string Non implementato Internal Server Error HTTP 500 error string Errore Interno del Server Gone HTTP 410 error string Sparito Not Found HTTP 404 error string Non Trovato Forbidden HTTP 403 error string Proibito Unauthorized HTTP 401 error string Non Autorizzato Bad Request HTTP 400 error string Richiesta Errata Moved Temporarily HTTP 302 error string Spostato Temporaneamente Moved Permanently HTTP 301 error string Spostato Permanentemente 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. Followers Followers Following Following Profile updated. Profilo aggiornato. Avatar published successfully. Avatar pubblicato con successo. 1 comment received. 1 commento ricevuto. %1 comments received. %1 commenti ricevuti. Post by %1 shared successfully. 1=author of the post we are sharing Post di %1 condiviso con successo. Received '%1'. %1 is the name of a feed Ricevuto '%1'. Adding items... Aggiungendo oggetti... SSL errors in connection to %1! Errori SSL durante la connessione a %1! OAuth error while authorizing application. Errore OAuth in fase di autorizzazione dell'appliazione. Activity Attività Favorites Preferiti Meanwhile Nel frattempo Mentions Menzioni Actions Azioni 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 deleted successfully. Lista di persone eliminata correttamente. Person list received. Lista di persone ricevuta. File uploaded successfully. Posting message... File caricato con successo. Pubblicando il messaggio... Authorized to use account %1. Getting initial data. Autorizzato ad utilizzare l'account %1. Ricevendo i dati iniziali. There is no authorized account. Non ci sono account autorizzati. Updating profile... Getting list of 'Following'... Ricevendo la lista dei 'Following'... Getting list of 'Followers'... Ricevendo la lista dei 'Followers'... Getting site users for %1... %1 is a server name Getting list of person lists... Rocevendo le liste delle persone... The comments for this post cannot be loaded due to missing data on the server. User timeline Error loading timeline! Error loading minor feed! Unable to verify the address! Bad Gateway HTTP 502 error string Server version: %1 E-mail updated: %1 %1 published successfully. Updating post content... %1 is the type of object: note, image... Untitled post %1 published successfully. %1 is a piece of the post (Jan) I tried my luck adjusting this one Messaggio senza titolo %1 pubblicato con successo. Post %1 published successfully. %1 is the title of the post (Jan) I tried my luck adjusting this one Messaggio %1 pubblicato con successo. Untitled post %1 updated successfully. %1 is a piece of the post (Jan) I tried my luck adjusting this one Messaggio senza titolo %1 aggiornato con successo. Post %1 updated successfully. %1 is the title of the post (Jan) I tried my luck adjusting this one Messaggio %1 aggiornato con successo. Comment %1 updated successfully. %1 is a piece of the comment (Jan) I tried my luck adjusting this one Commento %1 aggiornato con successo. Message liked or unliked successfully. Messaggio aggiunto o rimosso dai "Mi piace" correttamente. Likes received. "Mi piace" ricevuto. Comment %1 posted successfully. %1 is a piece of the comment (Jan) I tried my luck adjusting this one Commento %1 pubblicato con successo. Message deleted successfully. Messaggio cancellato correttamente. Following %1 (%2) successfully. %1 is a person's name, %2 is the ID Segui %1 (%2) con successo. Stopped following %1 (%2) successfully. %1 is a person's name, %2 is the ID Hai smesso di seguire %1 (%2) con successo. List of 'lists' received. Lista delle 'Liste' ricevuta. List of %1 users received. %1 is a server name Person list '%1' created successfully. Lista di persone '%1' creata con successo. %1 (%2) added to list successfully. 1=contact name, 2=contact ID %1 (%2) aggiunto alla lista con successo. %1 (%2) removed from list successfully. 1=contact name, 2=contact ID %1 (%2) rimosso dalla lista con successo. Group %1 created successfully. Gruppo %1 creato con successo. Group %1 joined successfully. Unito al gruppo %1 con successo. Left the %1 group successfully. Lasciato il gruppo %1 con successo. Avatar uploaded. Avatar caricato. Loading external image from %1 regardless of SSL errors, as configured... %1 is a hostname The application is not registered with your server yet. Registering... L'applicazione non è ancora autorizzata con il tuo server. Registrazione... Getting OAuth token... Ricevendo il token OAuth... OAuth support error Errore supporto OAuth Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support. La tua installazione di QOAuth, libreria usata da Dianara, sembra che non abbia il supporto HMAC-SHA1. You probably need to install the OpenSSL plugin for QCA: %1, %2 or similar. Probabilmente hai bisogno di installare il plugin OpenSSL per QCA: %1, %2 o simili. Authorization error Errore di Autorizzazione There was an OAuth error while trying to get the authorization token. C'è stato un errore OAuth nel tentativo di ottenere il token di autorizzazione. QOAuth error %1 Errore QOAuth %1 Application authorized successfully. Applicazione autorizzata con successo. Waiting for proxy password... In attesa della password del proxy... Still waiting for profile. Trying again... Aspettando ancora il tuo profilo. Provo ancora... %1 attempts 1 attempt Some initial data was not received. Restarting initialization... Alcuni dati iniziali non sono stati ricevuti. Riavvio inizializzazione... Can't follow %1 at this time. %1 is a user ID Trying to follow %1. %1 is a user ID Checking address %1 before following... Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally. Alcuni dati iniziali non sono stati ricevuti anche dopo diversi tentativi. Qualcosa potrebbe non funzionare sul tuo server. Potresti essere comunque in grado di usare il servizio normalmente. All initial data received. Initialization complete. Tutti i dati iniziali ricevuti. Inizializzazione completa. Ready. Pronto. SiteUsersList You can get a list of the newest users registered on your server by clicking the button below. More resources to find users: Wiki page 'Users by language' PPump user search service at inventati.org List of Followers for the Pump.io Community account Get list of users from your server Close list Loading... %1 users in %2 %1 = user count, %2 = server name TimeLine Welcome to Dianara Benvenuto in Dianara Dianara is a <b>Pump.io</b> client. Dianara è un client <b>Pump.io</b>. Press <b>F1</b> if you want to open the Help window. Premi <b>F1</b> se vuoi aprire la finestra di Aiuto. 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>. Dianara's blog Blog di Dianara Newest Ultimi Newer Più recenti Older Più vecchi Requesting... Loading... Page %1 of %2. Pagina %1 di %2. Showing %1 posts per page. Mostra %1 posts per pagina. %1 posts in total. %1 post in totale. Click here or press Control+G to jump to a specific page Clicca qui o premi Control+G per saltare ad una pagina specifica '%1' cannot be updated because a comment is currently being composed. %1 = feed's name '%1' non può essere aggiornato perchè un commento è in fase di composizione. %1 more posts pending for next update. Click here to receive them now. There are no posts If you don't have a Pump account yet, you can get one at the following address, for instance: se non hai ancora un account Pump, puoi averne uno al seguente indirizzo, per esempio: There are tooltips everywhere, so if you hover over a button or a text field with your mouse, you'll probably see some extra information. Ci sono tooltip ovunque, quindi se ti fermi col mouse sopra ad un bottone o ad un testo, probabilmente vedrai informazioni extra. Pump.io User Guide Guida Utente 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. Timestamp Invalid timestamp! Timestamp non Valida! A minute ago Un minuto fa %1 minutes ago %1 minuti fa An hour ago Un'ora fa %1 hours ago %1 ore fa Just now In questo istante In the future Prossimamente Yesterday Ieri %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 UserPosts Posts by %1 Loading... &Close &Chiudi Received '%1'. Ricevuto '%1'. %1 posts Error loading the timeline dianara-v1.4.1/translations/dianara_eu.ts0000644000175000017500000062621213212033716016574 0ustar janjan ASActivity Public %1 by %2 1=kind of object: note, comment, etc; 2=author's name ASObject Note Noun, an object type Article Noun, an object type Image Noun, an object type Audio Noun, an object type Video Noun, an object type File Noun, an object type Comment Noun, as in object type: a comment Group Noun, an object type Collection Noun, an object type Other As in: other type of post No detailed location Deleted on %1 and one other and %1 others ASPerson Hometown AccountDialog Your Pump.io address: Get &Verifier Code Verifier code: Enter or paste the verifier code provided by your Pump server here &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 If you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. 1=link to website If you need help: %1 Pump.io User Guide Your address, like username@pumpserver.org 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! &Authorize Application &Cancel Your account is properly configured. Press Unlock if you wish to configure a different account. &Unlock 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 Unable to open web browser! 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. ON THE LEFT should change to ON THE RIGHT in RTL languages Clear &List &Done &Cancel Public Followers Lists People... Selected People AvatarButton Open %1's profile in web browser Open your profile in web browser Send message to %1 Browse messages Stop following Follow Stop following? Are you sure you want to stop following %1? &Yes, stop following &No BannerNotification Timelines were not automatically updated to avoid interruptions. This happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you read Update now Hide this message ColorPicker Change... Choose a color Comment Posted on %1 Modified on %1 Like or unlike this comment Quote This is a verb, infinitive Reply quoting this comment Edit Modify this comment Delete Erase 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 You can press Control+Enter to send the comment with the keyboard Reload comments Comment Infinitive verb Cancel Press ESC to cancel the comment if there is no text Check for comments Show all %1 comments Comments are not available Error: Already composing You can't edit a comment at this time, because another comment is already being composed. Editing comment Loading comments... Posting comment failed. Try again. An error occurred Sending comment... Updating 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 List Table 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 You can attach only one file. You cannot drop folders here, only a single file. Insert as image? The link you are pasting seems to point to an image. Insert as visible image Insert as link Table Size How many rows (height)? How many columns (width)? Insert a link Type or paste a web address here. You could also select some text first, to turn it into a link. Make a link from selected text Type or paste a web address here. The selected text (%1) will be converted to a link. Invalid link The text you entered does not look like a link. It should start with one of these types: It = the link, from previous sentence &Use it anyway &Enter it again &Cancel link Insert an image from a URL Type or paste the image address here. The link must point to the image file directly. Error: Invalid URL The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// Yes, but saving a &draft Cancel message? Are you sure you want to cancel this message? &Yes, cancel it &No ConfigDialog minutes Top Bottom 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 Pro&xy Settings Network configuration Set Up F&ilters Filtering rules Highlighted activities, except mine Any highlighted activity Always Never Comments Left side tabs on left side/west; RTL not affected Right side tabs on right side/east; RTL not affected You are among the recipients of the activity, such as a comment addressed to you. Used also when highlighting posts addressed to you in the timelines. The activity is in reply to something done by you, such as a comment posted in reply to one of your notes. You are the object of the activity, such as someone adding you to a list. The activity is related to one of your objects, such as someone liking one of your posts. Used also when highlighting your own posts in the timelines. Item highlighted due to filtering rules. Item is new. Show snippets in minor feeds Show information for deleted posts No Before avatar Before avatar, subtle After avatar After avatar, subtle Hide duplicated posts Jump to new posts line on update Snippet limit when highlighted Minor feed avatar sizes Show activity icons Avatar size Avatar size in comments Show extended share information Show extra information Highlight post author's comments Highlight your own comments Ignore SSL errors in images Show full size images Show character counter Don't inform followers when following someone Don't inform followers when handling lists As system notifications Using own notifications Don't show notifications seconds Next to a duration, in seconds Notification Style Duration Persistent Notifications Also highlight taskbar entry Notify when receiving: New posts Highlighted posts New activities in minor feed Highlighted activities in minor feed Important errors Default System iconset, if available Show your current avatar Custom icon System Tray Icon &Type S&elect... Custom &Icon Hide window on startup Timelines Posts Composer Privacy Notifications System Tray This is a system notification System notifications are not available! Own notifications will be used. This is a basic notification Select custom icon Post Titles characters This is a suffix, after a number Snippet limit Post Contents Minor Feeds Only for images inserted from web sites. Use with care. Use attachment filename as initial post title Inform only the author when liking things General Options Fonts Colors Dianara stores data in this folder: &Save Configuration &Cancel Image files All files Invalid image The selected image is not valid. ContactCard Hometown Joined: %1 Updated: %1 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 Send Message Browse Messages User Options Follow Stop Following Stop following? Are you sure you want to stop following %1? &Yes, stop following &No ContactList Type a partial name or ID to find a contact... F&ull List ContactManager username@server.org or https://server.org/username &Enter address to follow: &Follow Reload Followers Reload Following Export Followers Export Following Reload Lists &Neighbors Follo&wers Followin&g &Lists Export list of 'following' to a file Export list of 'followers' to a file Cannot export to this file: Please enter another file name, or choose a different folder. The server seems to be a Pump server, but the account does not exist. %1 doesn't seem to be a Pump server. %1 is a hostname Following this account at this time will probably not work. The %1 server seems unavailable. %1 is a hostname Unknown Refers to server version Error The user address %1 does not exist, or the %2 server is down. Server version Check the address, and keep in mind that usernames are case-sensitive. Do you want to try following this address anyway? (not recommended) Yes, follow anyway No, cancel About to follow %1... DownloadWidget Open Verb, as in: Open the downloaded file Download Save the attached file to your folders Cancel Save File As... All files File not found! Abort download? Do you want to stop downloading the attached file? &Yes, stop &No, continue Download aborted Download completed Attachment downloaded successfully to %1 %1 = filename Open the downloaded attachment with your system's default program for this type of file. Download failed Downloading attachment failed: %1 %1 = filename Downloading %1 KiB... %1 KiB downloaded DraftsManager Draft Manager Load Save Manage drafts... &Delete selected draft &Close Untitled draft Delete draft? Are you sure you want to delete this draft? &Yes, delete it &No EmailChanger Change E-mail Address Change &Cancel E-mail Address: Again: Your Password: E-mail addresses don't match! Password is empty! FDNotifications Show FilterEditor Filter Editor %1 if %2 contains: %3 This explains a filter rule, like: Hide if Author ID contains JohnDoe Here you can set some rules for hiding or highlighting stuff. 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. You could also highlight messages that contain your name. Hide Highlight Post Contents Author ID Application Activity Description Keywords... &Add Filter Filters in use &Remove Selected Filter &Save Filters &Cancel if contains &New Filter C&urrent Filters FilterMatchesWidget Content The contents of the post matched Author App Application, short if possible Description FirstRunWizard Welcome Wizard Welcome to Dianara! This wizard will help you get started. You can access this window again at any time from the Help menu. The first step is setting up your account, by using the following button: Configure your &account Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already. &Edit your profile By default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes. Post to &Public by default Open general program &help window &Show this again next time Dianara starts &Close FontPicker Change... Choose a font HelpWidget Basic Help Getting started The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. At this point, your profile, contact lists and timelines will be loaded. You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. Settings You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. Timelines Contents Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. If you're new to Pump.io, take a look at this guide: Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. The main timeline, where you'll see all the stuff posted or shared by the people you follow. Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. Activity timeline, where you'll see your own posts, or posts shared by you. Favorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. Posting New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. It is possible to attach images, audio, video, and general files, like PDF documents, to your post. You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. If you add a specific person to the 'To' list, they will receive your message in their direct messages tab. Choose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. Managing contacts You can see the lists of people you follow, and who follow you from the Contacts tab. There, you can also manage person lists, used mainly to send posts to specific groups of people. You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. Keyboard controls Pump.io User Guide There are seven timelines: The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. LEFT SIDE should change to RIGHT SIDE on RTL languages The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). You can select who will see your post by using the To and Cc buttons. You can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. There is a text field at the top, where you can directly enter addresses of new contacts to follow them. You can also send a direct message (initially private) to that contact from this menu. You can find a list with some Pump.io users and other information here: Users by language Followers of Pump.io Community account The most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. Besides that, you can use: Control+Up/Down/PgUp/PgDown/Home/End to move around the timeline. Control+Left/Right to jump one page in the timeline. Control+G to go to any page in the timeline directly. Control+1/2/3 to switch between the minor feeds. Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. Dianara offers a D-Bus interface that allows some control from other applications. The interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. Command line options Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. Use the --debug parameter to have extra information in your terminal window, about what the program is doing. If your server does not support HTTPS, you can use the --nohttps parameter. If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. &Close ImageViewer Untitled Image &Save As... &Restart Restart animation Fit As in: fit image to window Rotate image to the left RTL: This actually means LEFT, anticlockwise Rotate image to the right RTL: This actually means RIGHT, clockwise &Close Save Image... Close Viewer Downloading full image... Error downloading image! Try again later. 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 LogViewer Log Clear &Log &Close MainWindow Status &Bar &Timeline The main timeline &Messages Messages sent explicitly to you &Activity Your own posts Your favorited posts &Contacts Initializing... Your account is not configured yet. Dianara started. Minor activities done by everyone, such as replying to posts Minor activities addressed to you Minor activities done by you The people you follow, the ones who follow you, and your person lists Press F1 for help Running with Qt v%1. Click here to configure your account &Session Auto-update &Timelines Mark All as Read &Post a Note &Quit &View Locked Panels and Toolbars Side Panel &Toolbar Full &Screen &Log S&ettings Edit &Profile &Account Basic &Help Report a &Bug Pump.io User &Guide Some Pump.io &Tips List of Some Pump.io &Users Pump.io &Network Status Website Toolbar Open the log viewer Auto-updating enabled Auto-updating disabled Proxy password required You have configured a proxy server with authentication, but the password is not set. Enter the password for your proxy server: Starting automatic update of timelines, once every %1 minutes. Stopping automatic update of timelines. Received %1 older posts in '%2'. %1 is a number, %2 = name of a timeline 1 highlighted singular, refers to a post %1 highlighted plural, refers to posts Direct messages By filters 1 more pending to receive. singular, one post %1 more pending to receive. plural, several posts Also: Last update: %1 '%1' updated. %1 is the name of a feed Show Welcome Wizard 1 filtered out. singular, refers to a post %1 filtered out. plural, refers to posts 1 deleted. singular, refers to a post %1 deleted. plural, refers to posts There is 1 new activity. There are %1 new activities. 1 highlighted. singular, refers to an activity %1 highlighted. plural, refers to activities 1 filtered out. singular, refers to one activity %1 filtered out. plural, several activities No new activities. Error storing image! %1 bytes Marking everything as read... Dianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. Closing due to environment shutting down... Quit? You are composing a note or a comment. Do you really want to close Dianara? &Yes, close the program &No Shutting down Dianara... System tray icon is not available. Dianara cannot be hidden in the system tray. Do you want to close the program completely? Timeline updated at %1. Update %1 Your Pump.io account is not configured Link to: %1 With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. English translation by JanKusanagi. TRANSLATORS: Change this with your language and name. If there was another translator before you, add your name after theirs ;) &Configure Dianara &Filters and Highlighting &Help Visit &Website About &Dianara Your biography is empty Click to edit your profile No new posts. Total posts: %1 Favor&ites Received %1 older activities in '%2'. %1 is a number, %2 = name of feed Minor feed updated at %1. 1 more pending to receive. singular, 1 activity %1 more pending to receive. plural, several activities &Hide Window &Show Window There is 1 new post. There are %1 new posts. About Dianara Dianara is a pump.io social networking client. Thanks to all the testers, translators and packagers, who help make Dianara better! MinorFeed Older Activities Get previous minor activities There are no activities to show yet. Get %1 newer As in: Get 3 newer (activities) MinorFeedItem Using %1 Application used to generate this activity To: %1 1=people to whom this activity was sent Cc: %1 1=people to whom this activity was sent as CC Open referenced post MiscHelpers bytes Error: Unable to launch browser The default system web browser could not be executed. You might need to install the XDG utilities. PageSelector Jump to page Page number: &First As in: first page &Last As in: last page Newer As in: newer pages Older As in: older pages &Go &Cancel PeopleWidget &Search: Enter a name here to search for it Add a contact to a list &Cancel Post Via %1 Shared on %1 Loading image... Edited: %1 Posted on %1 1=Date In To Post Noun, not verb Using %1 1=Program used for posting or sharing If you select some text, it will be quoted. Share Unshare Unshare this post Edit Delete Open post in web browser Click to download the attachment Cc Copy post link to clipboard Normalize text colors &Close Type As in: type of object Modified on %1 Parent As in 'Open the parent post'. Try to use the shortest word! Open the parent post, to which this one replies Comment verb, for the comment button Reply to this post. Share this post with your contacts Modify this post Erase this post Join Group %1 members in the group Image is animated. Click on it to play. Size Image size (resolution) Couldn't load image! Attached Audio Attached Video Attached File %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 Are you sure you want to share your own post? 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 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 &E-mail... Change &Avatar... This is your visible name Changing your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. &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. ProxyDialog Proxy Configuration Do not use a proxy Your proxy username Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. &Save &Cancel Proxy &Type &Hostname &Port Use &Authentication &User Pass&word Publisher Select Picture... Find the picture in your folders Select who will see this post To... Setting a title helps make the Meanwhile feed more informative Title Remove Cancel the attachment, and go back to a regular note Drafts Select who will get a copy of this post Picture Audio Video Other as in other kinds of files Ad&d... Upload media, like pictures or videos Hit Control+Enter to post with the keyboard Cancel Cancel the post Picture not set Select Audio File... Find the audio file in your folders Audio file not set Select Video... Find the video in your folders Video not set Select File... Find the file in your folders File not set File not found. Error: Already composing You can't edit a post at this time, because a post is already being composed. Update You can't create a message for %1 at this time, because a post is already being composed. Draft loaded. ERROR: Already composing You can't load a draft at this time, because a post is already being composed. Draft saved. Posting failed. Try again. Warning: You have no followers yet You're trying to post to your followers only, but you don't have any followers yet. If you post like this, no one will be able to see your message. Do you want to make the post public instead of followers-only? &Yes, make it public &Cancel, go back to the post Updating... Post is empty. File not selected. Select one image Image files Select one file Invalid file The file type cannot be detected. All files Since you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. File is too big Dianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. This is a temporary measure, since the servers cannot set their own limits yet. Sorry for the inconvenience. Error The selected file cannot be accessed: It is owned by %1. %1 = a username You might not have the necessary permissions. Resolution Image resolution (size) Type Size %1 KiB of %2 KiB uploaded Invalid image Add a brief title for the post here (recommended) Cc... Post verb Note started from another application. Ignoring new note request from another application. Editing post. &No, post to my followers only The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Select one audio file Audio files Invalid audio file The audio format cannot be detected. Select one video file Video files Invalid video file The video format cannot be detected. Posting... PumpController Creating person list... Deleting person list... Getting likes... Getting comments... Error connecting to %1 Unhandled HTTP error code %1 Message liked or unliked successfully. Comment %1 posted successfully. %1 is a piece of the comment Message deleted successfully. Likes received. Authorized to use account %1. Getting initial data. There is no authorized account. Updating profile... Getting list of 'Following'... Getting list of 'Followers'... Getting site users for %1... %1 is a server name Getting list of person lists... Getting a person list... Adding person to list... Removing person from list... Creating group... Joining group... Leaving group... Getting '%1'... %1 is the name of a feed Timeline Messages User timeline Uploading %1 1=filename Error loading timeline! Error loading minor feed! Unable to verify the address! HTTP error For the following HTTP error codesyou can check http://en.wikipedia.org/wiki/List_of_HTTP_status_codes in your language Gateway Timeout HTTP 504 error string Service Unavailable HTTP 503 error string Bad Gateway HTTP 502 error string Not Implemented HTTP 501 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 Server version: %1 Profile received. Followers Following Profile updated. E-mail updated: %1 %1 published successfully. Updating post content... %1 is the type of object: note, image... Untitled post %1 published successfully. %1 is a piece of the post Post %1 published successfully. %1 is the title of the post Avatar published successfully. Untitled post %1 updated successfully. %1 is a piece of the post Post %1 updated successfully. %1 is the title of the post Comment %1 updated successfully. %1 is a piece of the comment Adding items... Following %1 (%2) successfully. %1 is a person's name, %2 is the ID Stopped following %1 (%2) successfully. %1 is a person's name, %2 is the ID List of %1 users received. %1 is a server name Person list '%1' created successfully. %1 (%2) added to list successfully. 1=contact name, 2=contact ID %1 (%2) removed from list successfully. 1=contact name, 2=contact ID Group %1 created successfully. Group %1 joined successfully. Left the %1 group successfully. OAuth error while authorizing application. %1 attempts 1 attempt Some initial data was not received. Restarting initialization... Can't follow %1 at this time. %1 is a user ID Trying to follow %1. %1 is a user ID Checking address %1 before following... List of 'following' completely received. The comments for this post cannot be loaded due to missing data on the server. Activity Favorites Meanwhile Mentions Actions 1 comment received. %1 comments received. Post by %1 shared successfully. 1=author of the post we are sharing Received '%1'. %1 is the name of a feed Partial list of 'following' received. List of 'followers' completely received. Partial list of 'followers' received. List of 'lists' received. Person list deleted successfully. Person list received. File uploaded successfully. Posting message... Avatar uploaded. SSL errors in connection to %1! Loading external image from %1 regardless of SSL errors, as configured... %1 is a hostname The application is not registered with your server yet. Registering... Getting OAuth token... OAuth support error Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support. You probably need to install the OpenSSL plugin for QCA: %1, %2 or similar. Authorization error There was an OAuth error while trying to get the authorization token. QOAuth error %1 Application authorized successfully. Waiting for proxy password... Still waiting for profile. Trying again... Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally. All initial data received. Initialization complete. Ready. SiteUsersList You can get a list of the newest users registered on your server by clicking the button below. More resources to find users: Wiki page 'Users by language' PPump user search service at inventati.org List of Followers for the Pump.io Community account Get list of users from your server Close list Loading... %1 users in %2 %1 = user count, %2 = server name 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, for instance: Press <b>F1</b> if you want to open the Help window. 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 over a button or a text field with your mouse, you'll probably see some extra information. Dianara's blog Pump.io User Guide 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. Newest Newer Older Requesting... Loading... Page %1 of %2. Showing %1 posts per page. %1 posts in total. Click here or press Control+G to jump to a specific page '%1' cannot be updated because a comment is currently being composed. %1 = feed's name %1 more posts pending for next update. Click here to receive them now. There are no posts Timestamp Invalid timestamp! A minute ago %1 minutes ago An hour ago %1 hours ago Just now In the future Yesterday %1 days ago A month ago %1 months ago A year ago %1 years ago UserPosts Posts by %1 Loading... &Close Received '%1'. %1 posts Error loading the timeline dianara-v1.4.1/translations/dianara_gl.ts0000644000175000017500000067003113221212313016553 0ustar janjan ASActivity Public Público %1 by %2 1=kind of object: note, comment, etc; 2=author's name %1 por %2 ASObject Note Noun, an object type Nota Article Noun, an object type Artigo Image Noun, an object type Imaxe Audio Noun, an object type Audio Video Noun, an object type Vídeo File Noun, an object type Ficheiro Comment Noun, as in object type: a comment Comentario Group Noun, an object type Grupo Collection Noun, an object type Colección Other As in: other type of post Outro No detailed location Non hai ubicación detallada Deleted on %1 Borrado en %1 and one other e un máis and %1 others e %1 máis ASPerson Hometown Cidade AccountDialog Your Pump.io address: O seu enderezo Pump.io: Get &Verifier Code Obter Código de &Verificación Verifier code: Código de verificación: Enter or paste the verifier code provided by your Pump server here Introduza ou pegue eiquí o código de verificación que lle proporcionou o seu servidor Pump &Save Details &Gardar Detalles If the browser doesn't open automatically, copy this address manually Se o navegador non se abre automáticamente, copie este enderezo manualmente Account Configuration Configuración da Conta First, enter your Webfinger ID, your pump.io address. Primeiro, introduza o seu ID Webfinger, o seu enderezo pump.io. Your address looks like username@pumpserver.org, and you can find it in your profile, in the web interface. O seu enderezo é algo como nomedeusuario@servidor.org, e pódeo atopar no seu perfil, no interfaz web. If your profile is at https://pump.example/yourname, then your address is yourname@pump.example Se o seu perfil está en https://pump.exemplo/seunome, entón o seu enderezo é seunome@pump.exemplo If you don't have an account yet, you can sign up for one at %1. This link will take you to a random public server. 1=link to website Se aínda non ten unha conta, pode rexistrar unha e %1. Esta ligazón levaralle aleatoriamente a un servidor público. If you need help: %1 Se necesita axuda: %1 Pump.io User Guide Guía de Usuario Pump.io Your address, like username@pumpserver.org O seu enderezo, tal como nomedeusuario@servidorpump.org After clicking this button, a web browser will open, requesting authorization for Dianara Despois de premer neste botón, abrirase un navegador, 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! Unha vez teña autorizado a Dianara dene a interfaz web do seu servidor Pump, recibirá un código chamdo VERIFIER. Copie e pégueo no campo de embaixo. &Authorize Application &Autorizar Aplicación &Cancel &Cancelar Your account is properly configured. A súa conta está configurada correctamente. Press Unlock if you wish to configure a different account. Prema Desbloquear se desexa configurar unha conta diferente. &Unlock &Desbloquear A web browser will start now, where you can get the verifier code Agora arrancará un navegador, onde poderá obter o código de verificación Your Pump address is invalid O seu enderezo Pump non é válido Verifier code is empty O código de verificación está baleiro Dianara is authorized to access your data Dianara está autorizada para acceder aos seus datos Unable to open web browser! No se puido abrir o navegador! AudienceSelector 'To' List Lista 'Para' 'Cc' List Lista 'Cc' &Add to Selected &Engadir á Selección All Contacts Tódolos 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. ON THE LEFT should change to ON THE RIGHT in RTL languages Seleccione xente da lista da esquerda. Mode arrastralos con rato, facer click ou doble-click neles, ou seleccionalos e usar o botón de embaixo. Clear &List Limpar &Lista &Done &Feito &Cancel &Cancelar Public Público Followers Seguidores Lists Listas People... Xente... Selected People Persoas Seleccionadas AvatarButton Open %1's profile in web browser Abrir o perfil de %1 no navegador Open your profile in web browser Abrir o seu perfil no navegador Send message to %1 Enviar mensaxe a %1 Browse messages Revisar mensaxes Stop following Deixar de seguir Follow Seguir Stop following? Deixar de seguir? Are you sure you want to stop following %1? Esta vostede seguro de deixar de seguir a %1? &Yes, stop following &Sí, deixar de seguir &No &Non BannerNotification Timelines were not automatically updated to avoid interruptions. As liñas temporais non se actualizaron automáticamente para evitar interrupcións. This happens when it is time to autoupdate the timelines, but you are not at the top of the first page, to avoid interruptions while you read Isto ocorre cando é o momento de auto actualizar as liñas temporais, pero non está enrriba de todo na primeira páxina, para evitar interrupcións mentres vostede le Update now Actualizar agora Hide this message Ocultar esta mensaxe ColorPicker Change... Cambiar... Choose a color Elixir unha cor Comment Posted on %1 Publicado o %1 Modified on %1 Modificado o %1 Like or unlike this comment Gostar ou deixar de gostar este comentario Quote This is a verb, infinitive Citar Reply quoting this comment Respostar citar este comentario Edit Editar Modify this comment Modificar este comentario Delete Borrar Erase this comment Borrar este comentario Unlike Deixar de gostar Like Gostar %1 like this comment Plural: %1=list of people like John, Jane, Smith %1 lles gosta este comentario %1 likes this comment Singular: %1=name of just 1 person %1 góstalle este comentario WARNING: Delete comment? ATENCIÓN: Borrar comentario? Are you sure you want to delete this comment? Está vostede seguro de querer borrar este comentario? &Yes, delete it &Sí, borralo &No &Non CommenterBlock You can press Control+Enter to send the comment with the keyboard Pode premer Control+Enter para enviar o comentario c teclado Reload comments Recargar comentarios Comment Infinitive verb Comentar Cancel Cancelar Press ESC to cancel the comment if there is no text Prema ESC para cancelar o comentario se non ten texto Check for comments Comprobar se hai comentarios Show all %1 comments Amosar todos os %1 comentarios Comments are not available Comentarios non dispoñibles Error: Already composing Erro: Xa está redactando You can't edit a comment at this time, because another comment is already being composed. Non pode editar un comentario agora porque xa está redactando outro comentario. Editing comment Editando comentario Loading comments... Cargando comentarios... Posting comment failed. Try again. Falllou o comentario da publicación. Ténteo outra vez. An error occurred Ocurríu un erro Sending comment... Enviando comentario... Updating comment... Actualizando comentario... Comment is empty. O comentario está baleiro. Composer Type a message here to post it Escriba unha mensaxe eiquí para publicala Click here or press Control+N to post a note... Prema eiquí o pulse Ctrl+N para escribir unha nota... Symbols Sïmbolos Formatting Formato Normal Normal Bold Negrita Italic Itálica Underline Suliñar Strikethrough Tachado Header Cabeceiro List Lista Table Táboa Preformatted block Bloque preformateado Quote block Bloque de Cita Make a link Facer unha ligazón Insert an image from a web site Insertar unha imaxe dende un sitio web Insert line Insertar liña &Format Button for text formatting and related options &Formato Text Formatting Options Opcións de Formateo de Texto Paste Text Without Formatting Pegar Texto Sen Formatear Type a comment here Teclee un comentario eiquí You can attach only one file. Só pode mandar un ficheiro. You cannot drop folders here, only a single file. Vostede non pode arrastrar cartafoles aquí; só un ficheiro. Insert as image? Insertar como imaxen? The link you are pasting seems to point to an image. A ligazón que está pegando semella apuntar a unha imaxe. Insert as visible image Insertar como imaxen visible Insert as link Insertar como ligazón Table Size Tamaño de Táboa How many rows (height)? Cantas filas (altura)? How many columns (width)? Cantas columnas (anchura)? Insert a link Inserte unha ligazón Type or paste a web address here. You could also select some text first, to turn it into a link. Teclee ou copie o enderezo web eiquí. Tamén pode seleccionar antes algo de texto, para convertelo nunha ligazón. Make a link from selected text Facer unha ligazón a partires do texto seleccionado Type or paste a web address here. The selected text (%1) will be converted to a link. Teclee ou copie un enderezo web eiquí. O texto seleccionado (%1) converterase nunha ligazón. Invalid link Ligazón non válida The text you entered does not look like a link. O texto que introducíu non semella unha ligazón. It should start with one of these types: It = the link, from previous sentence Esta debería comenzar cun destes tipos: &Use it anyway &Usalo igualmente &Enter it again &Introducilo outra vez &Cancel link &Cancelar ligazón Insert an image from a URL Insertar unha imaxe dende unha URL Type or paste the image address here. The link must point to the image file directly. Teclee ou pegue a dirección da imaxe eiquí. A ligazón debe apuntar directamente ao ficheiro da imaxe. Error: Invalid URL Erro: URL non válida The address you entered (%1) is not valid. Image addresses should begin with http:// or https:// A dirección que introducíu (%1) non é válida. As direccións das imaxes deberían comenzar con http:// ou https:// Yes, but saving a &draft Mantengo al atajo de teclado Si, mais gardan&do un borrador Cancel message? Cancelar mensaxe? Are you sure you want to cancel this message? Está seguro de querer cancelar esta mensaxe? &Yes, cancel it &Sí, cancelalo &No &Non ConfigDialog minutes minutos Top Enrriba Bottom Abaixo de todo Program Configuration Configuración do Programa Timeline &update interval Intervalo de act&ualización da Liña Temporal posts Goes after a number, as: 25 posts mensaxes &Posts per page, main timeline &Mensaxes por páxina, liña temporal principal posts This goes after a number, like: 10 posts publicacións Posts per page, &other timelines Publicacións por páxina; &outras liñas temporais &Tabs position Poisición das Pes&tañas &Movable tabs Táboas &Movibeis Public posts as &default Publicacións públicas por &defecto Pro&xy Settings Axustes de Pro&xy Network configuration Configuración de rede Set Up F&ilters Configurar F&iltros Filtering rules Reglas de filtrado Highlighted activities, except mine Actividades destacadas, excepto as miñas Any highlighted activity Calquera actividade destacada Always Sempre Never Nunca Comments Comentarios Left side tabs on left side/west; RTL not affected Lado esquerdo Right side tabs on right side/east; RTL not affected Lado dereito You are among the recipients of the activity, such as a comment addressed to you. Vostede está entre os destinatarios da actividade, tal como un comentario dirixido a vostede. Used also when highlighting posts addressed to you in the timelines. Usado tamén cando se resaltan publicacións dirixidas a vostede nas liñas temporais. The activity is in reply to something done by you, such as a comment posted in reply to one of your notes. A actividade é en resposta a algo feito por vostede, tal como un comentario publicado en resposta a unha das súas notas. You are the object of the activity, such as someone adding you to a list. Vostede é o obxecto da actividade, como cando alguén lle engade a unha lista. The activity is related to one of your objects, such as someone liking one of your posts. A actividade está relacionada cun dos seus obxectos, como cando alguén lle gosta unha das súas publicacións. Used also when highlighting your own posts in the timelines. Usado tamén cando se resaltan as súas propias publicacións nas liñas temmporais. Item highlighted due to filtering rules. Elemento destacado debido ás regras de filtrado. Item is new. O elemento é novo. Show snippets in minor feeds Amosar anacos en liñas temporais menores Show information for deleted posts Amosar información para publicacións borradas No Non Before avatar Antes do avatar Before avatar, subtle Antes do avatar, sutil After avatar Despois do avatar After avatar, subtle Despois do avatar, sutil Hide duplicated posts Ocultar publicacións duplicadas Jump to new posts line on update Saltar á liña de novas publicacións ao actualizar Snippet limit when highlighted Límite de anaco cando sexa destacado Minor feed avatar sizes Tamaños dos avatares nas liñas temporais menores Show activity icons Amosar iconas de actividade Avatar size Tamaño do avatar Avatar size in comments Tamaño do avatar nos comentarios Show extended share information Amosar información de compartición extendida Show extra information Amosar información adicional Highlight post author's comments Destacar os comentarios do autor da publicación Highlight your own comments Destacara os seus propios comentarios Ignore SSL errors in images Ignorar erros SSL nas imaxes Show full size images Amosar imaxes en tamaño completo Show character counter Amosar contador de caracteres Don't inform followers when following someone Non informar aos seguidores cando sigo a alguén Don't inform followers when handling lists Non informar aos seguidores cando manexo as listas As system notifications Como notificacións do sistema Using own notifications Usar notificacións propias Don't show notifications Non amosar notificacións seconds Next to a duration, in seconds segundos Notification Style Estilo de Notificación Duration Duración Persistent Notifications Notificacións Persistentes Also highlight taskbar entry Suliñar tamén a entrada na barra de tarefas Notify when receiving: Notificar ao recibir: New posts Novas publicaións Highlighted posts Publicacións destacadas New activities in minor feed Novas actividades en liña temporal secundaria Highlighted activities in minor feed Actividades destacadas en liña temporal secundaria Important errors Erros importantes Default Por defecto System iconset, if available Conxunto de iconas do sistema, se o hai Show your current avatar Amosar o seu avatar actual Custom icon Icona personalizada System Tray Icon &Type &Tipo de Icona de Bandexa do Sistema S&elect... S&eleccionar... Custom &Icon &Icona Personalizada Hide window on startup Ocultar fiestra no arranque Timelines Liñas temporais Posts Publicacións Composer Editor Privacy Privacidade Notifications Notificacións System Tray Bandexa do Sistema This is a system notification Esta é unha notificación do sistema System notifications are not available! Non están dispoñibeis as notificacións do sistema! Own notifications will be used. Empregaranse as notificacións propias. This is a basic notification Esta é una notificación básica Select custom icon Elixir icona personalizada Post Titles Títulos das Publicacións characters This is a suffix, after a number caracteres Snippet limit Límite de anaco Post Contents Contidos das Publicacións Minor Feeds Liñas Temporais Secundarias Only for images inserted from web sites. Só para imaxes insertadas dende sitios web. Use with care. Usar con coidado. Use attachment filename as initial post title Usar o nome do ficheiro adxunto como título inicial da publicación Inform only the author when liking things Informar só ao autor cando lle gosten cousas General Options Opcións Xerais Fonts Fontes Colors Cores Dianara stores data in this folder: Dianara garda os datos neste cartafol: &Save Configuration &Gardar Configuración &Cancel &Cancelar Image files Ficheiros de imaxen All files Tódolos ficheiros Invalid image Imaxe non válida The selected image is not valid. A imaxe seleccionada non é válida. ContactCard Hometown Cidade Joined: %1 Unido: %1 Updated: %1 Actualizado: %1 Bio for %1 Abbreviation for Biography, but you can use the full word; %1=contact name Bio para %1 This user doesn't have a biography Este usuario non ten unha biografía No biography for %1 %1=contact name Non hai biografía para %1 Open Profile in Web Browser Abrir Perfil no Navegador Send Message Enviar Mensaxe Browse Messages Visualizar Mensaxes User Options Opcións de Usuario Follow Seguir Stop Following Deixar de Seguir Stop following? Deixar de seguir? Are you sure you want to stop following %1? Está seguro de querer deixar de seguir a %1? &Yes, stop following &Sí, deixar de seguir &No &Non ContactList Type a partial name or ID to find a contact... Escriba parte do nome ou o ID para atopar un contacto... F&ull List Lista C&ompleta ContactManager username@server.org or https://server.org/username nomedeusuario@servidor.org ou https://servidor.org/nomedeusuario &Enter address to follow: &Introduza dirección a seguir: &Follow &Seguir Reload Followers Recargar Seguidores Reload Following Recargar Seguindo Export Followers Exportar Seguidores Export Following Export Seguindo Reload Lists Recargar Listas &Neighbors &Veciños Follo&wers &Seguidores Followin&g Se&guindo &Lists &Listas Export list of 'following' to a file Exportar a un ficheiro a lista 'seguindo' Export list of 'followers' to a file Exportar a lista de 'seguidores' a un ficheiro Cannot export to this file: No podo exportar a este ficheiro: Please enter another file name, or choose a different folder. Por favor introduza outro nome de ficheiro, ou elixa un caratafol diferente. The server seems to be a Pump server, but the account does not exist. Semella que o servidor é un servidor Pump, pero a conta non existe. %1 doesn't seem to be a Pump server. %1 is a hostname %1 non semella ser un servidor Pump. Following this account at this time will probably not work. Seguir esta conta neste intre probablemente non funcionará. The %1 server seems unavailable. %1 is a hostname O servidor %1 semella non estar dispoñible. Unknown Refers to server version Descoñecida Error Erro The user address %1 does not exist, or the %2 server is down. A dirección de usuario %1 non existe, ou o servidor %2 está caído. Server version Versión do servidor Check the address, and keep in mind that usernames are case-sensitive. Comprobe a dirección, e teña en conta que os nomes de usuario distinguen MAIÚSCULAS e minúsculas. Do you want to try following this address anyway? Quere tentar seguir esta dirección igualmente? (not recommended) (non recomendado) Yes, follow anyway Sí, seguir igualmente No, cancel Non, cancelar About to follow %1... Vai seguir a %1... DownloadWidget Open Verb, as in: Open the downloaded file Abrir Download Descargar Save the attached file to your folders Grabar o ficheiro adxunto nos seus cartafois Cancel Cancelar Save File As... Gardar Ficheiro Como... All files Tódolos ficheiros File not found! Ficheiro non atopado! Abort download? Abortar descarga? Do you want to stop downloading the attached file? Quere deixar de descargar o ficheiro adxunto? &Yes, stop &Sí, parar &No, continue &Non, continuar Download aborted Descarga abortada Download completed Descarga completada Attachment downloaded successfully to %1 %1 = filename Adxunto descargado con éxito en %1 Open the downloaded attachment with your system's default program for this type of file. Abrir o adxunto descargado co programa por defecto do seu sistema para este tipo de ficheiro. Download failed Descarga fallida Downloading attachment failed: %1 %1 = filename A descarga do adxunto fallou: %1 Downloading %1 KiB... Descargando %1 KiB... %1 KiB downloaded %1 KiB descargados DraftsManager Draft Manager Xestor de Borradores Load Cargar Save Gardar Manage drafts... Xestionar borradores... &Delete selected draft Eliminar o borra&dor elixido &Close &Pechar Untitled draft Borrador sen título Delete draft? Eliminar borrador? Are you sure you want to delete this draft? Está vostede seguro de querer eliminar este borrador? &Yes, delete it &Si, bórrao &No &Non EmailChanger Change E-mail Address Cambiar Enderezo Electrónico Change Cambiar &Cancel &Cancelar E-mail Address: Enderezo Electrónico: Again: Outra vez: Your Password: O seu Contrasinal: E-mail addresses don't match! Os enderezos electrónicos non coinciden! Password is empty! O contrasinal está baleiro! FDNotifications Show Amosar FilterEditor Filter Editor Filtrar Editor %1 if %2 contains: %3 This explains a filter rule, like: Hide if Author ID contains JohnDoe %1 se %2 contén: %3 Here you can set some rules for hiding or highlighting stuff. 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. You could also highlight messages that contain your name. Eiquí pode crear algunhas regras para ocultar ou remarcar cousas. Pode filtrar por contido autor ou aplicación. Por exemplo, pode filtrar as mensaxes publicadas pola aplicación Open Farm Game, ou as que conteñan a palabra NSFW na mensaxe. Tamén podería resaltar as mensaxes que conteñan o seu nome. Hide Ocultar Highlight Destacado Post Contents Contidos da Publicación Author ID ID do Autor Application Aplicación Activity Description Descripción da Actividade Keywords... Palabras clave... &Add Filter &Engadir Filtro Filters in use Filtros en uso &Remove Selected Filter &Eliminar Filtro Seleccionado &Save Filters &Gardar Filtros &Cancel &Cancelar if se contains contén &New Filter &Novo Filtro C&urrent Filters Filtros Ac&tuais FilterMatchesWidget Content The contents of the post matched Contidos Author Autor/a App Application, short if possible App Description Descrición FirstRunWizard Welcome Wizard Asistente de Benvida Welcome to Dianara! Benvido/a a Dianara! This wizard will help you get started. O asistente axudaralle a comezar. You can access this window again at any time from the Help menu. Pode volver a acceder a esta ventá en calquera momento dende o menú Axuda. The first step is setting up your account, by using the following button: O primeiro paso é configurar a súa conta, usando o seguinte botón: Configure your &account Configurar a súa cont&a Once you have configured your account, it's recommended that you edit your profile and add an avatar and some other information, if you haven't done so already. Unha vez que teña configurada a súa conta, é recomendable que edite o seu perfil e engada un avatar e algunha outra información, se aínda non o fixo. &Edit your profile &Editar o seu perfil By default, Dianara will post only to your followers, but it's recommended that you post to Public, at least sometimes. Por defecto, Dianara publicará só para os seus seguidores, pero recoméndase que publique de xeito Público, alomenos algunhas veces. Post to &Public by default Publicar en modo &Público por defecto Open general program &help window Abrir fiestra de &axuda xeral do programa &Show this again next time Dianara starts &Amosar isto outra vez cando volva a arrancar Dianara &Close &Pechar FontPicker Change... Cambiar... Choose a font Elixa unha fonte HelpWidget Basic Help Axuda Básica Getting started Comezando The first time you start Dianara, you should see the Account Configuration dialog. There, enter your Pump.io address as name@server and press the Get Verifier Code button. A primeira vez que arranque Dianara, debería ver un diálogo de Configuración de Conta. Alí, introduza o seu enderezo Pump.io como nome@usuario e prema no botón Obter Código de Verificación. Then, your usual web browser should load the authorization page in your Pump.io server. There, you'll have to copy the full VERIFIER code, and paste it into Dianara's second field. Then press Authorize Application, and once it's confirmed, press Save Details. Despois, o seu navegador habitual debería cargar a páxina de autorización no seu servidor Pump.io. Ali, tera que copiar enteiro o código de VERIFICACIÓN, e pegalo no segundo campo de Dianara. Prema entón Autorizar Aplicación, e unha vez sexa confirmado, prema Gardar Detalles. At this point, your profile, contact lists and timelines will be loaded. Neste punto, se cargarán o seu perfil, lista de contactos e liñas temporais. You should take a look at the Program Configuration window, under the Settings - Configure Dianara menu. There are several interesting options there. Debería botarlle un vistazo á fiestra de Configuración do Programa, no menú Axustes - Configurar Dianara. Hai varias opcións interesantes ahí. Settings Axustes You can configure several things to your liking in the settings, like the time interval between timeline updates, how many posts per page you want, highlight colors, notifications or how the system tray icon looks. Pode configurar varias cousas ao seu xeito nos axustes, como o intervalo de tempo entre actualizacións das liñas temporais, cántas publicacións por páxina quere, cores de suliñado, notificiacións ou a apariencia da icona da bandexa do sistema. Timelines Liñas temporais Contents Contidos Keep in mind that there are a lot of places in Dianara where you can get more information by hovering over some text or button with your mouse, and waiting for the tooltip to appear. Teña en conta que hai un montón de lugares en Dianara onde pode obter máis información movendo o ratón sobre algún texto ou botón, e esperando a que apareza a descripción emerxente. If you're new to Pump.io, take a look at this guide: Se vostede é novo en Pump.io, bótelle un vistazo a esta guía: Here, you can also activate the option to always publish your posts as Public by default. You can always change that at the moment of posting. Eiquí, tamén pode activar a opción de facer sempre públicas por defecto as súas publicacións. Pode cambialo sempre no momento de publicar. The main timeline, where you'll see all the stuff posted or shared by the people you follow. A liña temporal principal, onde verá todo o publicado ou compartido pola xente que sigue. Messages timeline, where you'll see messages sent to you specifically. These messages might have been sent to other people too. Liña temporal de mensaxes, onde verá as mensaxes que lle enviaron específicamente a vostede. Estas mensaxes tamen poden ter sido enviadas a outras persoas. Activity timeline, where you'll see your own posts, or posts shared by you. Liña temporal de actividade, onde verá as súas propias publicacións, ous as compartidas por vostede. Favorites timeline, where you'll see the posts and comments you've liked. This can be used as a bookmark system. Liña temporal de favoritos, onde verá as pubicacións e comentarios que lle gustaron. Isto pode ser empregado como un sistema de marcadores. These activities might have a '+' button in them. Press it to open the post they're referencing. Also, as in many other places, you can hover with your mouse to see relevant information in the tooltip. Estas actividades poderían ter un botón '+' nelas. Prema nel para abrir a publicación á que fan referencia. Asemade, como en moitos outros sitios, pode pasar o ratón por riba para ver información relevante na información emerxente. Posting Publicando New messages appear highlighted in a different color. You can mark them as read just by clicking on any empty parts of the message. As novas mensaxes aparecen resaltadas nunha cor diferente. Pode marcalas como leídas premendo simplemente en calquera parte baleira da mensaxe. You can post notes by clicking in the text field at the top of the window or by pressing Control+N. Setting a title for your post is optional, but highly recommended, as it will help to better identify references to your post in the minor feed, e-mail notifications, etc. Pode publicar notas premendo no campo de texto na parte superior da fiestra ou premendo Control+N. Poñerlle un título ás súas publicacións é opcional, mais altamente recomendable, xa que axudará a identificar mellor as referencias á súa publicación na liña temporal secundaria, notificacións por correo electrónico, etc. It is possible to attach images, audio, video, and general files, like PDF documents, to your post. É posible adxuntar imaxes, audio, vídeo, e ficheiros xenéricos, como documentos PDF, á súa publicación. You can use the Format button to add formatting to your text, like bold or italics. Some of these options require text to be selected before they are used. Pode usar o botón Formato para engadir formato ao seu texto, tal como negrita ou itálica. Algunhas destas opcións requiren que haxa texto seleccionado antes de empregalas. If you add a specific person to the 'To' list, they will receive your message in their direct messages tab. Se engades unha persona en concreto á lista 'Para', esta recibirá a túa mensaxe na súa pestana de mensaxes directas. Choose one with the arrow keys and press Enter to complete the name. This will add that person to the recipients list. Elixha unha coas flechas e prema Enter para completar o nome. Isto engadirá esa persona á lista de destinatarios. You can create private messages by adding specific people to these lists, and unselecting the Followers or the Public options. Pode crear mesaxes privadas engadindo xente concreta a estas listas, e desmarcando as opcións Seguidores ou Público. Managing contacts Xestionando contactos You can see the lists of people you follow, and who follow you from the Contacts tab. Pode ver as listas de xente que segue, e quén lle segue na pestaña de Contactos. There, you can also manage person lists, used mainly to send posts to specific groups of people. Alí, tamén pode manexar listas de personas, usadas principalmente para enviar mensaxes a grupos específicos de xente. You can click on any avatars in the posts, the comments, and the Meanwhile column, and you will get a menu with several options, one of which is following or unfollowing that person. Pode premer en calquer avatar nas publicacións, nos comentarios e na columna Mentras tanto, e sairalle un menú con varias opcións, unha das cales é seguir ou deixar de seguir a esa persona. Keyboard controls Controis do teclado Pump.io User Guide Guía de Usuario Pump.io There are seven timelines: Hai sete liñas temporais: The fifth timeline is the minor timeline, also known as the Meanwhile. This is visible on the left side, though it can be hidden. Here you'll see minor activities done by everyone you follow, such as comment actions, liking posts or following people. LEFT SIDE should change to RIGHT SIDE on RTL languages A quinta liña temporal é a liña temporal secundaria, tamén coñecida como Mentras tanto. Esta é visible no lado esquerdo, aínda que pode ocultarse. Eiquí verá actividades secundarias realizadas por calquera que vostede siga, tal como accións sobre comentarios, gostar publicacións ou seguir xente. The sixth and seventh timelines are also minor timelines, similar to the Meanwhile, but containing only activities directly addressed to you (Mentions) and activities done by you (Actions). As liñas temporais sexta e sétima tamén son liñas temporais secundarias, similares ao Mentras tanto, pero conteñen só actividades dirixidas directamente a vostede (Mencións) e actividades feitas por vostede (Accións). You can select who will see your post by using the To and Cc buttons. Pode seleccionar quén verá a súa publicación usando os botóns Para e Cc. You can also type '@' and the first characters of the name of a contact to bring up a popup menu with matching choices. Tamén pode teclear '@' e os primeiros caracteres do nome dun contacto para sacar un menú emerxente con opcións axeitadas. There is a text field at the top, where you can directly enter addresses of new contacts to follow them. Hai un campo de texto enriba de todo, onde pode introducir directamente direccións de novos contactos para seguirlles. You can also send a direct message (initially private) to that contact from this menu. Tamén pode enviar unha mensaxe directa (inicialmente privada) ao contacto dende este menú. You can find a list with some Pump.io users and other information here: Pode atopar unha lista con algúns usuarios Pump.io e outra información eiquí: Users by language Usuarios por idioma Followers of Pump.io Community account Seguidores da conta Pump.io Community The most common actions found on the menus have keyboard shortcuts written next to them, like F5 or Control+N. As accións máis comúns atopadas nos menús teñen atallos de teclado escritos ao seu carón, como F5 ou Control+N. Besides that, you can use: Aparte diso, pode usar: Control+Up/Down/PgUp/PgDown/Home/End to move around the timeline. Control+Up/Down/PgUp/PgDown/Home/End para moverese porla liña temporal. Control+Left/Right to jump one page in the timeline. Control+Esquerda/Dereita para saltar unha páxina na liña temporal. Control+G to go to any page in the timeline directly. Control+G para ir directamente a calquera páxina na liña temporal. Control+1/2/3 to switch between the minor feeds. Control+1/2/3 para cambiar entre liñas temporais secundarias. Control+Enter to post, when you're done composing a note or a comment. If the note is empty, you can cancel it by pressing ESC. Control+Enter para publicar, cando teña rematada a nota ou o comentario. Se a nota está baleira, pódea cancelar premendo ESC. Dianara offers a D-Bus interface that allows some control from other applications. Dianara ofrece un interfaz D-Bus que permite algún control dende outras aplicacións. The interface is at %1, and you can access it with tools such as %2 or %3. It offers methods like %4 and %5. O interfaz está en %1, e pode acceder a el con ferramentas tales como %2 ou %3. Ofrece métodos como %4 e %5. Command line options Opcións en liña de comandos Under the 'Neighbors' tab you'll see some resources to find people, and have the option to browse the latest registered users from your server directly. Debaixo da pestaña 'Veciños' verá algúns recursos para atopar xente, e ten a opción de visualizar directamente os últimos usuarios rexistrados no seu servidor. While composing a note, press Enter to jump from the title to the message body. Also, pressing the Up arrow while you're at the start of the message, jumps back to the title. Mentras compón unha nota, prema Enter para saltar dende o título ao corpo da mensaxe. Tamén, premendo a tecla Arriba mentras está no comenzo da mensaxe, lévalle de volta ao título. Control+Enter to finish creating a list of recipients for a post, in the 'To' or 'Cc' lists. Control+Enter para rematar de crear a lista de destinatarios para unha publicación, nas listas 'Para' ou 'Cc'. You can use the --config parameter to run the program with a different configuration. This can be useful to use two or more different accounts. You can even run two instances of Dianara at the same time. Pode usar o parámetro --config para executar o programa cunha configuración diferente. Isto poder ser útil para usar dúas ou máis contas diferentes. Incluso pode executar dúas instancias de Dianara ao mesmo tempo. Use the --debug parameter to have extra information in your terminal window, about what the program is doing. Use o parámetro --debug para obter información extra na súa fiestra de terminal, acerca de qué está a facer o programa. If your server does not support HTTPS, you can use the --nohttps parameter. Se o seu servidor non soporta HTTPS, pode usar o parámetro --nohttps. If you use an alternate configuration, with something like '--config otherconf', then the interface will be at org.nongnu.dianara_otherconf. Se usa unha configuración alternativa, con algo como '--config outraconfig', entón a interface estará en org.nongnu.dianara_outraconfig. &Close &Pechar ImageViewer Untitled Sen Título Image Imaxe &Save As... &Gardar Como... &Restart Restart animation &Recomenzar Fit As in: fit image to window Encaixar Rotate image to the left RTL: This actually means LEFT, anticlockwise Rotar imáxe á esquerda Rotate image to the right RTL: This actually means RIGHT, clockwise Rotar imaxe á dereita &Close &Pechar Save Image... Gardar Imaxen... Close Viewer Pechar Visor Downloading full image... Descargando imaxe completa... Error downloading image! Erro descargando imaxe! Try again later. Ténteo máis tarde. Save Image As... Gardar Imaxen Como... Image files Ficheiros de imaxe All files Tódolos ficheiros Error saving image Erro gardando imaxe There was a problem while saving %1. Filename should end in .jpg or .png extensions. Houbo un problema gardando %1. O nome do ficheiro debería rematar en extensións .jpg ou .png. ListsManager Name Nome Members Membros Add Mem&ber Engadir Mem&bro &Remove Member &Eliminar Membro &Delete Selected List Borrar Lista &Seleccionada Add New &List Engadir Nova &Lista Create L&ist Crear L&ista &Add to List &Engadir a Lista Are you sure you want to delete %1? 1=Name of a person list Esta vostede seguro de querer borrar %1? Remove person from list? Quitar persona da lista? Are you sure you want to remove %1 from the %2 list? 1=Name of a person, 2=name of a list Esta seguro de querer quitar a %1 da lista %2? &Yes &Sí Type a name for the new list... Escriba un nome para a nova lista... Type an optional description here Escriba eiquí unha descripción opcional WARNING: Delete list? ATENCIÓN: Borrar lista? &Yes, delete it &Sí, borralo &No &Non LogViewer Log Rexistro Clear &Log Limpar &Rexistro &Close Pe&char MainWindow Side &Panel &Panel Lateral Status &Bar &Barra de Estado &Timeline Liña &Temporal The main timeline A liña temporal principal &Messages &Mensaxes Messages sent explicitly to you Mensaxes enviadas explícitamente a vostede &Activity &Actividade Your own posts As súas publicacións Your favorited posts A sús publicacións favoritas &Contacts &Contactos Initializing... Inicializando... Your account is not configured yet. A súa conta aínda non está configurada. Dianara started. Dianara arrancada. Minor activities done by everyone, such as replying to posts Actividades secundarias feitas por todo o mundo, tales como respostar ás publicacións Minor activities addressed to you Actividades secundarias dirixidas a vostede Minor activities done by you Actividades secundarias feitas por vostede The people you follow, the ones who follow you, and your person lists A xente á que segue, os que lle seguen a vostede, e as súas listas de personas Press F1 for help Prema F1 para axuda Running with Qt v%1. Executándose con Qt v%1. Click here to configure your account Prema eiquí para configurar a súa conta &Session &Sesión Auto-update &Timelines Auto actualizar &Liñas Temporais Mark All as Read Marcar Todo Como Leído &Post a Note &Publicar unha Nota &Quit &Sair &View &Vista Locked Panels and Toolbars Paneis e Barras de Ferramentas bloqueadas Side Panel Panel Lateral &Toolbar Barra de &Ferramentas Full &Screen Pantalla C&ompleta &Log &Rexistro S&ettings A&xustes Edit &Profile Editar &Perfil &Account &Conta Basic &Help &Axuda Básica Report a &Bug Informar dun &Bug Pump.io User &Guide Guía de Usuario &Pump.io Some Pump.io &Tips Algunhas Pis&tas sobre Pump.io List of Some Pump.io &Users Lista dalgúns &Usuarios Pump.io Pump.io &Network Status Website Sitio Web de Estado da &Rede Pump.io Toolbar Barra de ferramentas Open the log viewer Abrir o visor do rexistro Auto-updating enabled Auto-actualización habilitada Auto-updating disabled Auto-actualización deshabilitada Proxy password required Requírese contrasinal para o proxy You have configured a proxy server with authentication, but the password is not set. Configurou un servidor proxy con autentificación, pero non puxo o contrasinal. Enter the password for your proxy server: Introduza o contrasinal para o seu servidor proxy: Starting automatic update of timelines, once every %1 minutes. Comenzando actualización automática das liñas temporais, unha vez cada %1 minutos. Stopping automatic update of timelines. Parando actualización automática das liñas temporais. Received %1 older posts in '%2'. %1 is a number, %2 = name of a timeline Recibida(s) %1 publicación(s) antiga(s) en %2. 1 highlighted singular, refers to a post 1 destacada %1 highlighted plural, refers to posts %1 destacada Direct messages Mensaxes directas By filters Por filtros 1 more pending to receive. singular, one post 1 máis pendente de recibir. %1 more pending to receive. plural, several posts %1 máis pendente de recibir. Also: Tamén: Last update: %1 Última actualizaión: %1 '%1' updated. %1 is the name of a feed '%1' actualizada. Show Welcome Wizard Amosar Asistente de Benvida 1 filtered out. singular, refers to a post 1 filtrado. %1 filtered out. plural, refers to posts %1 filtrado. 1 deleted. singular, refers to a post 1 borrado. %1 deleted. plural, refers to posts %1 borrado. There is 1 new activity. Hai 1 nova actividade. There are %1 new activities. Hai %1 novas actividades. 1 highlighted. singular, refers to an activity 1 destacada. %1 highlighted. plural, refers to activities %1 destacadas. 1 filtered out. singular, refers to one activity 1 filtrada. %1 filtered out. plural, several activities %1 filtradas. No new activities. Non hai actividades novas. Error storing image! Erro almacenando imaxe! %1 bytes %1 bytes Marking everything as read... Marcando todo como leído... Dianara is Free Software, licensed under the GNU GPL license, and uses some Oxygen icons under LGPL license. Dianara é Software Libre, licenciada baixo a licenza GNU-GPL, e usa algunas iconas Oxygen, baixo a licenza LGPL. Closing due to environment shutting down... Pechando porque o entorno estase a apagar... Quit? Sair? You are composing a note or a comment. Está escrebendo unha nota ou un comentario. Do you really want to close Dianara? Realmente quere pechar Dianara? &Yes, close the program &Sí, pechar o programa &No &Non Shutting down Dianara... Pechando Dianara... System tray icon is not available. A icona da bandexa do sistema non está dispoñible. Dianara cannot be hidden in the system tray. Non se pode agochar Dianara na bandexa do sistema. Do you want to close the program completely? Quere pechar o programa completamente? Timeline updated at %1. Lilña temporal actualizada ás %1. Update %1 Actualización %1 Your Pump.io account is not configured A súa conta Pump.io non está configurada Link to: %1 Enlazar a: %1 With Dianara you can see your timelines, create new posts, upload pictures and other media, interact with posts, manage your contacts and follow new people. Con Dianara pode ver as súas liñas temporais, crear novas publicacións, subir imaxes ou outros medios, interactuar coas publicacións, xestionar os seus contactos e seguir nova xente. English translation by JanKusanagi. TRANSLATORS: Change this with your language and name. If there was another translator before you, add your name after theirs ;) Tradución ao galego por EVAnaRkISTO. &Configure Dianara &Configurar Dianara &Filters and Highlighting &Filtros e Destacados &Help &Axuda Visit &Website Visitar Sitio &Web About &Dianara Acerca de &Dianara Your biography is empty A súa biografía está baleira Click to edit your profile Prema para editar o seu perfil No new posts. Non hai novas publicacións. Total posts: %1 Publicacións totais: %1 Favor&ites Favor&itos Received %1 older activities in '%2'. %1 is a number, %2 = name of feed Recibidas %1 actividades más antigas en %2. Minor feed updated at %1. Liña temporal secundaria actualizada ás %1. 1 more pending to receive. singular, 1 activity 1 máis pendente de recibir. %1 more pending to receive. plural, several activities %1 máis pendentes de recibir. &Hide Window Agoc&har Fiestra &Show Window Amo&sar Fiestra There is 1 new post. Hai 1 nova publicación. There are %1 new posts. Hai %1 novas publicacions. About Dianara Acerca de Dianara Dianara is a pump.io social networking client. Dianara é un cliente para a rede social pump.io. Thanks to all the testers, translators and packagers, who help make Dianara better! Grazas a todos os probadores, tradutores e empaquetadores, que axudan a mellorar Dianara! MinorFeed Older Activities Actividades Máis Antigas Get previous minor activities Obter actividades menores previas There are no activities to show yet. Aínda non hai actividades que amosar. Get %1 newer As in: Get 3 newer (activities) Obter %1 novos MinorFeedItem Using %1 Application used to generate this activity Usando %1 To: %1 1=people to whom this activity was sent A: %1 Cc: %1 1=people to whom this activity was sent as CC Cc: %1 Open referenced post Abrir a mensaxe referenciada MiscHelpers bytes bytes Error: Unable to launch browser Erro: Non se puido arrancar o navegador The default system web browser could not be executed. Non se puido executar o navegador por defecto do sistema. You might need to install the XDG utilities. Pode necesitar instalar as utilidades XDG. PageSelector Jump to page Saltar á páxina Page number: Páxina número: &First As in: first page &Primeira &Last As in: last page &Derradeira Newer As in: newer pages Máis novas Older As in: older pages Máis antigas &Go &Ir &Cancel &Cancelar PeopleWidget &Search: &Procurar: Enter a name here to search for it Introduza eiquí un nome para procuralo Add a contact to a list Engadir á lista de contactos &Cancel &Cancelar Post Via %1 A través de %1 Shared on %1 Compartido en %1 Loading image... Cargando imaxe... Edited: %1 Editado: %1 Posted on %1 1=Date Publicado o %1 In En To Para Post Noun, not verb Mensaxe Using %1 1=Program used for posting or sharing Usando %1 If you select some text, it will be quoted. Se selecciona algún texto, será citado. Share Compartir Unshare Descompartir Unshare this post Descompartir esta publicación Edit Editar Delete Borrar Open post in web browser Abrir publicación no navegador Click to download the attachment Prema para descargar o adxunto Cc Cc Copy post link to clipboard Copiar ligazón da publicación ao portapapeis Normalize text colors Normalizar colores do texto &Close &Pechar Type As in: type of object Tipo Modified on %1 Modificado o %1 Parent As in 'Open the parent post'. Try to use the shortest word! Nai Open the parent post, to which this one replies Abrir a publicación nai, á que ésta resposta Comment verb, for the comment button Comentar Reply to this post. Respostar a esta publicación. Share this post with your contacts Compartir esta publicación cos seus contactos Modify this post Modificar esta publicación Erase this post Borrar esta publicación Join Group Unirse a Grupo %1 members in the group %1 membros no grupo Image is animated. Click on it to play. A imaxe é animada. Prema nela para reproducila. Size Image size (resolution) Tamaño Couldn't load image! Non puiden cargar a imaxen! Attached Audio Audio Adxunto Attached Video Vídeo Adxunto Attached File Ficheiro Adxunto %1 likes this One person %1 lle gusta isto %1 like this More than one person %1 lles gusta isto 1 like 1 Góstame %1 likes %1 Góstame 1 comment 1 comentario %1 comments %1 comentarios %1 shared this %1 = One person name %1 compartíu isto %1 shared this %1 = Names for more than one person %1 compartiron isto Shared once Compratido unha vez Shared %1 times Compartido %1 veces You like this A vostede góstalle isto Unlike Non gostar Like this post Góstame esta publicación Like Góstame Are you sure you want to share your own post? Está certo de quere compartir a súa propia publicación? Share post? Compartir publicación? Do you want to share %1's post? Quere compartir a publicación de %1? &Yes, share it &Sí, compartila &No &Non Unshare post? Deixar de compartir publicación? Do you want to unshare %1's post? Quere deixar de compartir a publicación de %1? &Yes, unshare it &Sí, deixar de compartila WARNING: Delete post? ATENCIÓN: Borrar publicación? Are you sure you want to delete this post? Esta seguro de querer borrar esta publicación? &Yes, delete it &Sí, bórraa Click the image to see it in full size Prema na imaxe para vela a tamaño completo ProfileEditor Profile Editor Editor do Perfil This is your Pump address Este é o seu enderezo Pump This is the e-mail address associated with your account, for things such as notifications and password recovery Este é o enderezo electrónico asociado á súa conta, para cousas como notificacións e recuperación de contrasinal Change &E-mail... Cambiar &Enderezo electrónico... Change &Avatar... Cambiar &Avatar... This is your visible name Este é o seu nombe visible Changing your avatar will create a post in your timeline with it. If you delete that post your avatar will be deleted too. Cambiar o seu avatar creará unha publicación na súa liña temporal con el. Se borra esa publicación tamén se borrará a seu avatar. &Save Profile &Gardar Perfil &Cancel &Cancelar Webfinger ID Webfinger ID E-mail Enderezo electrónico Avatar Avatar Full &Name &Nome Completo &Hometown &Cidade &Bio &Bio Not set In reference to the e-mail not being set for the account Non especificado Select avatar image Seleccione imaxen para avatar Image files Ficheiros de imaxe All files Tódolos ficheiros Invalid image Imaxe non válida The selected image is not valid. A imaxe seleccionada non é válida. ProxyDialog Proxy Configuration Configuración do Proxy Do not use a proxy Non usar un proxy Your proxy username O seu nom de usuario no proxy Note: Password is not stored in a secure manner. If you wish, you can leave the field empty, and you'll be prompted for the password on startup. Nota: O contrasinal non está gardado de xeito seguro. Se o desexa, pode deixar o campo baleiro, e se lle preguntará polo contrasinal no arranque. &Save &Gardar &Cancel &Cancelar Proxy &Type &Tipo de Proxy &Hostname Nome de &Host &Port &Porto Use &Authentication Empregar &Autentificación &User &Usuario Pass&word Contrasina&l Publisher Select Picture... Elixa Imaxe... Find the picture in your folders Atopar a imaxe nos seus cartafois Public Público Followers Seguidores People... Xente... Select who will see this post Seleccionar quen verá esta mensaxe To... Para... Setting a title helps make the Meanwhile feed more informative Escreber un título axuda a que a lista Mentras tanto sexa máis informativa Title Título Remove Eliminar Cancel the attachment, and go back to a regular note Cancelar o adxunto, e voltar a unha nota normal Drafts Borradores Lists Listas Select who will get a copy of this post Seleccione quén recibirá unha copia desta publicación Picture Imaxe Audio Audio Video Vídeo Other as in other kinds of files Outros Ad&d... Enga&dir... Upload media, like pictures or videos Subir medios, como imaxes ou vídeos Hit Control+Enter to post with the keyboard Prema Control+Énter para publicar co teclado Cancel Cancelar Cancel the post Cancelar a mensaxe File not found. Ficheiro non atopado. Picture not set Imaxen non establecida Select Audio File... Seleccionar Ficheiro de Audio... Find the audio file in your folders Atopar o ficheiro de audio nos seus cartafois Audio file not set Ficheiro de audio non establecido Select Video... Seleccione Vídeo... Find the video in your folders Atopar o vídeo nos seus cartafois Video not set Vídeo non seleccionado Select File... Seleccione Ficheiro... Find the file in your folders Atopar o ficheiro nos seus cartafois File not set Ficheiro non seleccionado Error: Already composing Erro: Xa está a compoñer You can't edit a post at this time, because a post is already being composed. Non pode editar a mensaxe neste intre porque xa se está compoñendo outra mensaxe. Update Actualizar It is owned by %1. %1 = a username É propiedade de %1 Editing post Editando publicación You can't create a message for %1 at this time, because a post is already being composed. Non pode crear unha mensaxe para %1 neste intre, porque xa se está a compoñer unha mensaxe. Draft loaded. Borrador cargado. ERROR: Already composing ERRO: Aínda estase a compoñer You can't load a draft at this time, because a post is already being composed. Non pode cargar un borrador neste intre porque agora estase compoñendo outra mensaxe. Draft saved. Borrador gardado. Posting failed. Try again. Publicación fallida. Ténteo outra vez. Warning: You have no followers yet Atención: Vostede aínda non ten seguidores You're trying to post to your followers only, but you don't have any followers yet. Está tentando escribirlle só aos seus seguidores, pero aínda non ten seguidores. If you post like this, no one will be able to see your message. Se publica deste xeito, ninguén poderá ver a súa mensaxe. Do you want to make the post public instead of followers-only? Quere facer pública a publicación en lugar de só para os seus seguidores? &Yes, make it public &Sí, facelo público &Cancel, go back to the post &Cancelar, voltar á publicación Updating... Actualizando... Post is empty. A publicación está baleira. File not selected. Ficheiro non seleccionado. Select one image Seleccione unha imaxe Image files Ficheiros de Imaxe Select one file Seleccione un ficheiro Invalid file Ficheiro inválido The file type cannot be detected. Non se pode detectar o tipo de ficheiro. All files Tódolos ficheiros Since you're uploading an image, you could scale it down a little or save it in a more compressed format, like JPG. Xa que está a subir unha imaxe, podería reducirlle a escala ou gardala nun formato máis comprimido, como JPG. File is too big O ficheiro é demasiado grande Dianara currently limits file uploads to 10 MiB per post, to prevent possible storage or network problems in the servers. Actualmente Dianara limita a subida de ficheiros a 10 MiB por publicación, para previr posibles problemas de almacenamento ou de rede nos servidores. This is a temporary measure, since the servers cannot set their own limits yet. Está é unha medida temporal, xa que os servidores aínda non poden establecer os seus propios límites. Sorry for the inconvenience. Pregámoslle disculpas polas molestias. Error Erro The selected file cannot be accessed: No se pode acceder ao ficheiro seleccionado: You might not have the necessary permissions. Poida que non teña vostede os permisos necesarios. Resolution Image resolution (size) Resolución Type Tipo Size Tamaño %1 KiB of %2 KiB uploaded %1 KiB de %2 KiB subidos Invalid image Imaxen non válida Add a brief title for the post here (recommended) Engada eiquí un título breve para a publicación (recomendado) Cc... Cc... Post verb Publicar Note started from another application. Nota arrancada dende outra aplicación. Ignoring new note request from another application. Ignorando petición de nova nota dende outra aplicación. Editing post. Editando publicación. &No, post to my followers only &Non, publicar só para os meus seguidores The image format cannot be detected. The extension might be wrong, like a GIF image renamed to image.jpg or similar. Non se pode detectar o formato da imaxe. A extensión pode ser incorrecta, como unha imaxe GIF renomeada a imaxen.jpg ou similar. Select one audio file Seleccione un ficheiro de audio Audio files Ficheiros de audio Invalid audio file Ficheiro de audio non válido The audio format cannot be detected. Non se pode detectar o formato do audio. Select one video file Seleccione un ficheiro de vídeo Video files Ficheiros de vídeo Invalid video file Ficheiro de vídeo non válido The video format cannot be detected. Non se pode detectar o formato do vídeo. Posting... Publicando... PumpController Creating person list... Creando lista de persoas... Deleting person list... Borrando lista de persoas... Getting likes... Recuperando Góstame... Getting comments... Obtendo comentarios... Error connecting to %1 Erro conectando con %1 Unhandled HTTP error code %1 Código de erro HTTP %1 non manexado Message liked or unliked successfully. Acción de gostar ou deixar de gostar mensaxe completouse con éxito. Comment %1 posted successfully. %1 is a piece of the comment Comentario %1 publicado con éxito. Message deleted successfully. Mensaxe borrada con éxito. Likes received. Góstame recibidos. Authorized to use account %1. Getting initial data. Autorizad a usar a conta %1. Obtendo datos iniciais. There is no authorized account. Non hay.unha conta autorizada. Updating profile... Actualizando perfil... Getting list of 'Following'... Obtendo lista de 'Seguindo'... Getting list of 'Followers'... Obtendo lista de 'Seguidores'... Getting site users for %1... %1 is a server name Obtendo usuarios do sitio %1... Getting list of person lists... Obtendo lista de listas de personas... Getting a person list... Obtendo unha lista de personas... Adding person to list... Engadindo persona á lista... Removing person from list... Borrando persona da lista... Creating group... Creando grupo... Joining group... Uníndose ao grupo... Leaving group... Abandonando grupo... Getting '%1'... %1 is the name of a feed Obtend %1... Timeline Liña temporal Messages Mensaxes User timeline Liña temporal do usuario Uploading %1 1=filename Subindo %1 Error loading timeline! Erro cargando liña temporal! Error loading minor feed! Erro cargando liña temporal secundaria! Unable to verify the address! Imposible verificar a dirección! HTTP error For the following HTTP error codesyou can check http://en.wikipedia.org/wiki/List_of_HTTP_status_codes in your language Erro HTTP Gateway Timeout HTTP 504 error string Tempo de espera da pasarela agotado Service Unavailable HTTP 503 error string Servizo Non Dispoñible Bad Gateway HTTP 502 error string Pasarela incorrecta Not Implemented HTTP 501 error string Non Implementado Internal Server Error HTTP 500 error string Erro Interno do Servidor Gone HTTP 410 error string Foise Not Found HTTP 404 error string Non Atopado Forbidden HTTP 403 error string Prohibido Unauthorized HTTP 401 error string Non Autorizado Bad Request HTTP 400 error string Mala Petición Moved Temporarily HTTP 302 error string Movido Temporalmente Moved Permanently HTTP 301 error string Movido Permanentemente Server version: %1 Versión do servidor: %1 Profile received. Perfil recibido. Followers Seguidores Following Seguindo Profile updated. Perfil actualizado. E-mail updated: %1 Enderezo electrónico actualizado: %1 %1 published successfully. Updating post content... %1 is the type of object: note, image... %1 publicado correctamente. Actualizando contido da publicación... Untitled post %1 published successfully. %1 is a piece of the post Publicación sen titular %1 publicada correctamente. Post %1 published successfully. %1 is the title of the post Publicación %1 publicada correctamente. Avatar published successfully. Avatar publicado correctamente. Untitled post %1 updated successfully. %1 is a piece of the post Publicación sen título %1 actualizada correctamente. Post %1 updated successfully. %1 is the title of the post Publicación %1 actualizada correctamente. Comment %1 updated successfully. %1 is a piece of the comment Comentario %1 actualizado correctamente. Adding items... Engadindo elementos... Following %1 (%2) successfully. %1 is a person's name, %2 is the ID Seguindo a %1 (%2) correctamente. Stopped following %1 (%2) successfully. %1 is a person's name, %2 is the ID Deixou de seguir a %1 (%2) correctamente. List of %1 users received. %1 is a server name Lista de usuarios de %1 recibida. Person list '%1' created successfully. Lista de personas '%1' creada correctamente. %1 (%2) added to list successfully. 1=contact name, 2=contact ID %1 (%2) engadido á lista correctamente. %1 (%2) removed from list successfully. 1=contact name, 2=contact ID %1 (%2) eliminado da lista correctamente. Group %1 created successfully. Grupo %1 creado correctamente. Group %1 joined successfully. Grupo %1 unido correctamente. Left the %1 group successfully. Abandonou o grupo %1 correctamente. OAuth error while authorizing application. Erro OAuth ao autorizar a aplicación. %1 attempts %1 intentos 1 attempt 1 intento Some initial data was not received. Restarting initialization... Non se recibiron alguns datos iniciais. Reiniciando inicialización... Can't follow %1 at this time. %1 is a user ID Non pode seguir a %1 neste intre. Trying to follow %1. %1 is a user ID Tentando seguir a %1. Checking address %1 before following... Comprobando dirección %1 antes de seguir... List of 'following' completely received. Lista de 'seguindo' recibida completamente. The comments for this post cannot be loaded due to missing data on the server. Os comentarios para esta publicación non se poden cargar porque hai datos que faltan no servidor. Activity Actividade Favorites Favoritos Meanwhile Mentras tanto Mentions Mencións Actions Accións 1 comment received. 1 comentario recibido. %1 comments received. %1 comentarios recibidos. Post by %1 shared successfully. 1=author of the post we are sharing Publicación de %1 compartida correctamente. Received '%1'. %1 is the name of a feed Recibido '%1'. Partial list of 'following' received. Recibida lista parcial de 'seguindo'. List of 'followers' completely received. Lista de 'seguidores' recibida completamente. Partial list of 'followers' received. Recibida lista parcial de 'seguidores'. List of 'lists' received. Recibida lista de 'listas'. Person list deleted successfully. Lista de personas borrada correctamente. Person list received. Lista de personas recibida. File uploaded successfully. Posting message... Ficheiro subido correctamente. Publicando mensaxe... Avatar uploaded. Avatar subido. SSL errors in connection to %1! Erros SSL na conexión con %1! Loading external image from %1 regardless of SSL errors, as configured... %1 is a hostname Cargando imaxen externa dende %1 a pesares dos erros SSL, tal como está configurado... The application is not registered with your server yet. Registering... A aplicación aínda non está rexistrada co seu servidor. Rexistrando... Getting OAuth token... Obtendo o token OAuth... OAuth support error Erro de soporte OAuth Your installation of QOAuth, a library used by Dianara, doesn't seem to have HMAC-SHA1 support. A súa instalación de QOAuth, a libraría empregada por Dianara, non semella ter soporte HMAC-SHA1. You probably need to install the OpenSSL plugin for QCA: %1, %2 or similar. Probablemente necesite instalar o plugin OpenSSL para QCA: %1, %2 ou similar. Authorization error Erro de autorización There was an OAuth error while trying to get the authorization token. Houbo un erro OAuth mentres se tentaba obter o token de autorización. QOAuth error %1 QOAuth erro %1 Application authorized successfully. Aplicación autorizada correctamente. Waiting for proxy password... Agardando por contrasinal do proxy... Still waiting for profile. Trying again... Aínda esperando polo perfil. Tentándoo outra vez... Some initial data was not received after several attempts. Something might be wrong with your server. You might still be able to use the service normally. Algúns datos iniciais non se recibiron tras varios intentos. Algo podería ir mal no seu servidor. Aínda así debería poder usar o servicio con normalidade. All initial data received. Initialization complete. Recibidos todos os datos iniciais. Inicialización completada. Ready. Listo. SiteUsersList You can get a list of the newest users registered on your server by clicking the button below. Pode obter unha lista dos novos usuarios rexistrados no seu servidor premendo no botón de embaixo. More resources to find users: Máis recursos para atopar usuarios: Wiki page 'Users by language' Páxina Wiki 'Usuarios por idioma' PPump user search service at inventati.org Servicio PPump de procura de usuario en inventati.org List of Followers for the Pump.io Community account Lista de Seguidores para a conta de Comunidade Pump.io Get list of users from your server Obter lista de usuario dende o seu servidor Close list Pechar lista Loading... Cargando... %1 users in %2 %1 = user count, %2 = server name %1 usuarios en %2 TimeLine Welcome to Dianara Benvido/a a Dianara Dianara is a <b>Pump.io</b> client. Dianara é un cliente <b>Pump.io</b>. If you don't have a Pump account yet, you can get one at the following address, for instance: Se aínda non ten unha conta Pump, pode obter unha no seguinte enderezo, por exemplo: Press <b>F1</b> if you want to open the Help window. Prema <b>F1</b> se quere abrir a fiestra de Axuda. First, configure your account from the <b>Settings - Account</b> menu. Primeiro, configure a súa conta dende o menú <b> Conta - Axustes</b>. After the process is done, your profile and timelines should update automatically. Unha vez rematado o proceso, o seu perfil e as liñas temporais deberían actualizarse automáticamente. Take a moment to look around the menus and the Configuration window. Tome uns momentos para revisar os menús e a fiestra de Configuración. You can also set your profile data and picture from the <b>Settings - Edit Profile</b> menu. Tamén pode establecer os datos do seu perfil e a imaxe no menú <b>Axustes - Edictar Perfil</b>. There are tooltips everywhere, so if you hover over a button or a text field with your mouse, you'll probably see some extra information. Hai pistas emerxentes por todas partes, de xeito que se move o ratón sobre un botón ou un campo de texto, probablemente verá algo de información extra. Dianara's blog Blog de Dianara Pump.io User Guide Guía de Usuario Pump.io Direct Messages Timeline Liña Temporal de Mensaxes Directas Here, you'll see posts specifically directed to you. Eiquí verá as publicacións dirixidas específicamente a vostede. Activity Timeline Liña Temporal de Actividade You'll see your own posts here. Eiquí verá as súas propias publicacións. Favorites Timeline Liña Temporal de Favoritos Posts and comments you've liked. Publicacións e comentarios que lle gustaron. Newest As máis novas Newer Máis novo Older Máis antigas Requesting... Solicitando... Loading... Cargando... Page %1 of %2. Páxina %1 de %2. Showing %1 posts per page. Amosando %1 publicacións por páxina. %1 posts in total. %1 publicacións en total. Click here or press Control+G to jump to a specific page Prema eiquí ou pulse Control+G para saltar a unha páxina específica '%1' cannot be updated because a comment is currently being composed. %1 = feed's name Non se pode actualizar %1 porque estase a compoñer unha mensaxe neste intre. %1 more posts pending for next update. %1 máis publicacións pendentes para a seguinte actualización. Click here to receive them now. Prema eiquí para recibilos agora. There are no posts Non hai publicacións Timestamp Invalid timestamp! Marca de tempo non válida! A minute ago Hai un minuto %1 minutes ago Hai %1 minutos An hour ago Hai unha hora %1 hours ago Hai %1 horas Just now Agora mesmo In the future No futuro Yesterday Onte %1 days ago Hai %1 días A month ago Hai un mes %1 months ago Hai %1 meses A year ago Hai un ano %1 years ago Hai %1 anos UserPosts Posts by %1 Mensaxes de %1 Loading... Cargando... &Close &Pechar Received '%1'. Recibido: '%1'. %1 posts %1 mensaxes Error loading the timeline Erro cargando a liña temporal dianara-v1.4.1/translations/dianara_he.qm0000664000175000017500000032702113202447474016555 0ustar janjanc3<<+q>Rh@)DBjH3H:*HH,VIa.KLbi-Lb0MeBNWNOSP7P7R`S TJ!T TU_m%VFV* V*"VRV7WjMWLY*XyXƥ" YZy%4I[ %C[!C\z\E'j  knq׎Pr$#!wj^yT7yb^zz>^4hbN%cM>G{}K4^ .f"4E̸)yՅEI5 SҥjH~7[* B:KNX|_%^d2ݣs3J$Y,&-!)0ET<#$<]kDInNM$MP3PkQE0jRSV8\qhl{op`Nm=#,/ qTrVh>g/^1t&L RA;% 2S  ": UT*ʑ$%g%T]3WX>BKbL=QlRxN&d.%hB\$pH4stɥ7v2^w=PSo{J(TZ^{ jt 6w6 6q6я666w  B fd] nMO!ƭ>_!Z4sn0sVϠN']qTM؞ddyV\|r5S~3E8Y/ҹ6xX#wJOX  1 j}ZmZ vS2$1!$1(J.sB0cM60c66U6'V(70HZEA9M.>XR@ZNZ(di6kijCdl#psC wh3?x2~)zjf{e}uRz-.Qn&JNnRs^dBd1b;¿Y¿ÌP^yjn˺>*q!Iq޷h?l_ږ#n  qr-n%=?<ʳ^ 1( SI/tg  f#n$H|+h6U>B(mP`3 Sa$WfXr)|}xwq::`+=7!5<& & bvU>]>I#kIryI-x6>iu, ,<3$xn}_ԁa^n3cc[u2mE44ͣw3g3gaOրvR{?@zz-7_WwNF 1<#,X(L4Y>1CKTKTc=LܤRMRRS1V|2]L.]H3aIbcCfy"+h9~I;hTRjys|u_yz{9{I|7}̏_',.{i0F9VV_Ah>@e4L>T&ctDtGs %:34?6ú_Xi_\wi0"0 ƨD324oni:q9rNPGأMzw*v+ -^a{4uGn4u!4u69$9<.J<..<.a}=AtC?Gmb?=IM?$c?ORQ|T$VхbZPZzZf;2 fg hCmn]p{.r7 u6By#sa/\k dNn%S9NXC,1̐;vDCIuVe"4 ґ`ڑYڱ}kM1sS8 td^-'Ch#D,0?1;3;XY<FO<2<!<6SPWsOcgj lqN+Upr\t' {k{-BNojž` oWnfY/INM+i"1.nQ Q i=MYnIE "#N)2K3T2?,6~~%FMJMJMrc\]ael:e7lkZ#s#9snk|2^|`PP]UV'gtejB>f)sBP/q:K`Jrn7ť+ʪ$ѿ] 琊',QysRbPNu. m !@ -Q 0\~ް 0u~U 1`#;t 6 17 7w0 <.g Mg P*] SgXm ^n r%j sx w>sJ | ~4 z t*/ mf 41 Q C~N ~; E  Nd ` P  o d_~ b}%  fZy+ ʞ U pB6 &hE ~_ Ԭ [O t*y SF hC N] A _ A n ~~s n$ o% % !/S #Z! (O\ ?mӂ @=c @Cca EeY Hkr Tuk, TGb TGz [dp ](q c6 d 4 d< d<$ d< dk1 e pO/ q\-n v'\ z3. _> .a G[ K I I I;@ IxC I{ I I I I% ) ~p I LY  y NJ ,kQ o$5 D_ t? Pa ^ '_ t; ՁBP 3Gf0 ֓* N %; T c ؄- v 0n Y+6 :$ | pc >| ~yy ~ ~ 9U c4 U \e "e" " ,i4 -֮ 0m 0S 5<_$ 7"A; <خv =n =/ngi @] A# ED j XCn Ya( bX%2 bX0 b>@ eT) f( f*29 mmTR o> y[>K |6( P/ !S q3JO S?p #R  H Vd j ' 5j * )T h U ! sd] 5- je OCx أ y q L C= ݙ 2 Ғ > (= - U /J 66 =xc BSx Ex RVoa Y; [ca@ ]ӡ% `u d.Lk eMU) j8z2 {W {# B! .H  JS rw V xbp r q #k s ͡ =; (y Б* ۅyo KU <T yqx |SD< 7~g < w2 7 a =:  Е #pn@ )R .d B G$ Nnu V+ _PD b e&N gH h1a iFCU 1j @ | LP ic  >} »P ^[ q? ˽+0! 71E  b"d + ' . ` V83N2qTĜcQX+j@!/!gDR/.uRT3BXەc?gO=KlNcyl<lql@ll,mhn'm\[K$8T<8Tw8T8T^>`Q'*NiT{K'וZ^(U~A%<FJC!.|U;O.ycR #up`^_^niz8'5%'5%P'5%'EdD)+<,0A aBtneB}~8CtE.FNJIPdiT