pax_global_header00006660000000000000000000000064134705424750014525gustar00rootroot0000000000000052 comment=ff6b84645ad76f18ed0a2dbfe41a144330c10b88 piperka-client-0.2.2/000077500000000000000000000000001347054247500144355ustar00rootroot00000000000000piperka-client-0.2.2/CHANGES000066400000000000000000000014401347054247500154270ustar00rootroot00000000000000* Mon May 20 2019 Kari Pahula 0.2.2-1 - Test and adapt generic Qt for Android - Fix update page sort type combobox - Generic: Fix previous page button enable condition * Sun May 12 2019 Kari Pahula 0.2.1-1 - Add generic Qt version - Fix update stats cleanup after logout - Fetch bookmarks after login * Wed Apr 24 2019 Kari Pahula 0.2-1 - Add French translation - Recommendations - Work around for page changes in webview for archives based on fragments * Mon Mar 25 2019 Kari Pahula 0.1.1-1 - Remove Core class from Application (Sailfish already set the correct paths without). - Fix emitting fetchSubscriptionEnd when doing sync with unlogged user and no bookmarks. * Mon Mar 25 2019 Kari Pahula 0.1-1 - First release piperka-client-0.2.2/COPYING000066400000000000000000000432541347054247500155000ustar00rootroot00000000000000 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. piperka-client-0.2.2/generic/000077500000000000000000000000001347054247500160515ustar00rootroot00000000000000piperka-client-0.2.2/generic/android/000077500000000000000000000000001347054247500174715ustar00rootroot00000000000000piperka-client-0.2.2/generic/android/AndroidManifest.xml000066400000000000000000000147631347054247500232750ustar00rootroot00000000000000 piperka-client-0.2.2/generic/android/build.gradle000066400000000000000000000027601347054247500217550ustar00rootroot00000000000000buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.2.0' } } repositories { google() jcenter() } apply plugin: 'com.android.application' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) } android { /******************************************************* * The following variables: * - androidBuildToolsVersion, * - androidCompileSdkVersion * - qt5AndroidDir - holds the path to qt android files * needed to build any Qt application * on Android. * * are defined in gradle.properties file. This file is * updated by QtCreator and androiddeployqt tools. * Changing them manually might break the compilation! *******************************************************/ compileSdkVersion androidCompileSdkVersion.toInteger() buildToolsVersion androidBuildToolsVersion sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] res.srcDirs = [qt5AndroidDir + '/res', 'res'] resources.srcDirs = ['src'] renderscript.srcDirs = ['src'] assets.srcDirs = ['assets'] jniLibs.srcDirs = ['libs'] } } lintOptions { abortOnError false } } piperka-client-0.2.2/generic/android/res/000077500000000000000000000000001347054247500202625ustar00rootroot00000000000000piperka-client-0.2.2/generic/android/res/drawable-ldpi/000077500000000000000000000000001347054247500227715ustar00rootroot00000000000000piperka-client-0.2.2/generic/android/res/drawable-ldpi/icon.png000066400000000000000000000356141347054247500244400ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYsuu?YXtEXtSoftwarewww.inkscape.org< IDATxyT_-o44 5cd̞N'yߘI,&$.IT%(j2ApAٺꮪ t7]9us'W])9󜪊e٬S# " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " "ࠤurЊkʷ|Zռ\7~-Yқ>Et6:M(+"+e( ?i)] B ܖΞU @::p-^1_ooxGﵭ dZ*3J'zRW[ 4o]f9&6Vi֋ˋfiEZں\+6*˷#&ҳn Hw+}S<^|k^_@eVkW+ >XD<ުn=9A<-~k;lhZo'q&="@lڠ>yy>~lؠ<{+Ǥ(? v=>v==zu-[JIev.ڬ*M'}ϱ(B=CsV*UXW/ɜɽDū>;= ܼzޓucXtcl 3_׿e:*ߚuc)t#i{nkxz6lHU}ϱy=}E}`rc9n# R[Zuݍ_״wiUCgd?n4ܞ=ǀh7]%q?'u&nc\?NlZ) (ܽcP9=ov@ɤӺ斯%ju}''J3f9&! 0zF}[[YIɽ'}ϱ͟~gLRecructc_z3kW-7_Q'kNn4=\&Oo~ޭT.brd9&"|oVƺĺh{b"$V-K_ =\PY.sdzsL,F9DL޳nn웾% _o8K| ZW(n4nc?X65Ymr)a] J>A|}N֍ɿ>X #=v3&wX7VE7IOH~q:/jSuƺ o֍ғX%?8]gU~z2grf9&EpXK(U*brϝuCXľ+Q-߱.E+XUX&VXʷ_*w UhYIOlvx^[} U*W{hB??̓ mKx@/]sMS%H5(*@(?@gɊ+MnlѻqR[g?+H=) j )aTEu逊>/j.5@ulؠ8CM}ICIwZ]S>Ysz|&K襗j%9?~ϥn(cI%uvٺ258!ѻY@j?7y;ƾCQƚMΠ+S6F jl=}|U;^(cGVY6ǿEmw=zӟVgfHVo80ҨWg|v rz18A `R5/^1[j(~Ȳr5(>h6Fׅ4۴htϟw﷤fUGAWhpbpQéff@@l|]=oMk"yϷ!<W\4\/*mC&ѻ]F f2~bTo.K:u_)XEB_UM\s0ލc96!~y$c%,}1)eD|&VNtvg6"[z?(Ǽ(]tgސB0ލmJ`(٩G.P]&PmҀ&KW vɟm 8A(K?̝kZð[Vx#uU*y\FƱ` !i^H/[!I,}AJ7)u_E mB&ƱmnoLF}f{_-Ł՝S <0ލmF00g!&iҳҧ:㬆tnùlwml+ZKߐxTs.Д)[?`Ʊmn _{Tjf25j+ysՉ'pwX6z7PJZ׿j}YMҗ+fqssuZi&ѻ]F |`S)=[tғuXߠV^^ʞ_q7v1:cmrQ;wjmmyb)TYYhp'ס}_.P {0Y",p AW[^(ʣV;WP3աTZGdՙ ;X2%b9=6,:o&-%??! >x헿TXỲѣUw%[P>eú5p2ߞ?=~> - /T&TW6![6qn\.)xAT^k])'#‚%Do]EF^pAQ e"e3|5eo,DAkMOBdwU"ԤN.cLC8>6ub),XLb&B(@T_WtϺ &FK3@֮[?l]q՜{nɇe‚58\E[;:\Ja]GD, Jf5۬\Jga7>E7\K/y"2T*+(hM xVex*VUɓ &A;3f(b]N8Aj2z$kAk`<`]bee>D2Ƅ? P @?2zGT1(`].1ZК8=:6l.;BI& EGNOe&NcMCY} ‚% `V򊶬Yc]g.,8\B؅w{̺OUw ~s'@ ]xwLHc8\FCZ+ex⠃01 "X2@̙ʦexK&WFO`]Fјa%D ˞zʺTqD(/J%jM\zi22@ (:FYDN4b>Xe])~@$wݺ 4w)+P!f-.KR$%)9ru @$("Hڸtu E+9tu@D $m~}hj九ź: C\dq^u@"HH6.Wźh\tS[?07qKtGZ׬.h[#/"Y &NX@+VX CK_eu4<غ 4jAZuөfum ;vVV:2%/W }/ӡLgcf:K<֮.hFu3KB) M8p h]$*0#[g]Bbu\*jM\9:6n.hqPQ p yu E#ʼnZ' .p "*8.0 V,O-hM8ە E**KB'jM|9%x3wi e]'8&M`b 5q;@(X8a D% M998bat ؁@<"u SX2@IXW M]q: Ē3,eȦR%’tH[lD&N@eՊECt2$X2@#SYMuE P5qrvT^[k]BѲmm%&YxPii.5q |(.h͛K"+hM" oQkP5xu E 5q/ƍ%&N0@UCXPlsu @@$P @$RV@`ah4vx3^κY, #FX5%FkFY%`Q|(SECuE"yq+I9Һq(- vIQ+WZM.#(g2--ʬ_o]Y.,-I {i]':ߺ t~IDAT @.˭K"%D @Rq%x3@A @R^{)z=H Z,dUU$>kjeN_QkM$f9 x,hM%xwKB!jM|2qu |mP C mvJXDdB!M`bx'@M]˖YZК8~HZe550v/Yb]JRo{X r*PBxY^bfz_J5!'F"tteBjh.&NcCC<кO|S.pK#v~u vt(3@)v~x$.K" " `;q85/Zd]J:SyY xEжOXx ;H˕^NAK"Mqb*%Uׇn ;/b9?<|^rAI,Ј}L̘a]J2u̙ܺ3e˖"dorQJ741s(eK* dS&L幣H,jqY_.\z1j$@íK#JM<%,jb%b {vu j5˺ X+}0vui9"I8"žb]g2--|405q;@؅(Ij{ s',]uJVVZԂZܺ pi`kk5TSOYDK#@?>l+HX->9sJ"hM/<4pオXlVn F䄹PǎH_ IjmozDN8;KK.غ u.^ֻ.(X8a^KUGZ+JYvmO>Q8X˒v)=`R{i]F^2>&9|BObWqSQYACFAhl])GK/՜o.wwީ+&4HA\rh"!Mܫq*jTYUeiu XW]lg6|_BuvuqmniӔ0 ź?Šþ-Տe]FI.U7Ju)@hpGɪ*_e]FtΝӭ|Œ _C.h}1sf B'?qMϭ({Qg|ÒP&M GlVm+Ey@A\hJ,'L#V}Qƍ֥-6^Qo}Kq|kjjj]4tsz9ڵkbŊ^X+ cSY}p}F(s=·bb7{oD΢8q5{,Сore䥘/IЖiӔy]jD~va ﱍaYɤNzɵ_6]{2KZW]pug=NƎ@}?ߺxz?FeSbF 0ލmt Nf]+g?6m.INlcp . @:w"Kـo?TvR~0ލc96a!ud]]V]?2/\3PLmnlE?u::Fen]Jq ?.lcTFgNdUu)@٧Vk͛)y`8c!7{kק]@}O p?1 t.к MkUvt)) 9sag&nP'8~|+2dҌ5H=Eragۅm"@@c`0'=u%ƅɟmn˱ ;"Ѓҩb]F]W^s}@\FƱ`@HFpu9>_Nܢo,PM.48BxYΜ>]߿|~[\V|BrBDžɟmn0l!VبgT{X%TuJ,YR߅6/>\?j-֧8ːU}ժj͝59Bc'Jи^:U^[vZ %KT5mjJe?/uuY .LlwcK;Lgw֥xg˖[Tortׯ/ȊBc;)茻VϏm 8_4IÏ?u ޸dv? 6RGCcxeK^\F6矯du>@vΛg\;]U%%TU}t6P]>Nۦ\1wޖ]gȃ|&konփg=')ҫ -"[=[:`*e@8PSWsE?ɤړXtc PBqOǙ`X9&ݬ9aCVwtw+YUe]M֓8{Oc;<׿fPI:tc"JaQGgVӑGZRR &ѓX U:5 _m ɜɽG@|:ەLųnns~S}wRfXYs#r2#uk)SzwIɽ'}ϱƾ@AtCiӔe4Lhzs,&'/?cmJحˬ=(r!:{&ng; VРGxェ4ȺOY7VF7}1ATP1睧kT$zs,"MM:塇tM7=֍cэ} XL|:g\ ?Dj>bXi=Y{@ƌi?n]1 G,.D̟_l]*'}ϱ@૪&};tf(Ϟz2grf9&@a(g.ԁLnG3J&Y]JgΟn]N+M ( &OsOIɽ>XCQ眣3-7ݤ!C1aXiاp ee/EwxE/X7VCOc卍:'?є5m܋gX9&@0(u{cGuyR,<ºh{b}# &Lޫ^|Q!ɝɾ# ~>:~,5t$&wɾhOVcq?/V,(ɸ֍zs,5ʀwܡS-^]Ķw 0{Ϻr,Jc)k+PYcuIn4ܞwpV=jOuoW3֍fzsL2"!QYQ\я=)Sv}CfY"?޿N-&myݒ67m-}=[:`*ytZΜܢ5//ٳik D,mj(-B@Tܹ[j˼yR*%F[JSm uܭ` https://download.qt.io/ministro/android/qt5/qt-5.9 piperka-client-0.2.2/generic/piperka-client.desktop000066400000000000000000000002351347054247500223530ustar00rootroot00000000000000[Desktop Entry] Type=Application Icon=piperka-client Exec=piperka-client Name=Piperka Client Categories=Network;Feed;Qt; Keywords=mobile;web comic;webcomic; piperka-client-0.2.2/generic/piperka-client.pro000066400000000000000000000033001347054247500214760ustar00rootroot00000000000000TARGET = piperka-client SOURCES += src/piperka-client.cpp \ ../src/comic.cpp \ ../src/download.cpp \ ../src/user.cpp \ ../src/subscription.cpp \ ../src/updates.cpp \ ../src/browse.cpp \ ../src/application.cpp \ ../src/page.cpp \ ../src/sortmanager.cpp \ ../src/passwordvalidator.cpp \ ../src/recommend.cpp \ src/platform.cpp DISTFILES += \ android/AndroidManifest.xml \ android/build.gradle \ android/res/values/libs.xml \ qml/BrowsePage.qml \ qml/BrowseItem.qml \ qml/ReaderPage.qml \ qml/AllReadPage.qml \ qml/LoginPage.qml \ qml/NewAccountPage.qml \ qml/PageDetailPage.qml \ qml/RecommendPage.qml \ qml/UpdatesPage.qml \ qml/MainPage.qml \ qml/ForceLogout.qml \ qml/NetworkErrorPage.qml \ qml/NsfwMarker.qml RESOURCES = piperka-client.qrc TRANSLATIONS += \ translations/piperka-client-fi.ts HEADERS += \ ../src/comic.h \ ../src/download.h \ ../src/user.h \ ../src/subscription.h \ ../src/updates.h \ ../src/browse.h \ ../src/application.h \ ../src/page.h \ ../src/sortmanager.h \ ../src/passwordvalidator.h \ ../src/recommend.h \ src/platform.h VERSION = $$system("grep -E '^\*' ../CHANGES | head -n 1 | sed -r 's/^.+ ([0-9.]+\.[0-9]+).*/\1/'") DEFINES += APP_VERSION=\\\"$$VERSION\\\" CLIENT_NAME=\\\"GenericPiperka\\\" QT += network qml quick webview contains(ANDROID_TARGET_ARCH,armeabi-v7a) { ANDROID_PACKAGE_SOURCE_DIR = \ $$PWD/android } contains(ANDROID_TARGET_ARCH,arm64-v8a) { ANDROID_PACKAGE_SOURCE_DIR = \ $$PWD/android } contains(ANDROID_TARGET_ARCH,x86) { ANDROID_PACKAGE_SOURCE_DIR = \ $$PWD/android } piperka-client-0.2.2/generic/piperka-client.qrc000066400000000000000000000012111347054247500214620ustar00rootroot00000000000000 qml/MainPage.qml qml/NetworkErrorPage.qml qml/LoginPage.qml qml/NewAccountPage.qml qml/PageDetailPage.qml qml/BrowsePage.qml qml/UpdatesPage.qml qml/RecommendPage.qml qml/ReaderPage.qml qml/NsfwMarker.qml qml/ForceLogout.qml qml/BrowseItem.qml qml/AllReadPage.qml translations/piperka-client-fi.ts piperka-client-0.2.2/generic/qml/000077500000000000000000000000001347054247500166425ustar00rootroot00000000000000piperka-client-0.2.2/generic/qml/AllReadPage.qml000066400000000000000000000026511347054247500214620ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 Item { width: appWindow.width height: appWindow.height Button { id: back text: "<" onClicked: pageStack.pop() anchors { left: parent.left top: parent.top } visible: platform.explicitBackButtons } Item { anchors.fill: parent Label { anchors.centerIn: parent text: qsTr("All caught up!") wrapMode: Text.WordWrap } } } piperka-client-0.2.2/generic/qml/BrowseItem.qml000066400000000000000000000044251347054247500214420ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 MouseArea { id: delegate property bool bookmarkFirst: true width: appWindow.width height: subscribeStar.height*2 Label { id: subscribeStar visible: subscribed text: "★" anchors.verticalCenter: parent.verticalCenter } Label { x: subscribeStar.width text: title anchors.verticalCenter: parent.verticalCenter style: contextMenu.opened ? Text.Sunken : Text.Normal } onPressAndHold: { contextMenu.popup() } onClicked: { pageModel.loadComic(cid) pageStack.push(Qt.resolvedUrl("ReaderPage.qml")) } NsfwMarker { visible: nsfw } Menu { id: contextMenu width: onPiperka.width modal: true MenuItem { text: qsTr("Subscribe") visible: !subscribed height: visible ? implicitHeight : 0 onClicked: user.subscribe(cid, bookmarkFirst) } MenuItem { text: qsTr("Unsubscribe") visible: subscribed height: visible ? implicitHeight : 0 onClicked: user.unsubscribe(cid) } MenuItem { id: onPiperka text: qsTr("View entry on Piperka") onClicked: Qt.openUrlExternally("https://piperka.net/info.html?cid="+cid) } } } piperka-client-0.2.2/generic/qml/BrowsePage.qml000066400000000000000000000115231347054247500214150ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 import "qrc:/qml" ListView { width: appWindow.width height: appWindow.height headerPositioning: ListView.PullBackHeader ScrollBar.vertical: ScrollBar {} Component.onCompleted: { browseModel.filterSubscribed = false; browseModel.sortType = 0; browseModel.setSearch("") pageModel.autoBookmark = false; pageModel.autoSwitch = false; if (!user.browseHelpSeen) { helpPopup.open() } } BusyIndicator { anchors.centerIn: parent running: user.loading } header: Rectangle { id: myHeader width: parent.width height: headerColumn.height z: 2 Column { id: headerColumn width: parent.width Row { spacing: 2 Button { text: "<" onClicked: { pageStack.pop() } visible: platform.explicitBackButtons } Button { text: qsTr("Options") onClicked: { optionsPopup.open() } } } Item { height: searchInput.height width: parent.width Label { id: searchLabel anchors { left: parent.left rightMargin: 4 verticalCenter: parent.verticalCenter } text: qsTr("Search") } TextField { id: searchInput anchors { left: searchLabel.right right: parent.right } onEditingFinished: browseModel.setSearch(text) } } } Label { anchors.right: parent.right text: qsTr("Browse comics") } } model: browseModel delegate: BrowseItem { bookmarkFirst: bookmarkFirstSwitch.checked } Popup { id: optionsPopup width: appWindow.width*5/6 x: appWindow.width/6 modal: true focus: true Column { width: parent.width Text { text: qsTr("Options") } ComboBox { id: sortType currentIndex: 0 width: parent.width model: ListModel { ListElement { text: qsTr("Alphabetical") } ListElement { text: qsTr("Most popular") } ListElement { text: qsTr("Date added") } ListElement { text: qsTr("Most recently updated") } } onCurrentIndexChanged: { browseModel.sortType = sortType.currentIndex } } CheckBox { id: bookmarkFirstSwitch text: qsTr("Bookmark first page") checked: true } CheckBox { id: showOnlySubscribed objectName: "browseSubscribed" text: qsTr("Show only subscribed comics") onClicked: browseModel.filterSubscribed = checked } } } Popup { id: helpPopup width: appWindow.width*2/3 x: appWindow.width/6 modal: true focus: true Text { text: qsTr("Press and hold a comic to see more options.") width: parent.width wrapMode: Text.WordWrap } onClosed: user.browseHelpSeen = true } } piperka-client-0.2.2/generic/qml/ForceLogout.qml000066400000000000000000000027341347054247500216130ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 Item { width: appWindow.width height: appWindow.height Button { id: back text: "<" onClicked: pageStack.pop() anchors { left: parent.left top: parent.top } visible: platform.explicitBackButtons } Label { anchors.centerIn: parent width: appWindow.width text: qsTr("The server didn't recognize your user session. You have been logged out. Please try logging in again.") wrapMode: Text.WordWrap } } piperka-client-0.2.2/generic/qml/LoginPage.qml000066400000000000000000000057631347054247500212350ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 Item { id: dialog signal login() width: appWindow.width height: appWindow.height onLogin: { user.login(nameField.text, passwordField.text, rememberField.checked, importBookmarks.checked) pageStack.pop() } Column { id: column width: parent.width spacing: 5 Item { width: parent.width height: back.height Button { id: back text: "<" onClicked: pageStack.pop() visible: platform.explicitBackButtons } Text { anchors.right: parent.right text: qsTr("Login") } } TextField { id: nameField width: parent.width inputMethodHints: Qt.ImhNoAutoUppercase placeholderText: qsTr("User name") text: user.storedLoginName validator: RegExpValidator { regExp: /.{2,}/ } onAccepted: passwordField.focus = true } TextField { id: passwordField echoMode: TextInput.Password width: parent.width placeholderText: qsTr("Password") validator: RegExpValidator { regExp: /.{2,}/ } onAccepted: dialog.login() } CheckBox { id: rememberField text: qsTr("Remember me") width: parent.width checked: user.rememberMe } CheckBox { id: importBookmarks width: parent.width text: qsTr("Import bookmarks") visible: user.localBookmarks() } Text { visible: user.localBookmarks() text: qsTr("Importing bookmarks will overwrite the ones on server. Not importing will discard local bookmarks.") } Button { id: loginButton text: qsTr("Login") enabled: nameField.acceptableInput && passwordField.acceptableInput onClicked: dialog.login() } } } piperka-client-0.2.2/generic/qml/MainPage.qml000066400000000000000000000223251347054247500210420ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 ApplicationWindow { id: appWindow visible: true title: "Piperka" BusyIndicator { id: busy anchors.centerIn: parent running: user.loading || updatesModel.subscriptionFlag } Connections { target: user onLoginFailed: { pageStack.push(Qt.resolvedUrl("LoginPage.qml")) } onCreateAccountNameReserved: { pageStack.push(Qt.resolvedUrl("NewAccountPage.qml")) } onNetworkError: { pageStack.push(Qt.resolvedUrl("NetworkErrorPage.qml")) } onForceLogout: { pageStack.push(Qt.resolvedUrl("ForceLogout.qml")) } onSilentSyncFailureChanged: { syncFailedLabel.text = user.silentSyncFailure == 1 ? qsTr("The last scheduled sync failed. The client will retry hourly.") : user.silentSyncFailure > 1 ? qsTr("Last %L1 scheduled syncs failed. The client will retry hourly.").arg(user.silentSyncFailure) : ""; } } // Switching this causes flicker, see https://bugreports.qt.io/browse/QTBUG-53716 visibility: pageStack.currentItem.makeFullScreen === true ? ApplicationWindow.FullScreen : ApplicationWindow.AutomaticVisibility StackView { id: pageStack initialItem: mainFlickable focus: true Keys.onBackPressed: pop() Flickable { id: mainFlickable contentWidth: parent.width contentHeight: mainColumn.height Column { id: mainColumn width: appWindow.width spacing: 5 Text { id: header text: "Piperka" anchors.right: parent.right Connections { target: user onLoggedChange: { if (user.name) { header.text = user.name + " — Piperka" } else { header.text = "Piperka" } } } } Row { width: parent.width height: loginButton.height spacing: 10 Button { text: qsTr("Options") onClicked: optionsPopup.open() } Button { visible: !user.logged id: loginButton text: qsTr("Login") onClicked: pageStack.push(Qt.resolvedUrl("LoginPage.qml")) } Button { visible: !user.logged text: qsTr("Create account") onClicked: pageStack.push(Qt.resolvedUrl("NewAccountPage.qml")) } Button { visible: user.logged text: qsTr("Logout") onClicked: logoutPopup.open() } } Button { enabled: !user.loading anchors { left: parent.left right: parent.right } text: qsTr("Browse comics") onClicked: { pageStack.push(Qt.resolvedUrl("BrowsePage.qml")) } } Button { anchors { left: parent.left right: parent.right } text: qsTr("Recommendations") + ((user.recSubscriptions && user.logged) ? "" : " *") onClicked: { if (user.recSubscriptions && user.logged) pageStack.push(Qt.resolvedUrl("RecommendPage.qml")) else recommendPopup.open(); } } Button { enabled: !updatesModel.noUnread && !user.loading anchors { left: parent.left right: parent.right } text: qsTr("Updates")+ (!updatesModel.noUnread ? (" ("+updatesModel.unreadPages+" / "+updatesModel.rowCount()+")") : "") onClicked: pageStack.push(Qt.resolvedUrl("UpdatesPage.qml")) } Button { enabled: !updatesModel.noUnread && !user.loading anchors { left: parent.left right: parent.right } text: qsTr("Quick read") onClicked: { pageStack.push(Qt.resolvedUrl("UpdatesPage.qml")) pageModel.loadComic(updatesModel.firstCid()); pageModel.autoBookmark = true; pageModel.autoSwitch = true; pageStack.push(Qt.resolvedUrl("ReaderPage.qml")) } } Label { width: parent.width wrapMode: Text.WordWrap text: user.noSubscriptions ? qsTr("Select comics to read from the browse comics page.") : qsTr("You have no unread comics. Wait for updates or subscribe to more comics.") visible: updatesModel.noUnread && !updatesModel.subscriptionFlag && !user.loading } Label { id: syncFailedLabel width: parent.width wrapMode: Text.WordWrap text: "" visible: user.silentSyncFailure > 0 } } } pushEnter: Transition { PropertyAnimation { property: "opacity" from: 0 to: 1 duration: 200 } } pushExit: Transition { PropertyAnimation { property: "opacity" from: 1 to: 0 duration: 200 } } popEnter: Transition { PropertyAnimation { property: "opacity" from: 0 to: 1 duration: 200 } } popExit: Transition { PropertyAnimation { property: "opacity" from: 1 to: 0 duration: 200 } } } Popup { id: recommendPopup modal: true focus: true x: appWindow.width/4 width: appWindow.width/2 contentItem: Text { width: parent.width text: qsTr("Log in or create account and subscribe to at least 5 comics to get recommendations.") wrapMode: Text.Wrap } } Popup { id: optionsPopup modal: true focus: true x: appWindow.width/6 width: appWindow.width*2/3 contentItem: Column { anchors.verticalCenter: parent.verticalCenter Label { text: qsTr("Synchronize") } Button { text: user.syncAvailable ? qsTr("Synchronize now") : qsTr("Please wait") onClicked: user.syncNow(true); enabled: user.syncAvailable && !user.loading } } } Popup { id: logoutPopup modal: true focus: true x: appWindow.width/4 width: appWindow.width/2 contentItem: Column { width: parent.width Text { text: qsTr("Confirm logout") } Button { text: qsTr("Logout") onClicked: { logoutPopup.close() user.logout(); } } } } } piperka-client-0.2.2/generic/qml/NetworkErrorPage.qml000066400000000000000000000033711347054247500226210ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 Item { id: networkErrorPage width: appWindow.width height: appWindow.height Item { width: parent.width anchors { left: parent.left top: parent.top } Button { text: "<" onClicked: pageStack.pop() visible: platform.explicitBackButtons } } Column { anchors.centerIn: parent width: parent.width Label { text: qsTr("Network failure") width: parent.width wrapMode: Text.WordWrap } Label { visible: user.networkErrorMessage != "" text: qsTr("Detail")+": " + user.networkErrorMessage width: parent.width wrapMode: Text.WrapAtWordBoundaryOrAnywhere } } } piperka-client-0.2.2/generic/qml/NewAccountPage.qml000066400000000000000000000070141347054247500222220ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 import net.piperka 0.1 Item { id: dialog signal create() width: appWindow.width height: appWindow.height onCreate: { if (nameField.acceptableInput && passwordField.acceptableInput && passwordFieldAgain.acceptableInput) { user.createAccount(nameField.text, emailField.text, passwordField.text, rememberField.checked) pageStack.pop() } } Label { anchors { right: parent.right top: parent.top } text: qsTr("Create account") } Column { width: parent.width spacing: 5 Button { id: back text: "<" onClicked: pageStack.pop() visible: platform.explicitBackButtons } Label { text: qsTr("Account name reserved. Please try again.") visible: user.storedCreatePassword != "" wrapMode: Text.WordWrap } TextField { id: nameField width: parent.width inputMethodHints: Qt.ImhNoAutoUppercase placeholderText: qsTr("User name") validator: RegExpValidator { regExp: /^.{2,}/ } onAccepted: emailField.focus = true } TextField { id: emailField width: parent.width inputMethodHints: Qt.ImhNoAutoUppercase placeholderText: qsTr("email (optional)") text: user.storedCreateEmail onAccepted: passwordField.focus = true } TextField { id: passwordField width: parent.width placeholderText: qsTr("Password") validator: RegExpValidator { regExp: /^.{4,}/ } objectName: "newAccountPassword" text: user.storedCreatePassword onAccepted: passwordFieldAgain.focus = true } TextField { id: passwordFieldAgain width: parent.width placeholderText: qsTr("Retype password") validator: PasswordValidator { } text: user.storedCreatePassword onAccepted: dialog.create() } CheckBox { id: rememberField width: parent.width text: qsTr("Remember me") checked: user.rememberMe } Button { id: newAccountButton text: qsTr("Create account") enabled: nameField.acceptableInput && passwordField.acceptableInput && passwordFieldAgain.acceptableInput onClicked: dialog.create() } } } piperka-client-0.2.2/generic/qml/NsfwMarker.qml000066400000000000000000000021571347054247500214410ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 Label { anchors { right: parent.right top: parent.top rightMargin: 5 } color: "red" text: "NSFW" font.pixelSize: 8 } piperka-client-0.2.2/generic/qml/PageDetailPage.qml000066400000000000000000000132371347054247500221570ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 ListView { property bool makeFullScreen: true width: appWindow.width height: appWindow.height headerPositioning: ListView.PullBackHeader ScrollBar.vertical: ScrollBar {} id: pageList model: pageModel currentIndex: -1 header: Rectangle { id: myHeader width: parent.width height: headerColumn.height z: 2 Label { anchors { right: parent.right top: parent.top } text: qsTr("Details") } Column { id: headerColumn width: parent.width Button { id: back text: "<" onClicked: pageStack.pop() visible: platform.explicitBackButtons } Item { width: 1 height: 5 } Label { text: qsTr("Page URL")+":\n" + pageModel.cursorUri width: parent.width wrapMode: Text.WrapAnywhere } Item { width: 1 height: 5 } Row { width: parent.width spacing: 5 Button { id: inBrowser text: qsTr("Open in browser") onClicked: Qt.openUrlExternally(pageModel.cursorUri) } Button { text: qsTr("Homepage") onClicked: Qt.openUrlExternally(pageModel.homepage) } } Label { text: qsTr("Jump to...") anchors.right: parent.right } Row { width: parent.width spacing: 5 Button { text: qsTr("Currently viewing") onClicked: { pageList.positionViewAtIndex(pageModel.cursor.row, ListView.Center) } } Button { text: qsTr("Subscribed") enabled: pageModel.subscription.valid onClicked: { pageList.positionViewAtIndex(pageModel.subscription.row, ListView.Center) } } } Item { width: 1 height: 5 } Label { text: qsTr("Subscription controls") anchors.right: parent.right } CheckBox { text: qsTr("Move bookmark on navigation") checked: pageModel.autoBookmark onClicked: pageModel.autoBookmark = checked } } } delegate: MouseArea { id: delegate width: parent.width height: 2 * ordText.height Row { Label { id: ordText width: 60 text: currentMarker ? "" : (1+ord) font.bold: cursor anchors.verticalCenter: parent.verticalCenter style: contextMenu.opened ? Text.Sunken : Text.Normal visible: !currentMarker } Label { text: "★" visible: subscribed anchors.verticalCenter: parent.verticalCenter } Label { text: name !== undefined ? name : "" font.bold: cursor anchors.verticalCenter: parent.verticalCenter visible: !currentMarker && name !== undefined } Label { text: qsTr("Newest page") font.italic: true font.bold: cursor anchors.verticalCenter: parent.verticalCenter visible: name == undefined && !currentMarker } Label { text: qsTr("Current") font.italic: true anchors.verticalCenter: parent.verticalCenter visible: currentMarker } } onPressAndHold: { contextMenu.popup() } onClicked: { pageModel.setCursor(ord) pageStack.pop() } Menu { id: contextMenu width: menuSetBookmark.width modal: true MenuItem { id: menuSetBookmark text: qsTr("Set bookmark") onClicked: { if (currentMarker) user.subscribe(pageModel.cid(), false); else user.subscribeAt(pageModel.cid(), ord); } } } } } piperka-client-0.2.2/generic/qml/ReaderPage.qml000066400000000000000000000062261347054247500213620ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 import QtWebView 1.1 Item { property bool makeFullScreen: true id: readerPage width: appWindow.width height: appWindow.height BusyIndicator { anchors.centerIn: parent running: user.loading } WebView { id: webView anchors { top: parent.top left: parent.left right: parent.right bottom: naviRow.top } url: pageModel.cursorUri visible: !pageModel.allRead } Item { id: naviRow anchors { left: parent.left right: parent.right bottom: parent.bottom } height: prev.height Button { anchors.left: parent.left id: prev text: "⇐" enabled: !pageModel.allRead && pageModel.cursor.row > 0 onClicked: pageModel.setCursor(pageModel.cursor.row-1) } Button { anchors.left: prev.right id: back text: "<" onClicked: pageStack.pop() visible: platform.explicitBackButtons } Button { anchors { left: platform.explicitBackButtons ? back.right : prev.right right: next.left } id: options text: (1+pageModel.cursor.row) + "/" + (pageModel.rowCount-1) enabled: !pageModel.allRead onClicked: pageStack.push(Qt.resolvedUrl("PageDetailPage.qml")) } Button { anchors.right: parent.right id: next text: pageModel.nextIsSwitch ? "↵" : "⇒" enabled: pageModel.haveNext onClicked: { if (pageModel.nextIsSwitch) { if (!pageModel.switchNext()) { var depth = pageStack.depth var i = 0; while (i++ < depth) { pageStack.pop(StackView.Immediate) } pageStack.push(Qt.resolvedUrl("AllReadPage.qml"), StackView.Immediate); } } else pageModel.setCursorNext() } } } } piperka-client-0.2.2/generic/qml/RecommendPage.qml000066400000000000000000000042341347054247500220660ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 import "qrc:/qml" Item { id: recommendPage width: appWindow.width height: appWindow.height Component.onCompleted: { recommendModel.load(); } Column { anchors.centerIn: parent width: parent.width visible: !user.loading && recommendModel.noRecommendations Label { width: parent.width wrapMode: Text.WordWrap text: qsTr("For some reason, no recommendations were received. Please try again later.") } } ListView { id: recommendList model: recommendModel anchors.fill: parent currentIndex: -1 ScrollBar.vertical: ScrollBar {} header: Item { height: back.visible ? back.height : label.height Button { id: back text: "<" onClicked: pageStack.pop() visible: platform.explicitBackButtons } Label { id: label text: qsTr("Recommendations") anchors { top: parent.top right: parent.right } } } delegate: BrowseItem { } } } piperka-client-0.2.2/generic/qml/UpdatesPage.qml000066400000000000000000000075641347054247500215730ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.9 import QtQuick.Controls 2.4 import "qrc:/qml" ListView { width: appWindow.width height: appWindow.height headerPositioning: ListView.PullBackHeader ScrollBar.vertical: ScrollBar {} id: updatesList model: updatesModel currentIndex: -1 header: Item { id: myHeader width: parent.width height: headerColumn.height z: 2 Label { anchors { top: parent.top right: parent.right } text: qsTr("Updates") } Column { id: headerColumn width: parent.width Button { id: back text: "<" onClicked: pageStack.pop() visible: platform.explicitBackButtons } CheckBox { id: offsetBack text: qsTr("Offset back by one") checked: user.offsetBack onClicked: user.offsetBack = offsetBack.checked } Item { width: parent.width height: sortType.height Label { id: label anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left text: qsTr("Sort type") } ComboBox { id: sortType anchors { top: parent.top left: label.right right: parent.right } currentIndex: updatesModel.sortType model: ListModel { ListElement { text: qsTr("Least new pages first") } ListElement { text: qsTr("Most recently updated") } ListElement { text: qsTr("Alphabetical") } } onCurrentIndexChanged: { updatesModel.sortType = sortType.currentIndex } } } } } delegate: MouseArea { id: delegate width: parent.width height: unreadCount.height * 2 Label { id: unreadCount width: appWindow.width/6 anchors.verticalCenter: parent.verticalCenter text: unread_count } Label { text: title anchors { left: unreadCount.right verticalCenter: parent.verticalCenter } } NsfwMarker { visible: nsfw } onClicked: { pageModel.loadComic(cid); pageModel.autoBookmark = true; pageModel.autoSwitch = true; pageStack.push(Qt.resolvedUrl("ReaderPage.qml")); } } } piperka-client-0.2.2/generic/src/000077500000000000000000000000001347054247500166405ustar00rootroot00000000000000piperka-client-0.2.2/generic/src/piperka-client.cpp000066400000000000000000000036441347054247500222620ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 #include #include "../src/application.h" #include "platform.h" int main(int argc, char *argv[]) { QCoreApplication::setOrganizationName("piperka.net"); QCoreApplication::setApplicationName("piperka-client"); QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); QCoreApplication::setApplicationName("piperka-client"); QCoreApplication::setApplicationVersion(APP_VERSION); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); parser.process(app); QQmlApplicationEngine engine; QQmlContext *ctxt = engine.rootContext(); Platform platform; ctxt->setContextProperty("platform", &platform); Application application(ctxt); engine.addImportPath("qrc:///qml"); engine.load(QUrl("qrc:qml/MainPage.qml")); application.viewComplete(); app.exec(); } piperka-client-0.2.2/generic/src/platform.cpp000066400000000000000000000017171347054247500211760ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 "platform.h" Platform::Platform() { } piperka-client-0.2.2/generic/src/platform.h000066400000000000000000000023171347054247500206400ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include class Platform : public QObject { Q_OBJECT Q_PROPERTY(bool explicitBackButtons READ explicitBackButtons CONSTANT) public: Platform(); bool explicitBackButtons() const { #ifdef Q_OS_ANDROID return false; #else return true; #endif } }; piperka-client-0.2.2/generic/translations/000077500000000000000000000000001347054247500205725ustar00rootroot00000000000000piperka-client-0.2.2/generic/translations/piperka-client-fi.ts000066400000000000000000000337341347054247500244570ustar00rootroot00000000000000 AllReadPage All caught up! Kaikki luettu! BrowseItem Subscribe Tilaa Unsubscribe Poista tilaus View entry on Piperka Katso sarjakuvan tiedot Piperkassa BrowsePage Options Asetukset Search Etsi Browse comics Selaa sarjakuvalistaa Alphabetical Aakkosellinen Most popular Suosituin ensin Date added Viimeksi lisätty ensin Most recently updated Viimeksi päivitetty Bookmark first page Aseta kirjanmerkki ensimmäiselle sivulle Show only subscribed comics Näytä vain tilatut sarjakuvat Press and hold a comic to see more options. Paina sarjakuvan nimeä valintojen avaamiseksi. ForceLogout The server didn't recognize your user session. You have been logged out. Please try logging in again. Palvelin ei tunnistanut käyttäjäsessiotasi. Sinut on kirjattu ulos. LoginPage Login Kirjaudu User name Käyttäjänimi Password Salasana Remember me Muista minut Import bookmarks Tuo kirjanmerkit Importing bookmarks will overwrite the ones on server. Not importing will discard local bookmarks. Kirjanmerkkien tuominen ylikirjoittaa palvelimelle tallennetut kirjanmerkit. Tuomatta jättäminen hylkää kirjanmerkit. MainPage The last scheduled sync failed. The client will retry hourly. Viimeisin ajastettu päivitys epäonnistui. Ohjelma yrittää uudestaan tunneittain. Last %L1 scheduled syncs failed. The client will retry hourly. Viimeiset %L1 ajastettua päivitystä epäonnistuivat. Ohjelma yrittää uudestaan tunneittain. Synchronize now Synkronoi nyt Please wait Odota hetki Login Kirjaudu Create account Luo käyttäjätunnus Logout Kirjaudu ulos Browse comics Selaa sarjakuvalistaa Recommendations Suositukset Updates Päivitykset Quick read Pikaluku Select comics to read from the browse comics page. Valitse luettavia sarjakuvia sarjakuvalistasta. You have no unread comics. Wait for updates or subscribe to more comics. Sinulla ei ole lukemattomia sarjakuvia. Odota päivityksiä tai lisää valintoihisi uusia sarjakuvia. Log in or create account and subscribe to at least 5 comics to get recommendations. Suosituksia nähdäksesi kirjaudu tai luo tunnus ja tilaa vähintään 5 sarjakuvaa. Confirm logout Vahvista uloskirjautuminen NetworkErrorPage Network failure Verkkovirhe Detail Yksityiskohdat NewAccountPage Create account Luo tunnus Account name reserved. Please try again. Käyttäjänimi on varattu. Yritä uudestaan. User name Käyttäjänimi email (optional) email (vapaaehtoinen) Password Salasana Retype password Salasana uudestaan Remember me Muista minut PageDetailPage Details Yksityiskohdat Page URL: Sivun URL: Page URL Sivun URL Open in browser Avaa selaimessa Homepage Kotisivu Jump to... Hyppää... Currently viewing Nyt avattu sivu Subscribed Tilattu sivu Subscription controls Tilauksenhallinta Move bookmark on navigation Siirrä kirjanmerkkiä navigoidessa Newest page Uusin sivu Current Nykyinen Set bookmark Aseta kirjanmerkki RecommendPage For some reason, no recommendations were received. Please try again later. Jostain syystä palvelimelta ei saatu suosituksia. Yritä uudestaan myöhemmin. Recommendations Suositukset UpdatesPage Updates Päivittyneet sarjakuvat offset back by one Avaa luettu sivu ensin Offset back by one Avaa luettu sivu ensin Sort type Lajittelutapa Least new pages first Vähiten päivittyneita sivuja ensin Most recently updated Viimeksi päivitetty Alphabetical Aakkosellinen piperka-client-0.2.2/harbour/000077500000000000000000000000001347054247500160775ustar00rootroot00000000000000piperka-client-0.2.2/harbour/harbour-piperka.desktop000066400000000000000000000002161347054247500225640ustar00rootroot00000000000000[Desktop Entry] Type=Application X-Nemo-Application-Type=silica-qt5 Icon=harbour-piperka Exec=harbour-piperka Name=Piperka Client #Name[fi]= piperka-client-0.2.2/harbour/harbour-piperka.pro000066400000000000000000000042051347054247500217150ustar00rootroot00000000000000# NOTICE: # # Application name defined in TARGET has a corresponding QML filename. # If name defined in TARGET is changed, the following needs to be done # to match new name: # - corresponding QML filename must be changed # - desktop icon filename must be changed # - desktop filename must be changed # - icon definition filename in desktop file must be changed # - translation filenames have to be changed # The name of your application TARGET = harbour-piperka CONFIG += sailfishapp SOURCES += src/harbour-piperka.cpp \ ../src/comic.cpp \ ../src/download.cpp \ ../src/user.cpp \ ../src/subscription.cpp \ ../src/updates.cpp \ ../src/browse.cpp \ ../src/application.cpp \ ../src/page.cpp \ ../src/sortmanager.cpp \ ../src/passwordvalidator.cpp \ ../src/recommend.cpp DISTFILES += qml/harbour-piperka.qml \ qml/cover/CoverPage.qml \ rpm/harbour-piperka.changes.in \ rpm/harbour-piperka.changes.run.in \ rpm/harbour-piperka.spec \ rpm/harbour-piperka.yaml \ translations/*.ts \ harbour-piperka.desktop \ qml/pages/BrowsePage.qml \ qml/pages/MainPage.qml \ qml/pages/LoginPage.qml \ qml/pages/ReaderPage.qml \ qml/pages/PageDetailPage.qml \ qml/pages/UpdatesPage.qml \ qml/pages/AllReadPage.qml \ qml/pages/InitialLoadingPage.qml \ qml/pages/NewAccountPage.qml \ qml/pages/NetworkErrorPage.qml \ qml/pages/ForceLogout.qml \ qml/pages/RecommendPage.qml \ qml/components/BrowseItem.qml \ qml/components/NsfwMarker.qml SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172 # to disable building translations every time, comment out the # following CONFIG line CONFIG += sailfishapp_i18n TRANSLATIONS += \ translations/harbour-piperka-fi.ts \ translations/harbour-piperka-fr.ts HEADERS += \ ../src/comic.h \ ../src/download.h \ ../src/user.h \ ../src/subscription.h \ ../src/updates.h \ ../src/browse.h \ ../src/application.h \ ../src/page.h \ ../src/sortmanager.h \ ../src/passwordvalidator.h \ ../src/recommend.h DEFINES += APP_VERSION=\\\"$$VERSION\\\" CLIENT_NAME=\\\"SailfishPiperka\\\" piperka-client-0.2.2/harbour/icons/000077500000000000000000000000001347054247500172125ustar00rootroot00000000000000piperka-client-0.2.2/harbour/icons/108x108/000077500000000000000000000000001347054247500201435ustar00rootroot00000000000000piperka-client-0.2.2/harbour/icons/108x108/harbour-piperka.png000066400000000000000000000071371347054247500237540ustar00rootroot00000000000000PNG  IHDRllfWsBIT|d pHYsAtEXtSoftwarewww.inkscape.org< IDATx{pT?ܛ&a" $& H)XR@!mc[::q3mGN?ǪNeN">ECA0 "n޳ww~gswނ9L0İ_6 k ?z7u@2zćXةr"*k ՘Y=E13i#Laj]gDX"YuW1UJ@Nʄ]ޙ.\llc|] J͙˭o]/B{C Tj ǭ&qΈfehV_ڕQ%.#bh84f~-gyr20As+a\ԩ ^igp|[<-Uq- {$ȸ8[cZ. cEbQ ٔ4/ 00Ԝ-c /*W1?+"bHfLH8NdΈ_n&y'CU\aŰJҕ){f8_2Z@XSƢFpHpqUNLyG:{jJoEr@,SQQȑD|ɮe*c%]GGǎ"cɪ!ߙAnlrO=G\7cjkk6mMRn1{:**ʒ&(aX`^뭌tE|熖T94S( w BK] @O0=- QyG{ߥ3C\ܠŋ%z83 F8z00PTgAJ=VZg,)!p /w3S!)Qv[BOQænI"lIpSK=\\>9 V nbB ŕpSK=D-( 9-[ 3.NZXI9)%|"smq%ܖdX`gӶn!NJ8"zj8+сח܃Ll 2a)e`Za! @87:G8 ՖEw۵Kȥȴl2St 6?pw#c~68VETi!zs8%̐3{-‘ph%m6 26裏6&m_b#Gk6@Xʉ0{Ο!K mv|y#N /<*JBAFVr F{6lp m0o%};w.5zؠ9<08 6?| Jo{;;akioVD!(Y%Mgc=R V1wҀC|iSF njGSUxn:Bìs-"$?1==y [Ln۶ *+q)o/2N:vCDؾKc0q<>J݊ᤇKt`iwW :XrWbQ@GyD(8<`rO2Ev8J&"J&=K/d2F!`Gi{;-%0e )$ؑ6@pn &`0 31VsGލܳ1/h?/ۄgV#B&ɓZg'FCF3Cbp3 vdGv fQ'}5#G0 gR]9N0phbHLV;v oG+-'2l֏r%c{[x»JUdqڹs_ƿkzw7#@W)+/ީ{VvwR*9ʬ8;i $T_Oph VV*/'TZJ) c$k n>"ɵi6YaCBdlt%S)M?ÑNaTD8%#  <;HI3p0Cϝ€)s XW_f,IIkd0[m0 X)DLg>U&']\0|ix W*.)&* ,7 fR4z'VH+sT u H^?ߤCAq9&75[43`) ,v04kPG=J ~ɩ±©]T[PIENDB`piperka-client-0.2.2/harbour/icons/128x128/000077500000000000000000000000001347054247500201475ustar00rootroot00000000000000piperka-client-0.2.2/harbour/icons/128x128/harbour-piperka.png000066400000000000000000000104161347054247500237520ustar00rootroot00000000000000PNG  IHDR>asBIT|d pHYsvtEXtSoftwarewww.inkscape.org<IDATx{lTם?ymc0G X$)ܦdi7&tW}$mn+n7@mRRU7E r$I41F`v<̌}Νw+{~w{~A `Ί17f4iiWȅ_H/FWH8w%y6r֙]}Fxl w Wvx+Q`O(`-\DL$`Q9#,VV$fKU_ELy.a۱XA7_}3w wDDuR(lUz_=Rd'jt]lOuh x;tЈߊ:Yv6zvX?‰{f5#;V,>Hm!{/@O3E-#juPUtl93MM1귫k..("n`8:e:f*k.`n\F˧{vV*`h=7ήf Yo&Ow]*wȉmnnJYvL%/b`^CG*]`16legu83l`WJeC]}j%l|O/-,nC/m>7>|L#/RLK)®&ꊕ+,Y k~ ~hhA4 ոdQRʩctt zz^j[>/BO_y${3g55WbSOv|etwwkYrݬ_~u]pTe$~X-V ~ #ֲVCHmX`AU===twwk^7ZsRH0C~Ī=DfNDwqZ+lMGEFe%kw},ϩW ^Ȱ&$E2H'/(:!eYQ];H)पnK0^:բA%`)a&Blg |6ikӡ#0n80xZe]A>%)NFG 0FGy.4J Φ]˅QQ1uɮe7?G‹B2 x{z w_~K#'w*?q²KϪUӮVc(= Nx7q+ʦgȚ@^%0~$k>QTuOaF(eTFwpLIc8ϫd`@)aO*켞$@R¦?*  1եd`dH w T`'!TUX8%ɳ4ؑ#j 1SU<1e5'0 W,^ӡʑD0zxXFDj+?NАӭzd!f%nl(IHН b.jr=N - GCvT$+ L*&а ud !@cpxƟI!~` (TX=1 _ɷ"GO[{Ne$|=1WS&dpp3gĕI)ާ[vYzR{yNɊBƏ7ޔ$(2 kN_曯d =|, p:W(_r_?v?>8ws*uI-_r_N]/|/[ys}ŖOb?Vk `0+s{Fk> 'zrˈs9耓 S.! N8-2)-\_c ]]X(\._?0;6sO%c'Cuu> vVCx'<<&S0!paA7tMgal;t{Hm4LJ^ܓey̻Y-E'oc_?zu]] 6?l398^b2ldQ'N9r "> WBNv>)@h3z9>Cp= .:ˆFn:LJ+Ǚq^qGR`K(b?aYf ǰĢEL̙DS&&.AozWlq Q(gKzjkE!XqeĖ'MgR>,Wy3Kp'TLAW"em:%Dd`Ǹxt2ņ$& \A@J ;r/<] VBgV xab(<e tnt4Ddc*\N]: dI+0x/|MzU(izG^F@vA =ҧabr ᅻ`"_јծYLڮ)<P$9[ v[EpS!C <$,N M `;6KD< XnrK eȰ!J0ˀfj^xc WUΆ8i VIENDB`piperka-client-0.2.2/harbour/icons/172x172/000077500000000000000000000000001347054247500201455ustar00rootroot00000000000000piperka-client-0.2.2/harbour/icons/172x172/harbour-piperka.png000066400000000000000000000132101347054247500237430ustar00rootroot00000000000000PNG  IHDR1GsBIT|d pHYsa4tEXtSoftwarewww.inkscape.org<IDATx}?OO.;.*,, uZ 4Ĝi`*)\0*F/Z)=S;˺oėht% ("! ]ؗٙ?]fgg{S5 ӿ٧WG^,_Mg jBGP_p\ߩo-cɿ͜SJv}#%7ˣcgqfo9+ Gd,nAEOBu]=S_FG_k,>A*,L v g 7MvKV_MG,DBT%& VX%7IL0?Ew] v,9Yv#/oKqb|u1QfM ZN*Uܳc'ӖOQP;ٮ+۱{[Ӳ/6]tuK׊ fdjή~:TkUq`?u9Wvr4؝8EVڵ 8z,ɨ,&u`<ᓃjVfBe?UR /;N fGZIvkZCٺsX&Ew]etckٝtѝj59 ۆA5 vmZYt ZNk`W?ޙەbEw]^V\xHU/]K/fMl~S92!qG$,TPA9hB#`ЄFJcaoH"&㤵BH=&,رi//KOl aYk^N1ID 5a0oat޵հ ?-hHOd>TjbB\Òd ׶(|>[;rb꘣a>f_3^E撮D,]Bqs<!(T ՠf-~#UF>Í:|M,.g^`>{j46k-_]W-\TvԤQ] ;)x2[Z)_C p˒"vbz3k*ЪckRvNRng;?yeVͅqYe,-[QY(vv"ւ'7Xi=; 0~֑cBJR&v.ʰ2L[]E-fRHOX ~zVU"ŧ~9M(vXN2g xNOL&wCd`6oPTU%Up懰{D,UW.21evP쾱B; 9L3l)L:Thjq*D&nŠ  n;Qհ[`pdoo-o:kuiNO5ߠ*-~'PKp7LV l= ބ27%4ۂm|kI:](*_U r "r_M5޼ylHPo"2\l;j;\ń+)#xZ\s\mewnea7UYfjQUע%$)7c[%r 7yVepᡵag0Nxy1a„Q2uEGϏ.Le&N2ӦMs26I!' #p)? r56?~~H5 0kkk4iYs@:@͊73c 2#k׸ Ȍ/ WV? F6l@OxiXч- \+/+qǷo/w>Jv ]ȞFU]@I43f Ʊֵ3 0s|DC %zPnYF~wwGva@$vBs)3^ 0jܹ.(k<YQhSjd.ce"&|uPB݌p{mVp3kC?>k<JmF X+3e3qZE)tflnyKbPN(bSJ]i9A^f֮ŗԝFH+\ SSgAhٲU0ĮA3&K2\6e@&Rv斂S ̜-sPvQav/19he7Fa1vNJ0ގڴlv׫L7@r#;.+ЪUL{mn@"ckeӛ> *]tT)T]-+K.ɹe J Hd`IdY%_DEeYkSB*(oYE@WlgaFS@y\X] .J`` X"#,pkK`ɀ凃 0;jj2$§e%?_i HdD8Z8EZ }d$hoah߮9j~Xճa7*K@) -S+DMFpM 5Oo0beVfBy*o4u]}i"=R$քnS0`GFQseWOVrdOUРE=76J Yn@&kI100b5lΛkg`C#㶥/X[!$2"X]MCڪiD,ElK_&"6xv 蛡\זYpNh#2\d}Dz<'8 =4=H/]q[Ka8vLY,@nGi?UZV >$dC`Ǐ{/^EB5!ňvK6<2桅pJ$  )(pZ{dk nJX2'ìGdv"b?o0>ta&7  *IFo/уeH*X?=^:fvcºT[PC ;-H |mʖH##}  +,o@,Q4h8=ń;*Qdu"|6@."tuuF+ؾu B!Hx,Beean[C`Չ nf3ھ#z/#QG x r҅@ ;t@-hiڴGdbQ"9% v+y'" He}ro}\zԫ{YOg'`RÌ022xu=N!s*8j;S؂"㶑[Z vh>" 8*w` o?ӲL40үw$~,K-4ϐZ裲L4RR_e%ywew#-r.3Z ,Dx ~|"+᡽{d/idMZnx/~A[Ҩ`d7j}}>,%[[ 2  wug%xL?U%gHAbq7 vD>H)$ײvѷu$J 7xm  uK#755`[1|> ruY1" 9Bw'W] ޮ0 y@bwnmRaX LۥڷsUV1I-wBmaD'?' m-9=޿m߷2dhn~B7Z5 )]ǩf~jt{{{ٳ'Rʤ7g3!x H]߁֯L |H>f&f{ΝfzeUD'NxE+} WVhLs$ײ_w/ V6U tݽ3%cMc ,!S-+؎D~C藳ոI(Vmȱ OD۽n?7I[qPT zn+8?)z{uuM-$qP mōU_K0x|ϵ bw݅30dXa۪t 91C(X ?@Y nlf40`ڲWZkcH~V9pPTiYVصW)]]{Rf EB,x!󁙙Y"ӦA ' Eۀ}ViwkRc>CC;w؁Q]aapPH7U " XKkt2|ic;;|?5g*U/E[*xƀu@LEh&̞9G-աvt `+\/Wg"M9Bm@h}=0.B)l,od״zE*L;~m{PVFZWub[`~ ^\!D[L ׆W uu;rUU4Rww,Zr;,7vLQY@{;3f08}:) im߱>|jY3aJ&fŅ򢗒]'aA \8ZKeNC h tEw]` {a_) λND; 4[S:{Z q=vTVk9ͮPR7'#kyEw]`^5Ed]`!>pLc`Ȋ gEw]e8IXkj8J\ ~ 8W3b©b\8R\ \ j X@t,~Q$Tj]s < AXk&-bu8l"ORYLb_<-:pUS1캀l"OBm v%tH]TB) 6!,&` B®K?@i0&T.Rծ 6ރ`/4ڀ 8Db K;1NstKx@!'IIENDB`piperka-client-0.2.2/harbour/icons/86x86/000077500000000000000000000000001347054247500200155ustar00rootroot00000000000000piperka-client-0.2.2/harbour/icons/86x86/harbour-piperka.png000066400000000000000000000057561347054247500236330ustar00rootroot00000000000000PNG  IHDRVVUVasBIT|d pHYsCtEXtSoftwarewww.inkscape.org< kIDATxmTyeم.uY]YU1Řb1YmZ5mlbjBHoXI` FPaaYff]ܙ{{gf?sss<9zno/McU2| ii)X*Dp9ơbS7@LU=ɗ+PpRoF J 8OF|S=VAM_% k"&+BNZe+B[xu94:! U T!ϲ>e9ͽ-:6 '2רKT- 9_myu2}%ֻy/xju2g>هyp'nP;U'ĮYǎ܅Fj[JY(X bf^}2eMe2yey=7Rh!~I:ɴ|zp#_ af"ZDE!BǗa eH)nN3)HbN~…>[\1Vhӥw1WKB$@^%|ZD 9"vrc/HVt% {Cb/g7}`9|´r-lnM9ijhewK0íf{3Йc~e''J|~[:iu@ X]ۙ* |._bKnX$$Zi>'T[&z{X[^~%YhHHk9 0,.;/8f>Fs O2ʩ+Oo i,,aGbku $`w0,>@ukyAXD{˗Xjyp 짫 ^+,-㑙8qQ؏d2޽{&w_I]0;liV#ЃZ67,0QFy2QsP(7,fΜZ،Z4 ?sREh+Ǚcm/n$F޷-+Djp~pO{q;T&`eMʵAmx:}A= r! pRIl&skTaɪ]FE|zRe+4sBp?@Tb4 .9:J9Y\)g,2K3]0NIP uI앶Uʅ]X(PՋ=9|5rRJ *J`ښӈ&Voh~ئ"0KfpպWAĈ|dh3Ȍz  wK3RJ2iu d w6 Gvpbё$O}XWPO cUߞRqJ240S+B~Ǝ1~Cu;L_Zߕ_2V[)2͘$q8A3;y%9;={Ytj9SdixIE3Ѩ CF e?k^T "uk)) Ci ϩV+}'g $/w %vi&ѾZT4>YNӧI;A}~hhK3,+ >bt/֝/5i %k oK֫x%[, g6|o}}h7ߌqd|oa PTz9{G_ qT郃l#ܮL@9hk27YZV"kK,t?ĕ+.^Dvvݢ؉JקּV'+ĚL+3g?j U&`%eŻbDCC"PWGflמCS|} Rav -*dZ >L4Dr{tw+-3^nاZ fiu.9zODC(D&WdV[IZlاRyäZ[ok#H&'dzd>&`9cOG2 U֓}G @dVx-HݕV{U֧R'˾`;az Cj =p(KQ9ITY?Y5zme}CtipɬֿCУ\SK^ľ5 ZpĚW~w밸ZV3*O5ܮA}W+IW|Dz61[Dv2݋?84ErBIENDB`piperka-client-0.2.2/harbour/icons/harbour-piperka.svg000066400000000000000000000150661347054247500230360ustar00rootroot00000000000000 image/svg+xml piperka-client-0.2.2/harbour/qml/000077500000000000000000000000001347054247500166705ustar00rootroot00000000000000piperka-client-0.2.2/harbour/qml/components/000077500000000000000000000000001347054247500210555ustar00rootroot00000000000000piperka-client-0.2.2/harbour/qml/components/BrowseItem.qml000066400000000000000000000044121347054247500236510ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 ListItem { id: delegate property bool bookmarkFirst: true Image { source: "image://theme/icon-s-favorite" visible: subscribed anchors.verticalCenter: parent.verticalCenter } Label { x: Theme.horizontalPageMargin text: title anchors.verticalCenter: parent.verticalCenter color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor } NsfwMarker { visible: nsfw } menu: ContextMenu { MenuItem { id: menuSubscribe text: qsTr("Subscribe") visible: !subscribed onClicked: user.subscribe(cid, bookmarkFirst) } MenuItem { id: menuUnsubscribe text: qsTr("Unsubscribe") visible: subscribed onClicked: { var c = cid Remorse.itemAction(delegate, qsTr("Unsubscribing"), function() { user.unsubscribe(c) }) } } MenuItem { text: qsTr("View entry on Piperka") onClicked: Qt.openUrlExternally("https://piperka.net/info.html?cid="+cid); } } onClicked: { pageModel.loadComic(cid); pageStack.push(Qt.resolvedUrl("../pages/ReaderPage.qml")) } } piperka-client-0.2.2/harbour/qml/components/NsfwMarker.qml000066400000000000000000000022201347054247500236430ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 Label { anchors { right: parent.right top: parent.top rightMargin: Theme.paddingSmall } color: "red" text: "NSFW" font.pixelSize: Theme.fontSizeTiny } piperka-client-0.2.2/harbour/qml/cover/000077500000000000000000000000001347054247500200065ustar00rootroot00000000000000piperka-client-0.2.2/harbour/qml/cover/CoverPage.qml000066400000000000000000000075131347054247500224020ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 CoverBackground { id: cover Connections { target: cover /* Apparenlty the cover gets these on screen unlock. * Use it to trigger sync if it's time for it. */ onStatusChanged: { if (cover.status === 1) user.unlockSync(); } } Connections { target: user onSynchronize: { coverUpdates.scrollToTop(); } } BusyIndicator { id: busy size: BusyIndicatorSize.Large anchors.centerIn: parent running: user.loading } Item { anchors.fill: parent visible: updatesModel.noUnread && !user.loading SectionHeader { id: header text: "Piperka" } Item { anchors { top: header.bottom left: parent.left right: parent.right bottom: parent.bottom } Label { anchors.centerIn: parent text: qsTr("All caught up!") color: Theme.highlightColor wrapMode: Text.WordWrap } } } SilicaListView { id: coverUpdates anchors.fill: parent visible: !updatesModel.noUnread model: updatesModel header: SectionHeader { text: "Piperka" } delegate: ListItem { id: delegate Label { x: Theme.horizontalPageMargin id: unreadCount width: Theme.itemSizeExtraSmall anchors.verticalCenter: parent.verticalCenter text: unread_count font.pixelSize: Theme.fontSizeExtraSmall } Label { anchors { verticalCenter: parent.verticalCenter left: unreadCount.right } text: title font.pixelSize: Theme.fontSizeExtraSmall truncationMode: TruncationMode.Fade } height: unreadCount.height } } CoverActionList { id: coverAction enabled: !updatesModel.noUnread CoverAction { iconSource: "image://theme/icon-cover-play" onTriggered: { appWindow.pageStack.clear(); appWindow.pageStack.push(Qt.resolvedUrl("../pages/MainPage.qml"), {}, PageStackAction.Immediate) appWindow.pageStack.push(Qt.resolvedUrl("../pages/UpdatesPage.qml"), {}, PageStackAction.Immediate) pageModel.loadComic(updatesModel.firstCid()); pageModel.autoBookmark = true; pageModel.autoSwitch = true; appWindow.pageStack.push(Qt.resolvedUrl("../pages/ReaderPage.qml"), {}, PageStackAction.Immediate); appWindow.activate(); } } } } piperka-client-0.2.2/harbour/qml/harbour-piperka.qml000066400000000000000000000022121347054247500224730ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 import "pages" ApplicationWindow { id: appWindow initialPage: Component { MainPage { } } cover: Qt.resolvedUrl("cover/CoverPage.qml") allowedOrientations: Orientation.All } piperka-client-0.2.2/harbour/qml/pages/000077500000000000000000000000001347054247500177675ustar00rootroot00000000000000piperka-client-0.2.2/harbour/qml/pages/AllReadPage.qml000066400000000000000000000024511347054247500226050ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 Page { allowedOrientations: Orientation.All Label { anchors.centerIn: parent x: Theme.horizontalPageMargin width: parent.width-Theme.horizontalPageMargin font.pixelSize: Theme.fontSizeExtraLarge color: Theme.highlightColor text: qsTr("All caught up!") wrapMode: Text.WordWrap } } piperka-client-0.2.2/harbour/qml/pages/BrowsePage.qml000066400000000000000000000076651347054247500225560ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 import "../components" Page { id: browsePage BusyIndicator { size: BusyIndicatorSize.Large anchors.centerIn: parent running: user.loading } allowedOrientations: Orientation.All backNavigation: !panel.open Component.onCompleted: { browseModel.filterSubscribed = false; browseModel.sortType = 0; browseModel.setSearch(""); pageModel.autoBookmark = false; pageModel.autoSwitch = false; } SilicaListView { id: browseList model: browseModel anchors.fill: parent currentIndex: -1 VerticalScrollDecorator { flickable: browseList } PullDownMenu { MenuItem { id: browseOptionsItem text: qsTr("Options") onClicked: panel.open = true } } header: Column { width: parent.width PageHeader { id: header title: qsTr("Browse comics") } SearchField { width: parent.width onTextChanged: browseModel.setSearch(text) } } delegate: BrowseItem { bookmarkFirst: bookmarkFirstSwitch.checked } } DockedPanel { id: panel width: parent.width height: parent.height modal: true dock: Dock.Right Column { width: parent.width PageHeader { title: qsTr("Options") } ComboBox { id: sortType label: qsTr("Sort type") currentIndex: 0 menu: ContextMenu { MenuItem { text: qsTr("Alphabetical") } MenuItem { text: qsTr("Most popular") } MenuItem { text: qsTr("Date added") } MenuItem { text: qsTr("Most recently updated") } onClicked: { browseModel.sortType = sortType.currentIndex } } } TextSwitch { id: bookmarkFirstSwitch text: qsTr("Bookmark first page") anchors { leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin } checked: true } TextSwitch { id: showOnlySubscribed objectName: "browseSubscribed" text: qsTr("Show only subscribed comics") anchors { leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin } onClicked: browseModel.filterSubscribed = checked } } } } piperka-client-0.2.2/harbour/qml/pages/ForceLogout.qml000066400000000000000000000026001347054247500227300ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 Page { allowedOrientations: Orientation.All Label { anchors.centerIn: parent x: Theme.horizontalPageMargin width: parent.width-Theme.horizontalPageMargin font.pixelSize: Theme.fontSizeExtraLarge color: Theme.highlightColor text: qsTr("The server didn't recognize your user session. You have been logged out. Please try logging in again.") wrapMode: Text.WordWrap } } piperka-client-0.2.2/harbour/qml/pages/LoginPage.qml000066400000000000000000000047001347054247500223500ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 Dialog { id: dialog allowedOrientations: Orientation.All Column { id: column width: parent.width DialogHeader { title: qsTr("Login") } TextField { id: nameField width: parent.width inputMethodHints: Qt.ImhNoAutoUppercase label: qsTr("User name") placeholderText: qsTr("User name") text: user.storedLoginName EnterKey.enabled: text.length >= 2 EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.onClicked: passwordField.focus = true } PasswordField { id: passwordField width: parent.width label: qsTr("Password") placeholderText: qsTr("Password") EnterKey.enabled: text.length >= 2 EnterKey.onClicked: dialog.accept() } TextSwitch { id: rememberField width: parent.width text: qsTr("Remember me") checked: user.rememberMe } TextSwitch { id: importBookmarks width: parent.width text: qsTr("Import bookmarks") description: qsTr("Importing bookmarks will overwrite the ones on server. Not importing will discard local bookmarks.") visible: user.localBookmarks() } } onAccepted: { user.login(nameField.text, passwordField.text, rememberField.checked, importBookmarks.checked) } } piperka-client-0.2.2/harbour/qml/pages/MainPage.qml000066400000000000000000000173541347054247500221750ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 Page { id: page allowedOrientations: Orientation.All BusyIndicator { id: busy size: BusyIndicatorSize.Large anchors.centerIn: parent running: user.loading || updatesModel.subscriptionFlag } Connections { target: user onLoginFailed: { pageStack.completeAnimation(); pageStack.push(Qt.resolvedUrl("LoginPage.qml")) } onCreateAccountNameReserved: { pageStack.completeAnimation(); pageStack.push(Qt.resolvedUrl("NewAccountPage.qml")); } onNetworkError: { pageStack.completeAnimation(); pageStack.push(Qt.resolvedUrl("NetworkErrorPage.qml")); } onForceLogout: { pageStack.completeAnimation(); pageStack.push(Qt.resolvedUrl("ForceLogout.qml")) } onSilentSyncFailureChanged: { syncFailedLabel.text = user.silentSyncFailure == 1 ? qsTr("The last scheduled sync failed. The client will retry hourly.") : user.silentSyncFailure > 1 ? qsTr("Last %L1 scheduled syncs failed. The client will retry hourly.").arg(user.silentSyncFailure) : ""; } } SilicaFlickable { anchors.fill: parent contentHeight: column.height PullDownMenu { MenuItem { id: newAccountItem text: qsTr("Create account") onClicked: { user.resetStoredAccountDetails(); pageStack.push(Qt.resolvedUrl("NewAccountPage.qml")); } visible: !user.logged } MenuItem { id: loginMenuItem text: qsTr("Login") onClicked: { user.resetStoredAccountDetails(); pageStack.push(Qt.resolvedUrl("LoginPage.qml")) } visible: !user.logged } MenuItem { id: logoutMenuItem text: qsTr("Logout") onClicked: Remorse.popupAction(page, qsTr("Logging out"), function() {user.logout();}) visible: user.logged } MenuItem { text: qsTr("Synchronize now") onClicked: user.syncNow(true) } } Column { id: column width: page.width; PageHeader { id: header title: "Piperka" Connections { target: user onLoggedChange: { if (user.name) { header.title = user.name + " — Piperka" } else { header.title = "Piperka" } } } } BackgroundItem { Label { enabled: !user.loading x: Theme.horizontalPageMargin text: qsTr("Browse comics") color: Theme.primaryColor font.pixelSize: Theme.fontSizeExtraLarge } onClicked: pageStack.push(Qt.resolvedUrl("BrowsePage.qml")) } BackgroundItem { Label { id: recommendLabel x: Theme.horizontalPageMargin text: qsTr("Recommendations") + ((user.recSubscriptions && user.logged) ? "" : " *") color: Theme.primaryColor font.pixelSize: Theme.fontSizeExtraLarge } onClicked: { if (user.recSubscriptions && user.logged) pageStack.push(Qt.resolvedUrl("RecommendPage.qml")) else recommendHint.open = true } } BackgroundItem { enabled: !updatesModel.noUnread && !user.loading Label { x: Theme.horizontalPageMargin text: qsTr("Updates")+ (!updatesModel.noUnread ? (" ("+updatesModel.unreadPages+" / "+updatesModel.rowCount()+")") : "") color: !updatesModel.noUnread ? Theme.primaryColor : Theme.secondaryColor font.pixelSize: Theme.fontSizeExtraLarge } onClicked: pageStack.push(Qt.resolvedUrl("UpdatesPage.qml")) } BackgroundItem { enabled: !updatesModel.noUnread && !user.loading Label { x: Theme.horizontalPageMargin text: qsTr("Quick read") color: !updatesModel.noUnread ? Theme.primaryColor : Theme.secondaryColor font.pixelSize: Theme.fontSizeExtraLarge } onClicked: { pageStack.push(Qt.resolvedUrl("UpdatesPage.qml"), {}, PageStackAction.Immediate) pageModel.loadComic(updatesModel.firstCid()); pageModel.autoBookmark = true; pageModel.autoSwitch = true; pageStack.push(Qt.resolvedUrl("ReaderPage.qml")); } } Label { x: Theme.horizontalPageMargin width: parent.width wrapMode: Text.WordWrap color: Theme.highlightColor text: user.noSubscriptions ? qsTr("Select comics to read from the browse comics page.") : qsTr("You have no unread comics. Wait for updates or subscribe to more comics.") visible: updatesModel.noUnread && !updatesModel.subscriptionFlag && !user.loading } Label { id: syncFailedLabel x: Theme.horizontalPageMargin width: parent.width-2*Theme.horizontalPageMargin wrapMode: Text.WordWrap color: Theme.highlightColor text: "" visible: user.silentSyncFailure > 0 } } } DockedPanel { id: recommendHint dock: Dock.Bottom height: Theme.itemSizeExtraLarge + Theme.paddingLarge width: parent.width modal: true Label { x: Theme.horizontalPageMargin width: parent.width-Theme.horizontalPageMargin*2 anchors.top: parent.top text: qsTr("Log in or create account and subscribe to at least 5 comics to get recommendations.") color: Theme.highlightColor wrapMode: Text.WordWrap } } } piperka-client-0.2.2/harbour/qml/pages/NetworkErrorPage.qml000066400000000000000000000032521347054247500237440ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 Page { id: networkErrorPage allowedOrientations: Orientation.All Column { anchors.centerIn: parent width: parent.width Label { x: Theme.horizontalPageMargin text: qsTr("Network failure") color: Theme.highlightColor font.pixelSize: Theme.fontSizeExtraLarge width: parent.width wrapMode: Text.WordWrap } Label { x: Theme.horizontalPageMargin color: Theme.secondaryHighlightColor visible: user.networkErrorMessage != "" text: qsTr("Detail") + ": " + user.networkErrorMessage width: parent.width wrapMode: Text.WordWrap } } } piperka-client-0.2.2/harbour/qml/pages/NewAccountPage.qml000066400000000000000000000070201347054247500233440ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 import net.piperka 0.1 Dialog { id: dialog allowedOrientations: Orientation.All Column { id: column width: parent.width DialogHeader { title: qsTr("Create account") } Label { x: Theme.horizontalPageMargin text: qsTr("Account name reserved. Please try again.") visible: user.storedCreatePassword != "" } TextField { id: nameField width: parent.width inputMethodHints: Qt.ImhNoAutoUppercase label: qsTr("User name") placeholderText: qsTr("User name") validator: RegExpValidator { regExp: /^.{2,}/ } EnterKey.enabled: text.length >= 2 EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.onClicked: emailField.focus = true } TextField { id: emailField width: parent.width inputMethodHints: Qt.ImhNoAutoUppercase label: qsTr("email") placeholderText: qsTr("email (optional)") text: user.storedCreateEmail EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.onClicked: passwordField.focus = true } PasswordField { id: passwordField width: parent.width label: qsTr("Password") placeholderText: qsTr("Password") validator: RegExpValidator { regExp: /^.{4,}/ } objectName: "newAccountPassword" text: user.storedCreatePassword EnterKey.enabled: text.length >= 4 EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.onClicked: passwordFieldAgain.focus = true } PasswordField { id: passwordFieldAgain width: parent.width label: qsTr("Retype password") placeholderText: qsTr("Retype password") validator: PasswordValidator { } text: user.storedCreatePassword EnterKey.enabled: text.length >= 4 EnterKey.onClicked: dialog.accept() } TextSwitch { id: rememberField width: parent.width text: qsTr("Remember me") checked: user.rememberMe } } canAccept: nameField.acceptableInput && passwordField.acceptableInput && passwordFieldAgain.acceptableInput onAccepted: { user.createAccount(nameField.text, emailField.text, passwordField.text, rememberField.checked) } } piperka-client-0.2.2/harbour/qml/pages/PageDetailPage.qml000066400000000000000000000141571347054247500233060ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 Page { id: detailPage allowedOrientations: Orientation.All SilicaListView { id: pageList model: pageModel anchors.fill: parent currentIndex: -1 VerticalScrollDecorator { id: verticalDecorator flickable: pageList } header: Column { id: content width: parent.width PageHeader { title: qsTr("Details") } Item { width: 1 height: Theme.paddingMedium } LinkedLabel { plainText: qsTr("Use the following link to open in external browser")+"\n" + pageModel.cursorUri anchors.leftMargin: Theme.paddingSmall width: parent.width onLinkActivated: Qt.openUrlExternally(link) } Item { width: 1 height: Theme.paddingMedium } Item { height: Theme.itemSizeMedium width: parent.width Button { id: inBrowser text: qsTr("Copy to clipboard") anchors.left: parent.left width: (parent.width-Theme.horizontalPageMargin)/2 onClicked: Clipboard.text = pageModel.cursorUri } Button { text: qsTr("Homepage") anchors.right: parent.right width: (parent.width-Theme.horizontalPageMargin)/2 onClicked: Qt.openUrlExternally(pageModel.homepage) } } SectionHeader { text: qsTr("Jump to...") } Item { height: Theme.itemSizeMedium width: parent.width Button { id: focusCursor text: qsTr("Currently viewing") anchors.left: parent.left width: (parent.width-Theme.horizontalPageMargin)/2 onClicked: { pageList.currentIndex = -1 pageList.currentIndex = pageModel.cursor.row verticalDecorator.showDecorator() } } Button { text: qsTr("Subscribed") anchors.right: parent.right width: (parent.width-Theme.horizontalPageMargin)/2 enabled: pageModel.subscription.valid onClicked: { pageList.currentIndex = pageModel.subscription.row verticalDecorator.showDecorator() } } } SectionHeader { text: qsTr("Subscription controls") } TextSwitch { text: qsTr("Move bookmark on navigation") checked: pageModel.autoBookmark onClicked: pageModel.autoBookmark = checked } Item { width: 1 height: Theme.paddingMedium } } delegate: ListItem { id: delegate Row { x: Theme.horizontalPageMargin height: Theme.itemSizeSmall width: parent.width Label { width: Theme.itemSizeSmall text: 1+ord font.bold: cursor anchors.verticalCenter: parent.verticalCenter visible: !currentMarker } Image { source: "image://theme/icon-s-favorite" visible: subscribed anchors.verticalCenter: parent.verticalCenter } Label { text: name ? name : "sadf" font.bold: cursor anchors.verticalCenter: parent.verticalCenter visible: !currentMarker && name !== undefined } Label { text: qsTr("Newest page") font.italic: true font.bold: cursor anchors.verticalCenter: parent.verticalCenter visible: name === undefined && !currentMarker } Label { text: qsTr("Current") font.italic: true anchors.verticalCenter: parent.verticalCenter visible: currentMarker } } menu: ContextMenu { MenuItem { id: menuSetBookmark text: qsTr("Set bookmark") onClicked: { if (currentMarker) user.subscribe(pageModel.cid(), false) else user.subscribeAt(pageModel.cid(), ord) } } } onClicked: { pageModel.setCursor(ord) pageStack.pop() } } } } piperka-client-0.2.2/harbour/qml/pages/ReaderPage.qml000066400000000000000000000066451347054247500225140ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.2 import Sailfish.Silica 1.0 import QtWebKit 3.0 Page { id: readerPage allowedOrientations: Orientation.All BusyIndicator { size: BusyIndicatorSize.Large anchors.centerIn: parent running: user.loading } SilicaWebView { id: webView anchors { top: parent.top left: parent.left right: parent.right bottom: naviRow.top } url: pageModel.cursorUri visible: !pageModel.allRead } /* LinkedLabel { anchors { top: parent.top left: parent.left right: parent.right bottom: naviRow.top } text: pageModel.cursorUri font.pixelSize: Theme.fontSizeExtraLarge } */ Item { id: naviRow anchors { left: parent.left right: parent.right bottom: parent.bottom } height: Theme.itemSizeSmall IconButton { anchors.left: parent.left id: prev icon.source: "image://theme/icon-m-left" width: Theme.buttonWidthExtraSmall enabled: !pageModel.allread && pageModel.cursor.row > 0 onClicked: pageModel.setCursor(pageModel.cursor.row-1) } Button { anchors.centerIn: parent id: options text: (1+pageModel.cursor.row) + "/" + (pageModel.rowCount-1) preferredWidth: Theme.buttonWidthMedium enabled: !pageModel.allRead onClicked: pageStack.push(Qt.resolvedUrl("PageDetailPage.qml")) } IconButton { anchors.right: parent.right id: next icon.source: pageModel.nextIsSwitch ? "image://theme/icon-m-next" : "image://theme/icon-m-right" width: Theme.buttonWidthExtraSmall enabled: pageModel.haveNext onClicked: { if (pageModel.nextIsSwitch) { if (!pageModel.switchNext()) { var depth = pageStack.depth var i = 0 while (i++ < depth) { pageStack.pop(null, PageStackAction.Immediate) } pageStack.push(Qt.resolvedUrl("AllReadPage.qml"), PageStackAction.Immediate) } } else pageModel.setCursorNext() } } } } piperka-client-0.2.2/harbour/qml/pages/RecommendPage.qml000066400000000000000000000037761347054247500232250ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 import "../components" Page { id: recommendPage allowedOrientations: Orientation.All BusyIndicator { size: BusyIndicatorSize.Large anchors.centerIn: parent running: user.loading } Component.onCompleted: { recommendModel.load(); } Column { anchors.centerIn: parent width: parent.width visible: !user.loading && recommendModel.noRecommendations Label { x: Theme.horizontalPageMargin width: parent.width-Theme.horizontalPageMargin color: Theme.secondaryColor wrapMode: Text.WordWrap text: qsTr("For some reason, no recommendations were received. Try again later.") } } SilicaListView { id: recommendList model: recommendModel anchors.fill: parent currentIndex: -1 VerticalScrollDecorator { flickable: recommendList } header: PageHeader { title: qsTr("Recommendations") } delegate: BrowseItem {} } } piperka-client-0.2.2/harbour/qml/pages/UpdatesPage.qml000066400000000000000000000071751347054247500227160ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ import QtQuick 2.0 import Sailfish.Silica 1.0 import "../components" Page { id: updatesPage allowedOrientations: Orientation.All BusyIndicator { size: BusyIndicatorSize.Large anchors.centerIn: parent running: user.loading } Component.onCompleted: { updatesModel.sortType = 0; } SilicaListView { id: updatesList model: updatesModel anchors.fill: parent currentIndex: -1 VerticalScrollDecorator { flickable: updatesList } PullDownMenu { MenuItem { id: syncNow text: qsTr("Synchronize now") onClicked: user.syncNow(true) } } header: Column { width: parent.width PageHeader { id: header title: qsTr("Updates") } TextSwitch { id: offsetBack text: qsTr("Offset back by one") checked: user.offsetBack anchors { leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin } onClicked: user.offsetBack = offsetBack.checked } ComboBox { id: sortType label: qsTr("Sort type") currentIndex: updatesModel.sortType menu: ContextMenu { MenuItem { text: qsTr("Least new pages first") } MenuItem { text: qsTr("Most recently updated") } MenuItem { text: qsTr("Alphabetical") } onClicked: { updatesModel.sortType = sortType.currentIndex } } } } delegate: ListItem { id: delegate Label { id: unreadCount x: Theme.horizontalPageMargin width: Theme.itemSizeSmall anchors.verticalCenter: parent.verticalCenter text: unread_count } Label { text: title anchors { left: unreadCount.right verticalCenter: parent.verticalCenter } } NsfwMarker { visible: nsfw } onClicked: { pageModel.loadComic(cid); pageModel.autoBookmark = true; pageModel.autoSwitch = true; pageStack.push(Qt.resolvedUrl("ReaderPage.qml")) } } } } piperka-client-0.2.2/harbour/rpm/000077500000000000000000000000001347054247500166755ustar00rootroot00000000000000piperka-client-0.2.2/harbour/rpm/harbour-piperka.changes000077700000000000000000000000001347054247500247362../../CHANGESustar00rootroot00000000000000piperka-client-0.2.2/harbour/rpm/harbour-piperka.spec000066400000000000000000000027161347054247500226520ustar00rootroot00000000000000# # Do NOT Edit the Auto-generated Part! # Generated by: spectacle version 0.27 # Name: harbour-piperka # >> macros # << macros %{!?qtc_qmake:%define qtc_qmake %qmake} %{!?qtc_qmake5:%define qtc_qmake5 %qmake5} %{!?qtc_make:%define qtc_make make} %{?qtc_builddir:%define _builddir %qtc_builddir} Summary: Piperka Client Version: 0.2.2 Release: 1 Group: Qt/Qt License: LICENSE URL: https://piperka.net/ Source0: %{name}-%{version}.tar.bz2 Source100: harbour-piperka.yaml Requires: sailfishsilica-qt5 >= 0.10.9 BuildRequires: pkgconfig(sailfishapp) >= 1.0.2 BuildRequires: pkgconfig(Qt5Core) BuildRequires: pkgconfig(Qt5Qml) BuildRequires: pkgconfig(Qt5Quick) BuildRequires: desktop-file-utils %description Piperka Client app — Read web comics and follow their updates %prep %setup -q -n %{name}-%{version} # >> setup # << setup %build # >> build pre # << build pre %qtc_qmake5 \ VERSION=%{version} \ RELEASE=%{release} %qtc_make %{?_smp_mflags} # >> build post # << build post %install rm -rf %{buildroot} # >> install pre # << install pre %qmake5_install # >> install post # << install post desktop-file-install --delete-original \ --dir %{buildroot}%{_datadir}/applications \ %{buildroot}%{_datadir}/applications/*.desktop %files %defattr(-,root,root,-) %{_bindir} %{_datadir}/%{name} %{_datadir}/applications/%{name}.desktop %{_datadir}/icons/hicolor/*/apps/%{name}.png # >> files # << files piperka-client-0.2.2/harbour/rpm/harbour-piperka.yaml000066400000000000000000000026661347054247500226660ustar00rootroot00000000000000Name: harbour-piperka Summary: Piperka Client Version: 0.2.2 Release: 1 # The contents of the Group field should be one of the groups listed here: # https://github.com/mer-tools/spectacle/blob/master/data/GROUPS Group: Qt/Qt URL: https://piperka.net/ License: LICENSE # This must be generated before uploading a package to a remote build service. # Usually this line does not need to be modified. Sources: - '%{name}-%{version}.tar.bz2' Description: | Piperka Client app — Read web comics and follow their updates Configure: none # The qtc5 builder inserts macros to allow QtCreator to have fine # control over qmake/make execution Builder: qtc5 QMakeOptions: - VERSION=%{version} - RELEASE=%{release} # This section specifies build dependencies that are resolved using pkgconfig. # This is the preferred way of specifying build dependencies for your package. PkgConfigBR: - sailfishapp >= 1.0.2 - Qt5Core - Qt5Qml - Qt5Quick # Build dependencies without a pkgconfig setup can be listed here # PkgBR: # - package-needed-to-build # Runtime dependencies which are not automatically detected Requires: - sailfishsilica-qt5 >= 0.10.9 # All installed files Files: - '%{_bindir}' - '%{_datadir}/%{name}' - '%{_datadir}/applications/%{name}.desktop' - '%{_datadir}/icons/hicolor/*/apps/%{name}.png' # For more information about yaml and what's supported in Sailfish OS # build system, please see https://wiki.merproject.org/wiki/Spectacle piperka-client-0.2.2/harbour/src/000077500000000000000000000000001347054247500166665ustar00rootroot00000000000000piperka-client-0.2.2/harbour/src/harbour-piperka.cpp000066400000000000000000000026031347054247500224660ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #ifdef QT_QML_DEBUG #include #endif #include #include #include "../src/application.h" int main(int argc, char *argv[]) { QGuiApplication *app = SailfishApp::application(argc, argv); QQuickView *view = SailfishApp::createView(); QQmlContext *ctxt = view->rootContext(); Application application(ctxt); view->setSource(SailfishApp::pathToMainQml()); view->show(); application.viewComplete(); app->exec(); } piperka-client-0.2.2/harbour/translations/000077500000000000000000000000001347054247500206205ustar00rootroot00000000000000piperka-client-0.2.2/harbour/translations/harbour-piperka-fi.ts000066400000000000000000000232761347054247500246710ustar00rootroot00000000000000 AllReadPage All caught up! Kaikki luettu! BrowseItem Subscribe Tilaa Unsubscribe Poista tilaus Unsubscribing Tilaus poistetaan View entry on Piperka Katso sarjakuvan tiedot Piperkassa BrowsePage Options Asetukset Browse comics Selaa sarjakuvalistaa Sort type Lajittelutapa Alphabetical Aakkosellinen Most popular Suosituin ensin Date added Viimeksi lisätty ensin Most recently updated Viimeksi päivitetty Bookmark first page Aseta kirjanmerkki ensimmäiselle sivulle Show only subscribed comics Näytä vain tilatut sarjakuvat CoverPage All caught up! Kaikki luettu! ForceLogout The server didn't recognize your user session. You have been logged out. Please try logging in again. Palvelin ei tunnistanut käyttäjäsessiotasi. Sinut on kirjattu ulos. LoginPage Login Kirjaudu User name Käyttäjänimi Password Salasana Remember me Muista minut Import bookmarks Tuo kirjanmerkit Importing bookmarks will overwrite the ones on server. Not importing will discard local bookmarks. Kirjanmerkkien tuominen ylikirjoittaa palvelimelle tallennetut kirjanmerkit. Tuomatta jättäminen hylkää kirjanmerkit. MainPage Create account Luo käyttäjätunnus Login Kirjaudu Logout Kirjaudu ulos Logging out Uloskirjaudutaan Synchronize now Synkronoi nyt Browse comics Selaa sarjakuvalistaa Updates Päivitykset Quick read Pikaluku You have no unread comics. Wait for updates or subscribe to more comics. Sinulla ei ole lukemattomia sarjakuvia. Odota päivityksiä tai lisää valintoihisi uusia sarjakuvia. Select comics to read from the browse comics page. Valitse luettavia sarjakuvia sarjakuvalistasta. Last %L1 scheduled syncs failed. The client will retry hourly. Viimeiset %L1 ajastettua päivitystä epäonnistuivat. Ohjelma yrittää uudestaan tunneittain. The last scheduled sync failed. The client will retry hourly. Viimeisin ajastettu päivitys epäonnistui. Ohjelma yrittää uudestaan tunneittain. Log in or create account and subscribe to at least 5 comics to get recommendations. Suosituksia nähdäksesi kirjaudu tai luo tunnus ja tilaa vähintään 5 sarjakuvaa. Recommendations Suositukset NetworkErrorPage Detail Yksityiskohdat Network failure Verkkovirhe NewAccountPage Create account Luo tunnus User name Käyttäjänimi email (optional) email (vapaaehtoinen) email email Password Salasana Retype password Salasana uudestaan Account name reserved. Please try again. Käyttäjänimi on varattu. Yritä uudestaan. Remember me Muista minut PageDetailPage Details Yksityiskohdat Copy to clipboard Kopioi leikepöydälle Homepage Kotisivu Jump to... Hyppää... Currently viewing Nyt avattu sivu Subscribed Tilattu sivu Current Nykyinen Set bookmark Aseta kirjamerkki Subscription controls Tilauksenhallinta Move bookmark on navigation Siirrä kirjanmerkkiä navigoidessa Newest page Uusin sivu Use the following link to open in external browser Käytä seuraavaa linkkiä avataksesi sivun erillisessä selaimessa RecommendPage For some reason, no recommendations were received. Try again later. Jostain syystä palvelimelta ei saatu suosituksia. Yritä uudestaan myöhemmin. Recommendations Suositukset UpdatesPage Synchronize now Synkronoi nyt Updates Päivittyneet sarjakuvat Sort type Lajittelutapa Least new pages first Vähiten päivittyneitä sivuja ensin Most recently updated Viimeksi päivitetty Alphabetical Aakkosellinen Offset back by one Avaa luettu sivu ensin piperka-client-0.2.2/harbour/translations/harbour-piperka-fr.ts000066400000000000000000000233011347054247500246670ustar00rootroot00000000000000 AllReadPage All caught up! Tout à jour! BrowseItem Subscribe S'abonner Unsubscribe Se désabonner Unsubscribing Désabonnement View entry on Piperka Afficher l'entrée sur Piperka BrowsePage Options Paramètres Browse comics Explorer les comics Sort type Mode de tri Alphabetical Alphabétique Most popular Plus populaire Date added Date d'ajout Most recently updated Mises à jour récentes Bookmark first page Ajouter la première page aux favoris Show only subscribed comics Seulement afficher les comics souscrits CoverPage All caught up! Tout à jour! ForceLogout The server didn't recognize your user session. You have been logged out. Please try logging in again. Le serveur n'a pas reconnu votre session ; vous avez été déconnecté. Merci de vous reconnecter. LoginPage Login Connexion User name Nom d'utilisateur Password Mot de passe Remember me Se souvenir de moi Import bookmarks Importer les favoris Importing bookmarks will overwrite the ones on server. Not importing will discard local bookmarks. Importer les favoris va importer ceux présents sur le serveur ; ne pas les importer va abandonner ceux en local. MainPage Create account Créer un compte Login Se connecter Logout Se déconnecter Logging out Déconnexion en cours Synchronize now Synchroniser Browse comics Parcourir les comics Updates Mises à jour Quick read Lecture rapide You have no unread comics. Wait for updates or subscribe to more comics. Vous n'avez pas de comic non lu. Attendez des mises à jour ou souscrivez à d'autres comics. Select comics to read from the browse comics page. Sélectionner des comics à lire depuis la page "Parcourir les comics". Last %L1 scheduled syncs failed. The client will retry hourly. Les %L1 dernières synchronisations automatiques ont échoué. Le client réessaiera chaque heure. The last scheduled sync failed. The client will retry hourly. La dernière synchronisation automatique a échoué. Le client réessaiera chaque heure. Recommendations Recommandations Log in or create account and subscribe to at least 5 comics to get recommendations. NetworkErrorPage Detail Détails Network failure Erreur réseau NewAccountPage Create account Créer un compte User name Nom d'utilisateur email (optional) E-mail (optionel) email E-mail Password Mot de passe Retype password Retaper le mot de passe Account name reserved. Please try again. Nom de compte déjà utilisé, merci de réessayer. Remember me Se souvenir de moi PageDetailPage Details Détails Copy to clipboard Copier dans le presse-papier Homepage Accueil Jump to... Aller à... Currently viewing Actuellement lus Subscribed Souscrits Current Actuel Set bookmark Ajouter en favori Subscription controls Contrôles de souscriptions Move bookmark on navigation Déplacer le favori en navigation Newest page Plus récente page Use the following link to open in external browser Utilisez ce lien pour ouvrir la page dans un navigateur externe RecommendPage For some reason, no recommendations were received. Try again later. Recommendations Recommandations UpdatesPage Synchronize now Synchronisez maintenant Updates Mises à jour Sort type Type de tri Least new pages first Le moins de mises à jour en premier Most recently updated Mis à jour le plus récemment Alphabetical Alphabétique Offset back by one jkkDécaler par un piperka-client-0.2.2/harbour/translations/harbour-piperka.ts000066400000000000000000000222631347054247500242700ustar00rootroot00000000000000 AllReadPage All caught up! BrowseItem Subscribe Unsubscribe Unsubscribing View entry on Piperka BrowsePage Options Browse comics Sort type Alphabetical Most popular Date added Most recently updated Bookmark first page Show only subscribed comics CoverPage All caught up! ForceLogout The server didn't recognize your user session. You have been logged out. Please try logging in again. LoginPage Login User name Password Remember me Import bookmarks Importing bookmarks will overwrite the ones on server. Not importing will discard local bookmarks. MainPage Create account Login Logout Logging out Synchronize now Browse comics Updates Quick read You have no unread comics. Wait for updates or subscribe to more comics. Select comics to read from the browse comics page. Last %L1 scheduled syncs failed. The client will retry hourly. The last scheduled sync failed. The client will retry hourly. Log in or create account and subscribe to at least 5 comics to get recommendations. Recommendations NetworkErrorPage Detail Network failure NewAccountPage Create account User name email (optional) email Password Retype password Account name reserved. Please try again. Remember me PageDetailPage Details Copy to clipboard Homepage Jump to... Currently viewing Subscribed Current Set bookmark Subscription controls Move bookmark on navigation Newest page Use the following link to open in external browser RecommendPage For some reason, no recommendations were received. Try again later. Recommendations UpdatesPage Synchronize now Updates Sort type Least new pages first Most recently updated Alphabetical Offset back by one piperka-client-0.2.2/src/000077500000000000000000000000001347054247500152245ustar00rootroot00000000000000piperka-client-0.2.2/src/application.cpp000066400000000000000000000137431347054247500202430ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 "application.h" #include "passwordvalidator.h" Application::Application(QQmlContext *ctxt, QObject *parent) : QObject(parent) , m_user(m_download) , m_browse(m_sortManager) , m_updates(m_sortManager) , m_page(m_updates, m_user) { qmlRegisterType("net.piperka", 0, 1, "PasswordValidator"); m_browse.setSource(&m_comics); m_updates.setSource(&m_comics); m_recommend.setSource(&m_comics); ctxt->setContextProperty("comicModel", &m_comics); ctxt->setContextProperty("browseModel", &m_browse); ctxt->setContextProperty("pageModel", &m_page); ctxt->setContextProperty("updatesModel", &m_updates); ctxt->setContextProperty("recommendModel", &m_recommend); ctxt->setContextProperty("user", &m_user); connect(&m_download, &Download::downloadSortComplete, &m_sortManager, &SortManager::sortDownloaded); // Make sure to reset updates stats after logout connect(&m_user, &User::loggedOut, &m_updates, &UpdatesModel::loggedOut); connect(&m_user, &User::loggedOut, &m_download, &Download::forgetUser); // Connect subscription events connect(&m_user, &User::newSubscription, &m_comics, &ComicModel::newSubscription); connect(&m_user, &User::refreshSubscription, &m_comics, &ComicModel::refreshSubscription); connect(&m_user, &User::newSubscription, &m_page, &PageModel::setSubscription); connect(&m_user, &User::subscriptionChange, &m_updates, &UpdatesModel::subscriptionChange); // Synchronize connect(&m_user, &User::synchronize, &m_comics, &ComicModel::downloadComicsListPrepare); connect(&m_user, &User::fetchSubscriptionsEnd, &m_comics, &ComicModel::pendingSubscriptionsComplete); // Comics list download connect(&m_comics, &ComicModel::downloadComicsListPrepare, &m_user, &User::networkActionBegin); connect(&m_download, &Download::downloadComicsListComplete, &m_user, &User::networkActionEnd); connect(&m_download, &Download::downloadComicsListUnchanged, &m_user, &User::networkActionEnd); connect(&m_comics, &ComicModel::downloadComicsListPrepare, &m_download, &Download::downloadComicsList); connect(&m_download, &Download::downloadComicsListComplete, &m_comics, &ComicModel::downloadComicsListComplete); connect(&m_download, &Download::downloadComicsListUnchanged, &m_comics, &ComicModel::downloadComicsListUnchanged); connect(&m_download, &Download::downloadComicsListError, &m_user, &User::syncError); // Browse page sort type loads connect(&m_browse, &BrowseModel::sortTypeChangePrepare, &m_user, &User::networkActionBegin); connect(&m_browse, &BrowseModel::sortTypeChanged, &m_user, &User::networkActionEnd); connect(&m_browse, &BrowseModel::sortTypeChangePrepare, &m_download, &Download::downloadSort); connect(&m_download, &Download::downloadSortComplete, &m_browse, &BrowseModel::sortDownloaded); // Updates page sort type loads connect(&m_updates, &UpdatesModel::sortTypeChangePrepare, &m_user, &User::networkActionBegin); connect(&m_updates, &UpdatesModel::sortTypeChanged, &m_user, &User::networkActionEnd); connect(&m_updates, &UpdatesModel::sortTypeChangePrepare, &m_download, &Download::downloadSort); connect(&m_download, &Download::downloadSortComplete, &m_updates, &UpdatesModel::sortDownloaded); // Page page page list loads connect(&m_page, &PageModel::loadPages, &m_download, &Download::downloadPages); connect(&m_download, &Download::downloadPagesComplete, &m_page, &PageModel::loadPagesComplete); connect(&m_page, &PageModel::loadPages, this, [=](){m_user.networkActionBegin();}); connect(&m_download, &Download::downloadPagesComplete, this, [=](){m_user.networkActionEnd();}); connect(&m_page, &PageModel::pagesLoaded, &m_user, &User::setCursor); // Recommend list loads connect(&m_recommend, &RecommendModel::loadRecommend, &m_download, &Download::downloadRecommend); connect(&m_download, &Download::downloadRecommendComplete, &m_recommend, &RecommendModel::loadRecommendComplete); connect(&m_recommend, &RecommendModel::loadRecommend, this, [=](){m_user.networkActionBegin();}); connect(&m_download, &Download::downloadRecommendComplete, this, [=](){m_user.networkActionEnd();}); // Errors for sort and page list downloads connect(&m_download, &Download::downloadSortError, &m_user, &User::genericNetworkError); connect(&m_download, &Download::downloadPagesError, &m_user, &User::genericNetworkError); connect(&m_download, &Download::downloadRecommendError, &m_user, &User::genericNetworkError); m_user.syncNow(true); } void Application::viewComplete() { m_user.emitIfLoggedin(); } piperka-client-0.2.2/src/application.h000066400000000000000000000031061347054247500177000ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include #include "browse.h" #include "comic.h" #include "download.h" #include "page.h" #include "recommend.h" #include "sortmanager.h" #include "updates.h" #include "user.h" class Application : public QObject { Q_OBJECT public: explicit Application(QQmlContext *ctxt, QObject *parent = nullptr); void viewComplete(); signals: public slots: private: Download m_download; User m_user; ComicModel m_comics; BrowseModel m_browse; UpdatesModel m_updates; RecommendModel m_recommend; // TODO: Destroy PageModel when not needed and don't use it as a singleton PageModel m_page; SortManager m_sortManager; }; piperka-client-0.2.2/src/browse.cpp000066400000000000000000000075571347054247500172470ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 "browse.h" #include "sortmanager.h" BrowseModel::BrowseModel(const SortManager &mgr, QObject *parent) : QSortFilterProxyModel(parent) , m_mgr(mgr) { sort(0); } void BrowseModel::setFilterSubscribed(bool filter) { if (filter != m_filterSubscribed) { m_filterSubscribed = filter; sort(0); invalidateFilter(); invalidate(); emit filterSubscribedChanged(); } } void BrowseModel::setSortType(int sortType) { if (sortType == m_sortType) { return; } else if (sortType == 0 || sortType == 2) { m_sortType = sortType; emit sortTypeChanged(); sort(0); invalidateFilter(); invalidate(); } else if (sortType == 1 || sortType == 3) { emit sortTypeChangePrepare(sortType); } } void BrowseModel::setSearch(const QString &search) { m_search = search; invalidateFilter(); } void BrowseModel::setSource(QAbstractItemModel *model) { connect(model, &QAbstractItemModel::rowsInserted, this, &QSortFilterProxyModel::invalidate); setSourceModel(model); } void BrowseModel::sortDownloaded(const int &sortType, const QJsonDocument &doc) { Q_UNUSED(doc); m_sortType = sortType; emit sortTypeChanged(); invalidateFilter(); invalidate(); } bool BrowseModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QVariant subs = sourceModel()->data(index, ComicModel::SubscribedRole); if (!subs.isValid()) { return false; } if (!m_search.isEmpty()) { QString title = sourceModel()->data(index, ComicModel::TitleRole).toString().toLower(); if (!title.contains(m_search.toLower())) return false; } if (m_filterSubscribed) return subs.toBool(); return true; } bool BrowseModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { int cid1; int cid2; const QHash *target = NULL; switch (m_sortType) { case 2: cid1 = sourceModel()->data(source_left, ComicModel::CidRole).toInt(); cid2 = sourceModel()->data(source_right, ComicModel::CidRole).toInt(); return (cid1 > cid2); case 1: target = &m_mgr.sortPopular(); break; case 3: target = &m_mgr.sortUpdated(); break; } if (target) { cid1 = sourceModel()->data(source_left, ComicModel::CidRole).toInt(); cid2 = sourceModel()->data(source_right, ComicModel::CidRole).toInt(); int prio1 = target->value(cid1); int prio2 = target->value(cid2); if (prio1 != prio2) return prio1 > prio2; // Default to ordering from the server } return sourceModel()->data(source_left, ComicModel::NativeOrderRole).toInt() < sourceModel()->data(source_right, ComicModel::NativeOrderRole).toInt(); } piperka-client-0.2.2/src/browse.h000066400000000000000000000045731347054247500167070ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include #include "comic.h" #include "sortmanager.h" /* ComicModel with filters/sorting for BrowsePage */ class BrowseModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(bool filterSubscribed READ filterSubscribed WRITE setFilterSubscribed NOTIFY filterSubscribedChanged) Q_PROPERTY(int sortType READ sortType WRITE setSortType NOTIFY sortTypeChanged) public: BrowseModel(const SortManager &mgr, QObject *parent = 0); bool filterSubscribed() const { return m_filterSubscribed; } void setFilterSubscribed(bool filter); int sortType() const { return m_sortType; } void setSortType(int sortType); Q_INVOKABLE void setSearch(const QString &search); void setSource(QAbstractItemModel *model); signals: void filterSubscribedChanged(); void sortTypeChangePrepare(int sortType); void sortTypeChanged(); public slots: void sortDownloaded(const int &sortType, const QJsonDocument &doc); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; bool filterAcceptsColumn(int sourceColumn, const QModelIndex &sourceParent) const override { Q_UNUSED(sourceColumn); Q_UNUSED(sourceParent); return true; } bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; private: bool m_filterSubscribed = false; int m_sortType = 0; const SortManager &m_mgr; QString m_search; }; piperka-client-0.2.2/src/comic.cpp000066400000000000000000000133511347054247500170250ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 "comic.h" #include ComicModel::ComicModel(QObject *parent) : QAbstractListModel(parent) { emit downloadComicsListPrepare(); connect(this, &ComicModel::downloadComicsListPrepare, this, [=]() { toAdd.clear(); pendingSubscriptions.clear(); pendingSubscriptionRefresh.clear(); loadPending = true; syncPending = true; }); } static const QVector subscriptionModels = {ComicModel::SubscribedRole, ComicModel::UnreadCountRole}; void ComicModel::newSubscription(const QPointer &subscription) { if (loadPending || syncPending) pendingSubscriptions.insert(subscription->cid(), subscription); else setSubscription(subscription); } void ComicModel::refreshSubscription(const QPointer &subscription) { pendingSubscriptionRefresh.append(subscription); } void ComicModel::pendingSubscriptionsComplete() { syncPending = false; finishComicsListSync(); } void ComicModel::setSubscription(const QPointer &subscription) { int cid = subscription->cid(); int row = cid_row.value(cid); if (row > 0) { m_comics[row-1].setSubscription(subscription); QModelIndex index = createIndex(row-1, 0); emit dataChanged(index, index, subscriptionModels); connect(&(*subscription), &Subscription::unsubscribing, this, &ComicModel::subscriptionDeleted); } } int ComicModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_comics.count(); } QVariant ComicModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= m_comics.count()) return QVariant(); const Comic &comic = m_comics[index.row()]; if (role == CidRole) return comic.cid(); else if (role == TitleRole) return comic.title(); else if (role == NSFWRole) return comic.nsfw(); else if (role == NativeOrderRole) return comic.nativeOrder(); else if (role == SubscribedRole) return !comic.subscription().isNull(); else if (role == UnreadCountRole) return !comic.subscription().isNull() ? comic.subscription()->num() : QVariant(); return QVariant(); } void ComicModel::downloadComicsListComplete(const QJsonDocument &doc) { QJsonArray array = doc.array(); int i = 1; for (QJsonArray::const_iterator iter = array.constBegin(); iter != array.constEnd(); ++iter) { QJsonArray val = (*iter).toArray(); toAdd.append(new Comic(i++, val[0].toInt(), val[1].toString(), val.at(2).toInt() == 1)); } loadPending = false; finishComicsListSync(); } void ComicModel::downloadComicsListUnchanged() { loadPending = false; finishComicsListSync(); } void ComicModel::subscriptionDeleted() { Subscription *subs = qobject_cast(sender()); int cid = subs->cid(); if (cid == -1) return; int row = cid_row.value(cid); if (row) { connect(subs, &QObject::destroyed, this, [=]() { QModelIndex index = createIndex(row-1, 0); emit dataChanged(index, index, subscriptionModels); }); } } QHash ComicModel::roleNames() const { QHash roles; roles[CidRole] = "cid"; roles[TitleRole] = "title"; roles[NSFWRole] = "nsfw"; roles[SubscribedRole] = "subscribed"; roles[NativeOrderRole] = "native_order"; roles[UnreadCountRole] = "unread_count"; return roles; } void ComicModel::finishComicsListSync() { if (!loadPending && !syncPending) { if (toAdd.empty()) { foreach(QPointer subs, pendingSubscriptions.values()) { setSubscription(subs); } } else { foreach(QPointer subs, pendingSubscriptionRefresh) { pendingSubscriptions.insert(subs->cid(), subs); } beginResetModel(); m_comics.clear(); endResetModel(); beginInsertRows(QModelIndex(), rowCount(), rowCount()); for (QVector::iterator iter = toAdd.begin(); iter != toAdd.end();) { QPointer subs = pendingSubscriptions.take((*iter)->cid()); if (!subs.isNull()) { (*iter)->setSubscription(subs); connect(&(*subs), &Subscription::unsubscribing, this, &ComicModel::subscriptionDeleted); } m_comics << **(iter++); } endInsertRows(); cid_row.clear(); for (int i = 0; i < m_comics.count(); ++i) { cid_row.insert(m_comics[i].cid(), i+1); } } } } void Comic::setSubscription(const QPointer subscription) { mSubscription = subscription; } piperka-client-0.2.2/src/comic.h000066400000000000000000000056751347054247500165040ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include #include #include #include "subscription.h" class Comic { public: Comic(const int &native, const int &cid, const QString &title, const bool &nsfw) : mNative(native), mCid(cid), mTitle(title), mNSFW(nsfw) {} int nativeOrder() const {return mNative;} int cid() const {return mCid;} QString title() const {return mTitle;} bool nsfw() const { return mNSFW; } bool subscribed() const { return !mSubscription.isNull(); } QPointer subscription() const { return mSubscription; } void setSubscription(const QPointer subscription); private: int mNative; int mCid; QString mTitle; bool mNSFW; QPointer mSubscription; }; class ComicModel : public QAbstractListModel { Q_OBJECT public: enum ComicRoles { CidRole = Qt::UserRole + 1 ,TitleRole ,NSFWRole ,NativeOrderRole ,SubscribedRole ,UnreadCountRole }; ComicModel(QObject *parent = 0); void setSubscription(const QPointer &subscription); int rowCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; signals: void downloadComicsListPrepare(); public slots: void downloadComicsListComplete(const QJsonDocument &doc); void downloadComicsListUnchanged(); void newSubscription(const QPointer &subscription); void refreshSubscription(const QPointer &subscription); void pendingSubscriptionsComplete(); private slots: void subscriptionDeleted(); protected: QHash roleNames() const; private: void finishComicsListSync(); // values offset by +1 QHash cid_row; QList m_comics; bool loadPending = false; bool syncPending = false; QMap> pendingSubscriptions; QList> pendingSubscriptionRefresh; QVector toAdd; }; piperka-client-0.2.2/src/download.cpp000066400000000000000000000225711347054247500175460ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 #include #include #include #include #include #include "browse.h" #include "download.h" void addBookmarksToQuery(QUrlQuery &query, const QMap &bm) { for (QMap::const_iterator iter = bm.cbegin(); iter != bm.cend(); ++iter) { if (iter.value() == -1) query.addQueryItem("bm", QString::number(iter.key())); else query.addQueryItem("bm", QString::number(iter.key()).append("+") .append(QString::number(iter.value()))); } } void addBookmarksToPostData(QByteArray &postData, const QMap &bm) { for (QMap::const_iterator iter = bm.cbegin(); iter != bm.cend(); ++iter) { postData.append("&bm="); if (iter.value() == -1) postData.append(QString::number(iter.key())); else postData.append(QString::number(iter.key())) .append('+') .append(QString::number(iter.value())); } } Download::Download(QObject *parent) : QObject(parent) , userAgent(CLIENT_NAME "/" APP_VERSION) { #ifdef PIPERKA_DEVEL connect(&mgr, &QNetworkAccessManager::sslErrors, this, &Download::ignoreSSL); #endif userAgent .append(" (") .append(QSysInfo::prettyProductName()) .append(")"); } QNetworkReply *Download::createAccount(const QString &user, const QString &email, const QString &password, const QMap &bookmarks) { QNetworkRequest request = QNetworkRequest(uri_createAccount); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); QByteArray postData; postData.append("_new_login="); postData.append(QUrl::toPercentEncoding(user)); postData.append("&email="); postData.append(QUrl::toPercentEncoding(email)); postData.append("&_new_password="); postData.append(QUrl::toPercentEncoding(password)); postData.append("&_new_password_again="); postData.append(QUrl::toPercentEncoding(password)); addBookmarksToPostData(postData, bookmarks); QNetworkReply *reply = mgr.post(request, postData); return reply; } QNetworkReply *Download::login(const QString &user, const QString &password, const QMap *bookmarks) { QNetworkRequest request = QNetworkRequest(uri_login); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); QByteArray postData; postData.append("user="); postData.append(QUrl::toPercentEncoding(user)); postData.append("&passwd="); postData.append(QUrl::toPercentEncoding(password)); if (bookmarks) addBookmarksToPostData(postData, *bookmarks); QNetworkReply *reply = mgr.post(request, postData); return reply; } QNetworkReply *Download::logout(const QString &token) { QUrlQuery query; query.addQueryItem("action", "logout"); query.addQueryItem("csrf_ham", token); QUrl uri = QUrl(uri_logout); uri.setQuery(query); QNetworkRequest request = QNetworkRequest(uri); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); return mgr.get(request); } QNetworkReply *Download::subscribe(const QString &token, const int &cid, const int &ord) { QUrlQuery query; query.addQueryItem("getstatrow", "1"); query.addQueryItem("bookmark[]", QString::number(cid)); query.addQueryItem("bookmark[]", ord == INT_MAX ? "max" : QString::number(ord)); query.addQueryItem("csrf_hash", token); QUrl uri = QUrl(uri_userPrefs); uri.setQuery(query); QNetworkRequest request = QNetworkRequest(uri); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); return mgr.get(request); } QNetworkReply *Download::unsubscribe(const QString &token, const int &cid) { QUrlQuery query; query.addQueryItem("bookmark[]", QString::number(cid)); query.addQueryItem("bookmark[]", "del"); query.addQueryItem("csrf_hash", token); QUrl uri = QUrl(uri_userPrefs); uri.setQuery(query); QNetworkRequest request = QNetworkRequest(uri); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); return mgr.get(request); } QNetworkReply *Download::bookmarkPositions(const QMap &bm) { QUrlQuery query; addBookmarksToQuery(query, bm); QUrl uri = QUrl(uri_bookmarkPositions); uri.setQuery(query); QNetworkRequest request = QNetworkRequest(uri); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); return mgr.get(request); } void Download::setSession(const QByteArray &session) { QNetworkCookieJar *jar = mgr.cookieJar(); QNetworkCookie cookie = QNetworkCookie("p_session", session); cookie.setDomain(PIPERKA_HOST); cookie.setPath("/"); jar->insertCookie(cookie); } void Download::downloadPages(int cid) { QUrl uri = QUrl(uri_archive); uri.setPath(uri_archive.path().append(QString::number(cid))); QNetworkRequest request = QNetworkRequest(uri); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); QNetworkReply *reply = mgr.get(request); connect(reply, &QNetworkReply::finished, this, [=]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit downloadPagesError(reply); return; } QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); emit downloadPagesComplete(doc); }); } void Download::downloadRecommend() { QNetworkRequest request = QNetworkRequest(uri_recommend); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); QNetworkReply *reply = mgr.get(request); connect(reply, &QNetworkReply::finished, this, [=]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit downloadRecommendError(reply); return; } QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); emit downloadRecommendComplete(doc); }); } void Download::downloadComicsList() { QNetworkRequest request = QNetworkRequest(uri_comicsOrdered); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); if (!comicsListStamp.isEmpty()) request.setRawHeader("if-modified-since", comicsListStamp); QNetworkReply *reply = mgr.get(request); connect(reply, &QNetworkReply::finished, this, [=]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit downloadComicsListError(reply); return; } int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (status == 304) { emit downloadComicsListUnchanged(); return; } for (QList::const_iterator iter = reply->rawHeaderList().cbegin(); iter != reply->rawHeaderList().cend(); ++iter) { if (QString::fromLatin1(*iter).toLower() == "last-modified") { comicsListStamp = reply->rawHeader(*iter); break; } } QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); emit downloadComicsListComplete(doc); }); } void Download::downloadSort(int sortType) { QUrlQuery query; query.addQueryItem("sort", QString::number(sortType)); QUrl uri = QUrl(uri_sort); uri.setQuery(query); QNetworkRequest request = QNetworkRequest(uri); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); QNetworkReply *reply = mgr.get(request); connect(reply, &QNetworkReply::finished, this, [=](){ reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit downloadSortError(reply); return; } QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); emit downloadSortComplete(sortType, doc); }); } void Download::forgetUser() { mgr.setCookieJar(new QNetworkCookieJar()); } QNetworkReply *Download::userPrefs() { QNetworkRequest request = QNetworkRequest(uri_userPrefs); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); return mgr.get(request); } #ifdef PIPERKA_DEVEL void Download::ignoreSSL(QNetworkReply *reply, const QList &errors) { Q_UNUSED(errors); reply->ignoreSslErrors(); } #endif piperka-client-0.2.2/src/download.h000066400000000000000000000067601347054247500172150ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #ifdef PIPERKA_DEVEL #define PIPERKA_HOST "dev.piperka.net" #else #define PIPERKA_HOST "piperka.net" #endif #include #include #include class Download : public QObject { Q_OBJECT public: explicit Download(QObject *parent = NULL); // These functions are coupled to User QNetworkReply *createAccount(const QString &user, const QString &email, const QString &password, const QMap &bookmarks); QNetworkReply *login(const QString &user, const QString &password, const QMap *bookmarks); QNetworkReply *logout(const QString &token); QNetworkReply *subscribe(const QString &token, const int &cid, const int &ord); QNetworkReply *unsubscribe(const QString &token, const int &cid); QNetworkReply *bookmarkPositions(const QMap &bm); QNetworkReply *userPrefs(); void setSession(const QByteArray &session); signals: // Error handled as syncError void downloadComicsListComplete(const QJsonDocument &doc); void downloadComicsListUnchanged(); void downloadPagesComplete(const QJsonDocument &doc); void downloadPagesError(QNetworkReply *reply); void downloadSortComplete(int sortType, const QJsonDocument &doc); void downloadSortError(QNetworkReply *reply); void downloadRecommendComplete(const QJsonDocument &doc); void downloadRecommendError(QNetworkReply *reply); void downloadComicsListError(QNetworkReply *reply); public slots: void downloadComicsList(); void downloadSort(int sortType); void downloadPages(int cid); void downloadRecommend(); void forgetUser(); private slots: #ifdef PIPERKA_DEVEL void ignoreSSL(QNetworkReply *reply, const QList &errors); #endif private: QNetworkAccessManager mgr; QByteArray comicsListStamp; QByteArray userAgent; const QUrl uri_comicsOrdered = QUrl("https://" PIPERKA_HOST "/d/comics_ordered_rated.json"); const QUrl uri_createAccount = QUrl("https://" PIPERKA_HOST "/s/createAccount"); const QUrl uri_login = QUrl("https://" PIPERKA_HOST "/s/login"); const QUrl uri_logout = QUrl("https://" PIPERKA_HOST "/"); const QUrl uri_userPrefs = QUrl("https://" PIPERKA_HOST "/s/uprefs"); const QUrl uri_sort = QUrl("https://" PIPERKA_HOST "/s/sortedlist"); const QUrl uri_archive = QUrl("https://" PIPERKA_HOST "/s/archive/"); const QUrl uri_bookmarkPositions = QUrl("https://" PIPERKA_HOST "/s/lookupPositions"); const QUrl uri_recommend = QUrl("https://" PIPERKA_HOST "/s/recommend"); }; piperka-client-0.2.2/src/page.cpp000066400000000000000000000212611347054247500166460ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 "page.h" static const QVector subscriptionRoles = {PageModel::SubscribedRole}; Page::Page(const int &ord, const QString &name, const int subPages) : m_ord(ord), m_name(name), m_subPages(subPages) { } PageModel::PageModel(UpdatesModel &updates, User &user, QObject *parent) : QAbstractListModel(parent) , m_updates(updates) , m_user(user) , timer(this) { timer.setSingleShot(true); connect(&timer, &QTimer::timeout, this, [=]() { emit cursorChanged(); }); } int PageModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); // +1 for current marker row return m_pages.count()+1; } void PageModel::loadComic(int cid) { m_cid = cid; //m_loading = true; emit loadingChanged(); emit loadPages(cid); } static const QVector cursorModels = {PageModel::CursorRole}; void PageModel::setCursor(int ord) { if (m_allRead) { m_allRead = false; emit allReadChanged(); } if (ord < 0) ord = 0; else if (ord >= m_pages.count()) ord = m_pages.count()-1; QModelIndex oldIndex = createIndex(m_cursor, 0); QModelIndex newIndex = createIndex(ord, 0); m_cursor = ord; emit cursorChanged(); emit dataChanged(oldIndex, oldIndex, cursorModels); emit dataChanged(newIndex, newIndex, cursorModels); bool oldHaveNext = m_haveNext; // Extra -1 to account for current marker bool atEnd = m_cursor >= rowCount() - 2; if (m_autoSwitch && atEnd) { if (!m_nextIsSwitch) { m_nextIsSwitch = true; emit nextIsSwitchChanged(); } } else if (m_nextIsSwitch) { m_nextIsSwitch = false; emit nextIsSwitchChanged(); } m_haveNext = !atEnd || m_nextIsSwitch; if (oldHaveNext != m_haveNext) emit haveNextChanged(); /* Archives using fragments don't seem to change WebView contents on * navigation without emitting cursorChanged twice. */ if (!m_initialLoad && data(cursor(), UriRole).toString().contains('#')) timer.start(0); m_initialLoad = false; } // Bump cursor and set bookmark when appropriate void PageModel::setCursorNext() { setCursor(m_cursor+1); if (m_autoBookmark && (m_sub.isNull() || m_cursor > m_sub->ord())) m_user.subscribeAt(m_cid, m_cursor+1); } bool PageModel::switchNext() { if (m_autoBookmark && !m_sub.isNull()) m_sub->setOrdMax(); if (m_updates.rowCount() == 0) { m_allRead = true; emit allReadChanged(); return false; } QModelIndex index = m_updates.index(0, 0); int cid = m_updates.data(index, ComicModel::CidRole).toInt(); if (cid == m_cid) { index = m_updates.index(1, 0); if (index.isValid()) cid = m_updates.data(index, ComicModel::CidRole).toInt(); else { m_allRead = true; emit allReadChanged(); return false; } } loadComic(cid); return true; } void PageModel::setSubscription(const QPointer &subs) { if (!subs.isNull() && subs->cid() != m_cid) return; if (!m_sub.isNull()) { disconnect(&(*m_sub), &Subscription::move, this, &PageModel::subscriptionMoved); disconnect(&(*m_sub), &Subscription::unsubscribing, this, &PageModel::unsubscribing); } m_sub = subs; if (!m_sub.isNull()) { connect(&(*m_sub), &Subscription::move, this, &PageModel::subscriptionMoved); connect(&(*m_sub), &Subscription::unsubscribing, this, &PageModel::unsubscribing); } if (!m_sub.isNull()) { QModelIndex index = createIndex(m_sub->ord(), 0); emit dataChanged(index, index, subscriptionRoles); } emit subscriptionChanged(); } QModelIndex PageModel::cursor() const { return createIndex(m_cursor, 0); } QModelIndex PageModel::subscription() const { if (m_sub.isNull()) return QModelIndex(); else { int ord = m_sub->ord(); if (ord < 0 || ord > m_pages.count()) return QModelIndex(); return createIndex(ord, 0); } } bool PageModel::invalidSubscription() const { return !m_sub.isNull() && !subscription().isValid(); } QString PageModel::cursorUri() const { return data(cursor(), UriRole).toString(); } void PageModel::setAutoBookmark(const bool &bm) { if (m_autoBookmark == bm) return; m_autoBookmark = bm; emit autoBookmarkChanged(); } void PageModel::loadPagesComplete(const QJsonDocument &doc) { m_initialLoad = true; beginResetModel(); m_pages.clear(); endResetModel(); QJsonObject obj = doc.object(); QJsonValue val = obj.value("url_base"); if (val.isUndefined()) return; m_url_base = val.toString(); m_url_tail = obj.value("url_tail").toString(); m_fixed_head = obj.value("fixed_head").toString(); m_homepage = obj.value("homepage").toString(); QJsonArray arr = obj.value("pages").toArray(); int i = 0; beginInsertRows(QModelIndex(), rowCount(), rowCount()); for (QJsonArray::const_iterator iter = arr.constBegin(); iter != arr.constEnd(); ++iter, ++i) { QJsonArray sub = iter->toArray(); if (!sub.empty()) { m_pages.append(Page(i, sub.at(0).toString(), sub.at(1).toInt())); } } endInsertRows(); emit rowCountChanged(); emit cursorChanged(); emit pagesLoaded(); } void PageModel::subscriptionMoved(const int &old) { Subscription *subs = qobject_cast(sender()); if (subs->cid() != m_cid || old == subs->ord()) return; QModelIndex oldIndex = createIndex(old, 0); QModelIndex newIndex = createIndex(subs->ord(), 0); if (abs(oldIndex.row()-newIndex.row())) { if (oldIndex.row() > newIndex.row()) emit dataChanged(newIndex, oldIndex, subscriptionRoles); else emit dataChanged(oldIndex, newIndex, subscriptionRoles); } else { emit dataChanged(oldIndex, oldIndex, subscriptionRoles); emit dataChanged(newIndex, newIndex, subscriptionRoles); } emit subscriptionChanged(); } void PageModel::unsubscribing() { Subscription *subs = qobject_cast(sender()); if (subs->cid() != m_cid) return; QModelIndex index = createIndex(subs->ord(), 0); emit dataChanged(index, index, subscriptionRoles); } QVariant PageModel::data(const QModelIndex &index, int role) const { if (role == SubscribedRole) { QModelIndex i = subscription(); return i.isValid() && i.row() == index.row(); } else if (index.row() == m_pages.count()) { if (role == CurrentMarkerRole) return true; else if (role == CursorRole) return false; } if (index.row() < 0 || index.row() >= m_pages.count()) return QVariant(); if (role == CursorRole) return index.row() == m_cursor; else if (role == CurrentMarkerRole) return false; const Page &page = m_pages[index.row()]; if (role == OrdRole) return index.row(); else if (role == NameRole) return page.name().isNull() ? QVariant() : page.name(); else if (role == UriRole) { if (page.name().isNull()) return m_fixed_head.isNull() ? m_homepage : m_fixed_head; QString str(m_url_base); str.append(page.name()); str.append(m_url_tail); return str; } return QVariant(); } QHash PageModel::roleNames() const { QHash roles; roles[OrdRole] = "ord"; roles[NameRole] = "name"; roles[UriRole] = "uri"; roles[CursorRole] = "cursor"; roles[CurrentMarkerRole] = "currentMarker"; roles[SubscribedRole] = "subscribed"; return roles; } piperka-client-0.2.2/src/page.h000066400000000000000000000112011347054247500163040ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include #include #include "subscription.h" #include "updates.h" #include "user.h" class Page { public: Page(const int &ord, const QString &name, const int subPages); int ord() const { return m_ord; } int subPages() const { return m_subPages; } const QString &name() const { return m_name; } private: int m_ord; QString m_name; int m_subPages; }; class PageModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QModelIndex cursor READ cursor NOTIFY cursorChanged) Q_PROPERTY(QModelIndex subscription READ subscription NOTIFY subscriptionChanged) Q_PROPERTY(QString cursorUri READ cursorUri NOTIFY cursorChanged) Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged) Q_PROPERTY(QString homepage READ homepage NOTIFY pagesLoaded) // True if navigating forward updates the user's book mark Q_PROPERTY(bool autoBookmark READ autoBookmark WRITE setAutoBookmark NOTIFY autoBookmarkChanged) // True if navigating forward from last page takes to another comic with unread pages Q_PROPERTY(bool autoSwitch READ autoSwitch WRITE setAutoSwitch) // Is forward navigation button enabled Q_PROPERTY(bool haveNext READ haveNext NOTIFY haveNextChanged) // Is forward navigation a skip to next unread comic Q_PROPERTY(bool nextIsSwitch READ nextIsSwitch NOTIFY nextIsSwitchChanged) // Has the user read everything and navigated forward from the last unread page Q_PROPERTY(bool allRead READ allRead NOTIFY allReadChanged) public: enum PageRoles { OrdRole = Qt::UserRole + 1 ,NameRole ,UriRole ,CursorRole ,CurrentMarkerRole ,SubscribedRole }; PageModel(UpdatesModel &updates, User &user, QObject *parent = 0); int rowCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; Q_INVOKABLE void loadComic(int cid); Q_INVOKABLE int cid() const {return m_cid;} Q_INVOKABLE void setCursor(int ord); Q_INVOKABLE void setCursorNext(); QModelIndex cursor() const; QModelIndex subscription() const; Q_INVOKABLE bool invalidSubscription() const; QString cursorUri() const; QString homepage() const { return m_homepage; } bool autoBookmark() const { return m_autoBookmark; } void setAutoBookmark(const bool &bm); bool autoSwitch() const { return m_autoSwitch; } void setAutoSwitch(const bool &as) { m_autoSwitch = as; } bool haveNext() const { return m_haveNext; } bool nextIsSwitch() const {return m_nextIsSwitch;} Q_INVOKABLE bool switchNext(); bool allRead() const {return m_allRead; } signals: void loadPages(int cid); void loadingChanged(); void cursorChanged(); void subscriptionChanged(); void rowCountChanged(); void pagesLoaded(); void autoBookmarkChanged(); void haveNextChanged(); void nextIsSwitchChanged(); void allReadChanged(); public slots: void loadPagesComplete(const QJsonDocument &doc); void setSubscription(const QPointer &subs); private slots: void subscriptionMoved(const int &old); void unsubscribing(); protected: QHash roleNames() const; private: UpdatesModel &m_updates; // Used to invoke subscriptions on autoBookmark mode User &m_user; int m_cid = -1; int m_cursor = 0; QList m_pages; QString m_url_base; QString m_url_tail; QString m_fixed_head; QString m_homepage; QPointer m_sub; bool m_autoBookmark = false; bool m_haveNext = false; bool m_nextIsSwitch = false; bool m_autoSwitch; bool m_allRead = false; bool m_initialLoad = false; QTimer timer; }; piperka-client-0.2.2/src/passwordvalidator.cpp000066400000000000000000000026771347054247500215140ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 "passwordvalidator.h" PasswordValidator::PasswordValidator(QObject *parent) : QValidator(parent) { } QValidator::State PasswordValidator::validate(QString &input, int &pos) const { Q_UNUSED(pos) if (input.isEmpty()) return QValidator::Invalid; QObject *passwordField = parent()->parent()->findChild("newAccountPassword"); if (QQmlProperty::read(passwordField, "text") == input) return QValidator::Acceptable; else return QValidator::Intermediate; } piperka-client-0.2.2/src/passwordvalidator.h000066400000000000000000000022361347054247500211500ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include class PasswordValidator : public QValidator { Q_OBJECT public: explicit PasswordValidator(QObject *parent = nullptr); QValidator::State validate(QString &, int &) const override; signals: public slots: }; piperka-client-0.2.2/src/recommend.cpp000066400000000000000000000043651347054247500177110ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 "recommend.h" RecommendModel::RecommendModel(QObject *parent) : QSortFilterProxyModel(parent) { sort(0); } void RecommendModel::load() { emit loadRecommend(); } void RecommendModel::setSource(QAbstractItemModel *model) { setSourceModel(model); } void RecommendModel::loadRecommendComplete(const QJsonDocument &doc) { QJsonArray recommends = doc.array(); bool wasEmpty = recommendRanks.empty(); recommendRanks.clear(); int i = 1; for (QJsonArray::const_iterator iter = recommends.constBegin(); iter != recommends.constEnd(); ++iter, ++i) recommendRanks[(*iter).toInt()] = i; if (recommendRanks.empty() != wasEmpty) emit noRecommendationsChanged(); invalidate(); invalidateFilter(); } bool RecommendModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { QModelIndex index = sourceModel()->index(source_row, 0, source_parent); int cid = sourceModel()->data(index, ComicModel::CidRole).toInt(); return recommendRanks.contains(cid); } bool RecommendModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { int cid1 = sourceModel()->data(source_left, ComicModel::CidRole).toInt(); int cid2 = sourceModel()->data(source_right, ComicModel::CidRole).toInt(); return recommendRanks[cid1] < recommendRanks[cid2]; } piperka-client-0.2.2/src/recommend.h000066400000000000000000000036231347054247500173520ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include #include "comic.h" class RecommendModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(bool noRecommendations READ noRecommendations NOTIFY noRecommendationsChanged) public: RecommendModel(QObject *parent = 0); bool noRecommendations() const { return recommendRanks.empty(); } Q_INVOKABLE void load(); void setSource(QAbstractItemModel *model); signals: void loadRecommend(); void noRecommendationsChanged(); public slots: void loadRecommendComplete(const QJsonDocument &doc); protected: bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const override { Q_UNUSED(source_column); Q_UNUSED(source_parent); return true; } bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; private: QMap recommendRanks; }; piperka-client-0.2.2/src/sortmanager.cpp000066400000000000000000000033131347054247500202520ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 "sortmanager.h" SortManager::SortManager(QObject *parent) : QObject(parent) { } void SortManager::sortDownloaded(const int &sortType, const QJsonDocument &doc) { QJsonArray ordering = doc.array(); QHash &target = sortType == 1 ? mSortPopular : mSortUpdated; target.clear(); int i = INT_MAX; for (QJsonArray::const_iterator iter = ordering.constBegin(); iter != ordering.constEnd(); --i, ++iter) { if (iter->isArray()) { QJsonArray sub = iter->toArray(); for (QJsonArray::const_iterator iter2 = sub.constBegin(); iter2 != sub.constEnd(); ++iter2) { target.insert(iter2->toInt(), i); } } else { target.insert(iter->toInt(), i); } } } piperka-client-0.2.2/src/sortmanager.h000066400000000000000000000026031347054247500177200ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include #include class SortManager : public QObject { Q_OBJECT public: explicit SortManager(QObject *parent = nullptr); const QHash &sortPopular() const { return mSortPopular; } const QHash &sortUpdated() const { return mSortUpdated; } signals: public slots: void sortDownloaded(const int &sortType, const QJsonDocument &doc); private: QHash mSortPopular; QHash mSortUpdated; }; piperka-client-0.2.2/src/subscription.cpp000066400000000000000000000040031347054247500204510ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 "subscription.h" Subscription::Subscription(const QJsonArray &arr, QObject *parent) : QObject(parent) , m_cid(arr.at(0).toInt(-1)) , m_ordOffset(arr.at(1).toInt(0)) , m_maxord(arr.at(2).toInt(0)) , m_maxsubord(arr.at(3).toInt(0)) , m_num(arr.at(4).toInt(0)) { } bool Subscription::update(const QJsonArray &arr) { int cid = arr.at(0).toInt(-1); if (cid != m_cid) return false; int old = m_ordOffset; m_cid = cid; m_ordOffset = arr.at(1).toInt(); m_maxord = arr.at(2).toInt(); m_maxsubord = arr.at(3).toInt(); m_num = arr.at(4).toInt(); if (m_ordOffset != old) emit move(old); return true; } std::ostream &operator<<(std::ostream &os, const Subscription &sub) { return os << "subscription " << sub.cid() << " " << sub.ord() << " " << sub.max_ord() << " " << sub.max_sub_ord() << " " << sub.num(); } QDebug operator<<(QDebug d, const Subscription &sub) { return d << "subscription" << sub.cid() << sub.ord() << sub.max_ord() << sub.max_sub_ord() << sub.num(); } piperka-client-0.2.2/src/subscription.h000066400000000000000000000037031347054247500201240ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include #include #include #include class Comic; class Subscription : public QObject { Q_OBJECT public: Subscription(const QJsonArray &, QObject *parent = 0); // Used by User to update actual data bool update(const QJsonArray &); int cid() const { return m_cid; } int ord() const { return m_ordOffset; } int max_ord() const { return m_maxord; } int max_sub_ord() const { return m_maxsubord; } int num() const { return m_num; } // Used by classes holding Subscription to request bookmark move void setOrd(const int &ord) {emit requestMove(ord);} void setOrdMax() {emit requestMove(INT_MAX);} signals: void unsubscribing(); void move(const int &old); // Listened to by User void requestMove(const int &ord); private: int m_cid; int m_ordOffset; int m_maxord; int m_maxsubord; int m_num; }; std::ostream &operator<<(std::ostream &os, const Subscription &sub); QDebug operator<<(QDebug d, const Subscription &sub); piperka-client-0.2.2/src/updates.cpp000066400000000000000000000116021347054247500173750ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 "comic.h" #include "sortmanager.h" #include "updates.h" UpdatesModel::UpdatesModel(const SortManager &mgr, QObject *parent) : QSortFilterProxyModel(parent) , m_mgr(mgr) , subscriptionTimeout(this) { sort(0); /* For some reason, initial data load without timer use leads to off by * one error with comics in updatesModel. */ subscriptionTimeout.setSingleShot(true); connect(&subscriptionTimeout, &QTimer::timeout, this, [=]() { if (!m_subscriptionFlag) return; invalidateFilter(); invalidate(); int oldUnreadCount = m_unreadCount; m_unreadCount = countUnreadPages(); if (oldUnreadCount != m_unreadCount) emit unreadPagesChanged(); bool oldNoUnread = m_noUnread; m_noUnread = rowCount() == 0; if (oldNoUnread != m_noUnread) emit noUnreadChanged(); m_subscriptionFlag = false; emit subscriptionFlagChange(); }); } void UpdatesModel::setSource(QAbstractItemModel *model) { connect(model, &QAbstractItemModel::rowsInserted, this, &UpdatesModel::subscriptionChange); setSourceModel(model); } void UpdatesModel::setSortType(const int &sortType) { // 1 is switched to 3 to share most recently updated with BrowseModel if (sortType == m_sortType) return; else if (sortType == 1) { emit sortTypeChangePrepare(3); return; } else { m_sortType = sortType; } emit sortTypeChanged(); sort(0); subscriptionChange(); } int UpdatesModel::firstCid() const { QVariant cid = data(index(0,0), ComicModel::CidRole); return cid.isValid() ? cid.toInt() : -1; } int UpdatesModel::countUnreadPages() const { int total = 0; for (int i = 0; i < rowCount(); ++i) { QVariant rowData = data(index(i, 0), ComicModel::UnreadCountRole); if (rowData.isValid()) { int count = rowData.toInt(); if (count > 0) total += count; } } return total; } void UpdatesModel::sortDownloaded(const int &sortType, const QJsonDocument &doc) { Q_UNUSED(doc); // Only use last updated sort from downloaded sort types if (sortType != 3) return; m_sortType = 1; emit sortTypeChanged(); subscriptionChange(); } void UpdatesModel::subscriptionChange() { m_subscriptionFlag = true; emit subscriptionFlagChange(); subscriptionTimeout.start(1); } void UpdatesModel::loggedOut() { if (m_unreadCount > 0) { m_unreadCount = 0; emit unreadPagesChanged(); } if (!m_noUnread) { m_noUnread = true; emit noUnreadChanged(); } m_subscriptionFlag = false; } bool UpdatesModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QVariant unread = sourceModel()->data(index, ComicModel::UnreadCountRole); return unread.isValid() && unread > 0; } bool UpdatesModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { if (m_sortType == 0) { // Least new pages first int num1 = sourceModel()->data(source_left, ComicModel::UnreadCountRole).toInt(); int num2 = sourceModel()->data(source_right, ComicModel::UnreadCountRole).toInt(); if (num1 != num2) return num1 < num2; } else if (m_sortType == 1) { // Most recently updated int cid1 = sourceModel()->data(source_left, ComicModel::CidRole).toInt(); int cid2 = sourceModel()->data(source_right, ComicModel::CidRole).toInt(); int prio1 = m_mgr.sortUpdated().value(cid1); int prio2 = m_mgr.sortUpdated().value(cid2); if (prio1 != prio2) return prio1 > prio2; } // m_sortType == 2 Alphabetical and fall through return sourceModel()->data(source_left, ComicModel::NativeOrderRole).toInt() < sourceModel()->data(source_right, ComicModel::NativeOrderRole).toInt(); } piperka-client-0.2.2/src/updates.h000066400000000000000000000054301347054247500170440ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include #include #include #include #include "comic.h" #include "sortmanager.h" #include "subscription.h" class UpdatesModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(int sortType READ sortType WRITE setSortType NOTIFY sortTypeChanged) Q_PROPERTY(int unreadPages READ unreadPages NOTIFY unreadPagesChanged) Q_PROPERTY(bool noUnread READ noUnread NOTIFY noUnreadChanged) Q_PROPERTY(bool subscriptionFlag READ subscriptionFlag NOTIFY subscriptionFlagChange) public: UpdatesModel(const SortManager &mgr, QObject *parent = 0); void setSource(QAbstractItemModel *model); int sortType() const { return m_sortType; } void setSortType(const int &sortType); int unreadPages() const { return m_unreadCount;} bool noUnread() const { return m_noUnread; } bool subscriptionFlag() const { return m_subscriptionFlag; } Q_INVOKABLE int firstCid() const; signals: void sortTypeChangePrepare(int sortType); void sortTypeChanged(); void unreadPagesChanged(); void noUnreadChanged(); void subscriptionFlagChange(); public slots: void sortDownloaded(const int &sortType, const QJsonDocument &doc); void subscriptionChange(); void loggedOut(); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const override { Q_UNUSED(source_column); Q_UNUSED(source_parent); return true; } bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; private: int m_sortType = 0; int m_unreadCount = 0; bool m_noUnread = true; const SortManager &m_mgr; bool m_subscriptionFlag = false; QTimer subscriptionTimeout; int countUnreadPages() const; }; piperka-client-0.2.2/src/user.cpp000066400000000000000000000411131347054247500167060ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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 #include #include #include #include #include "page.h" #include "subscription.h" #include "user.h" User::User(Download &download, QObject *parent) : QObject(parent) , m_download(download) , timer(this) , syncAvailableTimer(this) { QSettings settings; m_browseHelpSeen = settings.value("help/browseOffsetSeen", false).toBool(); m_offsetBack = settings.value("app/offsetBack", false).toBool(); QString name = settings.value("account/name").value(); if (!name.isEmpty()) { m_name = name; m_token = settings.value("account/token").value(); QByteArray session = settings.value("account/session").value(); m_download.setSession(session); } else { QMap tmp = settings.value("local/bookmarks").value>(); for (QMap::const_iterator iter = tmp.cbegin(); iter != tmp.cend(); ++iter) { int cid = iter.key().toInt(); bool ordOk; int ord = iter.value().toInt(&ordOk); if (cid > 0 && ordOk) localBM[cid] = ord; } } timer.setSingleShot(true); connect(&timer, &QTimer::timeout, this, [=]() { User::syncNow(false); }); connect(&syncAvailableTimer, &QTimer::timeout, this, [=]() { m_syncAvailable = true; emit syncAvailableChanged(); }); } void User::emitIfLoggedin() { if (!m_name.isEmpty()) { emit loggedChange(); } } void User::setBrowseHelpSeen(const bool &seen) { QSettings settings; m_browseHelpSeen = seen; settings.setValue("help/browseOffsetSeen", true); } void User::createAccount(const QString &user, const QString &email, const QString &password, bool remember) { m_storedCreateEmail = email; m_storedCreatePassword = password; networkActionBegin(); QNetworkReply *reply = m_download.createAccount(user, email, password, localBM); connect(reply, &QNetworkReply::finished, this, [=]() { QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); QJsonObject obj = doc.object(); if (reply->error() != QNetworkReply::NoError) { QString code = obj.value("code").toString(); if (code == "DuplicateName") { emit createAccountNameReserved(); } else if (!code.isEmpty()) { m_networkErrorMessage.clear(); m_networkErrorMessage.append(reply->errorString()); m_networkErrorMessage.append(" / "); m_networkErrorMessage.append(code); emit networkError(); } else { m_networkErrorMessage = reply->errorString(); emit networkError(); } } else if (!extractAccountData(reply, obj)){ m_networkErrorMessage.clear(); emit networkError(); } networkActionEnd(); reply->deleteLater(); }); m_remember = remember; } void User::login(const QString &user, const QString &password, bool remember, bool importBookmarks) { m_storedLoginName = user; networkActionBegin(); QNetworkReply *reply = m_download.login(user, password, importBookmarks ? &localBM : nullptr); connect(reply, &QNetworkReply::finished, this, [=]() { QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); QJsonObject obj = doc.object(); if (!extractAccountData(reply, obj)) emit loginFailed(); else syncNow(true); networkActionEnd(); reply->deleteLater(); }); m_remember = remember; } void User::logout() { if (!m_token.isEmpty()) { QSettings settings; networkActionBegin(); QNetworkReply *reply = m_download.logout(QString::fromLatin1(m_token)); // Ignore network failures and just remove local data connect(reply, &QNetworkReply::finished, this, [=]() { networkActionEnd(); m_token = QByteArray(); reply->deleteLater(); }); m_name = QString(); settings.remove("account"); int oldCount = subs_set.size(); subs_set.clear(); if (oldCount > 0) emit noSubscriptionsChanged(); if (oldCount > 5) emit recSubscriptionsChanged(); deleteSubscriptions(); emit loggedOut(); emit loggedChange(); emit subscriptionChange(); } else { fprintf(stderr, "Logout with empty token, not doing anything"); } } void User::subscribe(const int &cid, const bool &bookmarkFirst) { subscribeAt(cid, bookmarkFirst ? 0 : INT_MAX); } void User::subscribeAt(const int &cid, const int &ord) { networkActionBegin(); if (logged()) { QNetworkReply *reply = m_download.subscribe(m_token, cid, ord); connect(reply, &QNetworkReply::finished, this, [=](){ reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) handleNetworkError(reply, false); else parseAndSaveSubscription("stat_row", cid, reply); }); } else { QMap pos; pos[cid] = ord == INT_MAX ? -1 : ord; QNetworkReply *reply = m_download.bookmarkPositions(pos); connect(reply, &QNetworkReply::finished, this, [=](){ reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { handleNetworkError(reply, false); return; } Subscription *subs = parseAndSaveSubscription("subscriptions", cid, reply); if (subs) { localBM[cid] = subs->ord(); storeLocalSubscriptions(); } }); } } void User::unsubscribe(const int &cid) { if (logged()) { networkActionBegin(); QNetworkReply *reply = m_download.unsubscribe(m_token, cid); connect(reply, &QNetworkReply::finished, this, [=]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { handleNetworkError(reply, false); return; } QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); QJsonObject obj = doc.object(); QJsonValue ok = obj.value("ok"); if (ok.toBool(false)) { int oldCount = subs_set.size(); Subscription *ptr = subs_set.take(cid); if (subs_set.empty() && oldCount > 0) emit noSubscriptionsChanged(); else if (oldCount >= 5 && subs_set.size() < 5) emit recSubscriptionsChanged(); if (ptr) { emit ptr->unsubscribing(); ptr->deleteLater(); } } emit subscriptionChange(); networkActionEnd(); }); } else { int oldCount = subs_set.size(); Subscription *subs = subs_set.take(cid); if (subs) { if (subs_set.empty() && oldCount > 0) emit noSubscriptionsChanged(); else if (oldCount >= 5 && subs_set.size() < 5) emit recSubscriptionsChanged(); emit subs->unsubscribing(); subs->deleteLater(); localBM.remove(cid); storeLocalSubscriptions(); emit subscriptionChange(); } } } void User::resetStoredAccountDetails() { m_storedLoginName.clear(); m_storedCreateEmail.clear(); m_storedCreatePassword.clear(); } void User::syncNow(bool interactive) { lastSync.start(); if (interactive && m_syncAvailable) { m_syncAvailable = false; syncAvailableTimer.start(interactiveSyncInterval); emit syncAvailableChanged(); } if (m_silentSyncFailure && interactive) { m_silentSyncFailure = 0; emit silentSyncFailureChanged(); } m_interactiveSync = interactive; m_reportedSyncError = false; m_countedSilentFailure = false; emit synchronize(); fetchSubscriptions(); timer.start(syncInterval); } void User::unlockSync() { if (lastSync.elapsed() > unlockSyncInterval) syncNow(false); } void User::setOffsetBack(const bool &offsetBack) { QSettings settings; settings.setValue("app/offsetBack", offsetBack); m_offsetBack = offsetBack; emit offsetBackChanged(); } void User::networkActionBegin() { if (!m_loading++) emit loadingChange(); } void User::networkActionEnd() { if (!--m_loading) emit loadingChange(); } void User::syncError(QNetworkReply *reply) { networkActionEnd(); if (m_interactiveSync) { if (!m_reportedSyncError) { m_reportedSyncError = true; m_networkErrorMessage = reply->errorString(); emit networkError(); } } else if (!m_countedSilentFailure){ m_countedSilentFailure = true; emit silentSyncFailureChanged(); } reply->deleteLater(); } void User::genericNetworkError(QNetworkReply *reply) { networkActionEnd(); m_networkErrorMessage = reply->errorString(); emit networkError(); } void User::setCursor() { PageModel *model = qobject_cast(sender()); if (!model) { return; } Subscription *sub = subs_set.value(model->cid()); model->setSubscription(sub); int ord = sub == nullptr ? 0 : sub->ord(); if (m_offsetBack && ord > 0) --ord; model->setCursor(ord); } bool User::extractAccountData(QNetworkReply *reply, QJsonObject &obj) { QString name = obj.value("name").toString(); QByteArray token = obj.value("csrf_ham").toString().toLatin1(); if (name.isEmpty() || token.isEmpty()) return false; QSettings settings; if (m_remember) { QNetworkCookieJar *jar = reply->manager()->cookieJar(); QByteArray session; QList cookies = jar->cookiesForUrl(QUrl("https://" PIPERKA_HOST "/")); for (QList::const_iterator iter = cookies.constBegin(); iter != cookies.constEnd(); ++iter) { if (iter->name() == "p_session") { session = iter->value(); break; } } if (session.isEmpty()) { fprintf(stderr, "Session cookie extraction from jar failed\n"); // Non-fatal. It just means that user needs to log in again. } else { settings.setValue("account/name", name); settings.setValue("account/token", token); settings.setValue("account/session", session); } } localBM.clear(); settings.remove("local/bookmarks"); m_name = name; m_token = token; fetchSubscriptions(); emit loggedChange(); return true; } void User::deleteSubscriptions() { subs_set.clear(); QList subs = findChildren(); foreach(Subscription *sub, subs) { emit sub->unsubscribing(); sub->deleteLater(); } } Subscription *User::addSubscription(const QJsonArray &val) { Subscription *subs = new Subscription(val, this); int cid = subs->cid(); if (cid != -1) { int oldCount = subs_set.size(); subs_set.insert(cid, subs); if (oldCount == 0) emit noSubscriptionsChanged(); else if (oldCount < 5) emit recSubscriptionsChanged(); connect(subs, &Subscription::requestMove, this, [=](const int &ord) { Subscription *s = qobject_cast(sender()); if (s == nullptr) return; if (ord == INT_MAX) subscribe(s->cid(), false); else subscribeAt(s->cid(), ord); }); emit newSubscription(QPointer(subs)); } else { delete subs; } return subs; } void User::fetchSubscriptions() { QNetworkReply *reply = nullptr; if (logged()) reply = m_download.userPrefs(); else if (!localBM.empty()) reply = m_download.bookmarkPositions(localBM); else { emit fetchSubscriptionsEnd(); return; } networkActionBegin(); connect(reply, &QNetworkReply::finished, this, [=]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { // Emit all old subscriptions for comic model QList subs = findChildren(); foreach(Subscription *sub, subs) { emit refreshSubscription(QPointer(sub)); } emit fetchSubscriptionsEnd(); handleNetworkError(reply, true); return; } networkActionEnd(); QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); QJsonObject mainObj = doc.object(); QJsonArray subsArr = mainObj.value("subscriptions").toArray(); QSet oldSubsCids; for (QHash::const_iterator iter = subs_set.cbegin(); iter != subs_set.cend(); ++iter) { oldSubsCids.insert(iter.key()); } for (QJsonArray::const_iterator iter = subsArr.constBegin(); iter != subsArr.constEnd(); ++iter) { QJsonArray array = (*iter).toArray(); if (array.empty()) continue; int cid = array.at(0).toInt(); if (cid == 0) continue; oldSubsCids.remove(cid); Subscription *subs = subs_set.value(cid); if (subs) { subs->update(array); refreshSubscription(QPointer(subs)); } else { addSubscription(array); } } foreach(int cid, oldSubsCids) { Subscription *subs = subs_set.take(cid); if (subs) { emit subs->unsubscribing(); delete subs; } } emit subscriptionChange(); emit fetchSubscriptionsEnd(); }); } Subscription *User::parseAndSaveSubscription(const QString &fieldName, const int &cid, QNetworkReply *reply) { QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); QJsonObject obj = doc.object(); QJsonArray statsRow = obj.value(fieldName).toArray(); if (statsRow.at(0).isArray()) statsRow = statsRow.at(0).toArray(); Subscription *subs = subs_set.value(cid); if (subs) subs->update(statsRow); else subs = addSubscription(statsRow); emit subscriptionChange(); networkActionEnd(); reply->deleteLater(); return subs; } void User::storeLocalSubscriptions() { QSettings settings; QMap tmp; for (QMap::const_iterator iter = localBM.cbegin(); iter != localBM.cend(); ++iter) tmp[QString::number(iter.key())] = iter.value(); settings.setValue("local/bookmarks", QVariant(tmp)); } void User::handleNetworkError(QNetworkReply *reply, const bool sync) { if (logged() && (reply->error() == QNetworkReply::ContentOperationNotPermittedError || reply->error() == QNetworkReply::ContentAccessDenied)) { emit forceLogout(); logout(); networkActionEnd(); } else if (sync) { syncError(reply); } else { m_networkErrorMessage = reply->errorString(); emit networkError(); } } piperka-client-0.2.2/src/user.h000066400000000000000000000140761347054247500163630ustar00rootroot00000000000000/************************************************************************** ** Piperka Client ** Copyright (C) 2019 Kari Pahula ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You 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. **************************************************************************/ #pragma once #include #include #include #include #include "download.h" #include "subscription.h" class User : public QObject { Q_OBJECT Q_PROPERTY(QString name READ name NOTIFY nameChange) Q_PROPERTY(bool loading READ loading NOTIFY loadingChange) Q_PROPERTY(bool logged READ logged NOTIFY loggedChange) Q_PROPERTY(bool offsetBack READ offsetBack WRITE setOffsetBack NOTIFY offsetBackChanged) Q_PROPERTY(QString storedLoginName READ storedLoginName CONSTANT) Q_PROPERTY(QString storedCreateEmail READ storedCreateEmail CONSTANT) Q_PROPERTY(QString storedCreatePassword READ storedCreatePassword CONSTANT) Q_PROPERTY(QString networkErrorMessage READ networkErrorMessage CONSTANT) Q_PROPERTY(bool syncAvailable READ syncAvailable NOTIFY syncAvailableChanged) Q_PROPERTY(int silentSyncFailure READ silentSyncFailure NOTIFY silentSyncFailureChanged) Q_PROPERTY(bool rememberMe READ rememberMe CONSTANT) Q_PROPERTY(bool noSubscriptions READ noSubscriptions NOTIFY noSubscriptionsChanged) Q_PROPERTY(bool recSubscriptions READ recSubscriptions NOTIFY recSubscriptionsChanged) Q_PROPERTY(bool browseHelpSeen READ browseHelpSeen WRITE setBrowseHelpSeen) public: explicit User(Download &download, QObject *parent = nullptr); void emitIfLoggedin(); const QString name() { return m_name; } bool loading() const { return m_loading > 0; } bool logged() const { return !m_name.isEmpty(); } QString storedLoginName() const { return m_storedLoginName; } QString storedCreateEmail() const { return m_storedCreateEmail; } QString storedCreatePassword() const { return m_storedCreatePassword; } QString networkErrorMessage() const { return m_networkErrorMessage; } bool syncAvailable() const { return m_syncAvailable; } int silentSyncFailure() const { return m_silentSyncFailure; } bool rememberMe() const { return m_remember; } bool noSubscriptions() const {return subs_set.empty();} bool recSubscriptions() const {return subs_set.size() >= 5;} bool browseHelpSeen() const {return m_browseHelpSeen;} void setBrowseHelpSeen(const bool &seen); Q_INVOKABLE void createAccount(const QString &user, const QString &email, const QString &password, bool remember); Q_INVOKABLE void login(const QString &user, const QString &password, bool remember, bool importBookmarks); Q_INVOKABLE void logout(); Q_INVOKABLE void subscribe(const int &cid, const bool &bookmarkFirst); Q_INVOKABLE void subscribeAt(const int &cid, const int &ord); Q_INVOKABLE void unsubscribe(const int &cid); Q_INVOKABLE int subscriptionCount() const { return subs_set.count(); } Q_INVOKABLE void resetStoredAccountDetails(); Q_INVOKABLE void syncNow(bool interactive); Q_INVOKABLE void unlockSync(); Q_INVOKABLE bool localBookmarks() const {return !localBM.isEmpty();} bool offsetBack() const { return m_offsetBack; } void setOffsetBack(const bool &offsetBack); signals: void nameChange(); void loadingChange(); void loggedChange(); void loggedOut(); void newSubscription(const QPointer &ptr); void refreshSubscription(const QPointer &ptr); void synchronize(); void syncAvailableChanged(); void fetchSubscriptionsEnd(); void offsetBackChanged(); void createAccountNameReserved(); void loginFailed(); void networkError(); void silentSyncFailureChanged(); void noSubscriptionsChanged(); void recSubscriptionsChanged(); void forceLogout(); // Catch all for any subscription changes void subscriptionChange(); public slots: void networkActionBegin(); void networkActionEnd(); void syncError(QNetworkReply *reply); void genericNetworkError(QNetworkReply *reply); void setCursor(); private: // Check hourly static const int syncInterval = 3600000; // On screen unlock, do an extra sync static const int unlockSyncInterval = 900000; // Give the sync button a rest static const int interactiveSyncInterval = 10000; bool m_interactiveSync; bool m_reportedSyncError; bool m_countedSilentFailure; int m_silentSyncFailure; bool m_syncAvailable = true; int m_loading = 0; Download &m_download; QTimer timer; QTimer syncAvailableTimer; QTime lastSync; bool m_remember = false; bool m_offsetBack; QString m_name; QByteArray m_token; QString m_storedLoginName; QString m_storedCreateEmail; QString m_storedCreatePassword; QString m_networkErrorMessage; bool m_browseHelpSeen; QHash subs_set; QMap localBM; bool extractAccountData(QNetworkReply *reply, QJsonObject &obj); void deleteSubscriptions(); void _subscribe(const int &cid, const int &ord, const bool &bookmarkFirst); Subscription *addSubscription(const QJsonArray &val); void fetchSubscriptions(); Subscription *parseAndSaveSubscription(const QString &fieldName, const int &cid, QNetworkReply *reply); void storeLocalSubscriptions(); void handleNetworkError(QNetworkReply *reply, bool sync); };