From bfef68a78b1dcfb775ab409d216d1880d974cfe9 Mon Sep 17 00:00:00 2001 From: Floens Date: Tue, 27 Jan 2015 19:48:32 +0100 Subject: [PATCH] Start of basis controller based system. --- .../src/main/assets/font/Roboto-Medium.ttf | Bin 0 -> 127488 bytes .../java/org/floens/chan/ChanApplication.java | 8 + .../floens/chan/controller/Controller.java | 44 +++ .../chan/controller/ControllerTransition.java | 22 ++ .../chan/controller/NavigationController.java | 173 +++++++++++ .../controller/PopControllerTransition.java | 38 +++ .../controller/PushControllerTransition.java | 37 +++ .../loader/{Loader.java => ChanLoader.java} | 32 +- .../floens/chan/core/loader/LoaderPool.java | 30 +- .../chan/core/manager/ReplyManager.java | 6 +- .../chan/core/manager/ThreadManager.java | 104 ++++--- .../chan/core/manager/WatchManager.java | 4 +- .../org/floens/chan/core/model/Loadable.java | 8 + .../chan/core/presenter/ThreadPresenter.java | 291 ++++++++++++++++++ .../floens/chan/core/watch/PinWatcher.java | 30 +- .../ui/SwipeDismissListViewTouchListener.java | 8 +- .../floens/chan/ui/activity/BaseActivity.java | 8 +- .../chan/ui/activity/BoardActivity.java | 40 ++- .../floens/chan/ui/activity/BoardEditor.java | 4 +- .../floens/chan/ui/activity/ChanActivity.java | 37 +-- .../chan/ui/activity/ImagePickActivity.java | 6 +- .../chan/ui/activity/ImageViewActivity.java | 2 +- .../ui/activity/PassSettingsActivity.java | 4 +- .../floens/chan/ui/adapter/PinnedAdapter.java | 5 +- .../floens/chan/ui/adapter/PostAdapter.java | 78 ++--- .../chan/ui/controller/BrowseController.java | 102 ++++++ .../controller/RootNavigationController.java | 65 ++++ .../ui/controller/SettingsController.java | 21 ++ .../ui/controller/ViewThreadController.java | 54 ++++ .../chan/ui/drawable/ArrowMenuDrawable.java | 153 +++++++++ .../chan/ui/fragment/ImageViewFragment.java | 14 +- .../chan/ui/fragment/PostRepliesFragment.java | 49 +-- .../chan/ui/fragment/ReplyFragment.java | 12 +- .../chan/ui/fragment/ThreadFragment.java | 73 +++-- .../chan/ui/helper/PostPopupHelper.java | 85 +++++ .../chan/ui/layout/ImageViewLayout.java | 234 ++++++++++++++ .../floens/chan/ui/layout/ThreadLayout.java | 157 ++++++++++ .../chan/ui/layout/ThreadListLayout.java | 79 +++++ .../floens/chan/ui/service/WatchNotifier.java | 4 +- .../chan/ui/toolbar/NavigationItem.java | 10 + .../org/floens/chan/ui/toolbar/Toolbar.java | 220 +++++++++++++ .../floens/chan/ui/toolbar/ToolbarMenu.java | 71 +++++ .../chan/ui/toolbar/ToolbarMenuItem.java | 86 ++++++ .../chan/ui/toolbar/ToolbarMenuSubItem.java | 19 ++ .../chan/ui/toolbar/ToolbarMenuSubMenu.java | 112 +++++++ .../chan/ui/view/CustomScaleImageView.java | 6 +- .../floens/chan/ui/view/HackyViewPager.java | 4 +- .../org/floens/chan/ui/view/LoadView.java | 111 +++++-- .../org/floens/chan/ui/view/PostView.java | 80 +++-- .../chan/ui/view/ThumbnailImageView.java | 8 +- .../org/floens/chan/utils/AndroidUtils.java | 193 ++++++++++++ .../org/floens/chan/utils/AnimationUtils.java | 51 +++ .../java/org/floens/chan/utils/FileCache.java | 2 +- .../java/org/floens/chan/utils/IOUtils.java | 39 ++- .../org/floens/chan/utils/ImageDecoder.java | 9 +- .../org/floens/chan/utils/ImageSaver.java | 13 +- .../chan/utils/SimpleAnimatorListener.java | 42 --- .../java/org/floens/chan/utils/Utils.java | 130 -------- .../src/main/res/drawable-hdpi/ic_more.png | Bin 0 -> 219 bytes .../src/main/res/drawable-mdpi/ic_more.png | Bin 0 -> 202 bytes .../src/main/res/drawable-xhdpi/ic_more.png | Bin 0 -> 269 bytes .../src/main/res/drawable-xxhdpi/ic_more.png | Bin 0 -> 313 bytes .../src/main/res/drawable-xxxhdpi/ic_more.png | Bin 0 -> 393 bytes .../res/drawable/gray_background_selector.xml | 14 + .../src/main/res/layout/image_view_layout.xml | 12 + .../app/src/main/res/layout/root_layout.xml | 42 +++ .../src/main/res/layout/settings_layout.xml | 13 + .../src/main/res/layout/toolbar_menu_item.xml | 9 + Clover/app/src/main/res/values/strings.xml | 2 +- Clover/app/src/main/res/values/styles.xml | 8 + docs/fonts/Roboto-Black.ttf | Bin 0 -> 127948 bytes docs/fonts/Roboto-BlackItalic.ttf | Bin 0 -> 134716 bytes docs/fonts/Roboto-Bold.ttf | Bin 0 -> 127744 bytes docs/fonts/Roboto-BoldItalic.ttf | Bin 0 -> 134556 bytes docs/fonts/Roboto-Italic.ttf | Bin 0 -> 132440 bytes docs/fonts/Roboto-Light.ttf | Bin 0 -> 126792 bytes docs/fonts/Roboto-LightItalic.ttf | Bin 0 -> 133172 bytes docs/fonts/Roboto-Medium.ttf | Bin 0 -> 127488 bytes docs/fonts/Roboto-MediumItalic.ttf | Bin 0 -> 134312 bytes docs/fonts/Roboto-Regular.ttf | Bin 0 -> 126072 bytes docs/fonts/Roboto-Thin.ttf | Bin 0 -> 127584 bytes docs/fonts/Roboto-ThinItalic.ttf | Bin 0 -> 132860 bytes docs/fonts/RobotoCondensed-Bold.ttf | Bin 0 -> 127340 bytes docs/fonts/RobotoCondensed-BoldItalic.ttf | Bin 0 -> 135504 bytes docs/fonts/RobotoCondensed-Italic.ttf | Bin 0 -> 133908 bytes docs/fonts/RobotoCondensed-Light.ttf | Bin 0 -> 126168 bytes docs/fonts/RobotoCondensed-LightItalic.ttf | Bin 0 -> 134544 bytes docs/fonts/RobotoCondensed-Regular.ttf | Bin 0 -> 125332 bytes 88 files changed, 2928 insertions(+), 499 deletions(-) create mode 100644 Clover/app/src/main/assets/font/Roboto-Medium.ttf create mode 100644 Clover/app/src/main/java/org/floens/chan/controller/Controller.java create mode 100644 Clover/app/src/main/java/org/floens/chan/controller/ControllerTransition.java create mode 100644 Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java create mode 100644 Clover/app/src/main/java/org/floens/chan/controller/PopControllerTransition.java create mode 100644 Clover/app/src/main/java/org/floens/chan/controller/PushControllerTransition.java rename Clover/app/src/main/java/org/floens/chan/core/loader/{Loader.java => ChanLoader.java} (92%) create mode 100644 Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/controller/SettingsController.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/drawable/ArrowMenuDrawable.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/helper/PostPopupHelper.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/layout/ImageViewLayout.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenu.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubItem.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubMenu.java create mode 100644 Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java create mode 100644 Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java delete mode 100644 Clover/app/src/main/java/org/floens/chan/utils/SimpleAnimatorListener.java delete mode 100644 Clover/app/src/main/java/org/floens/chan/utils/Utils.java create mode 100644 Clover/app/src/main/res/drawable-hdpi/ic_more.png create mode 100644 Clover/app/src/main/res/drawable-mdpi/ic_more.png create mode 100644 Clover/app/src/main/res/drawable-xhdpi/ic_more.png create mode 100644 Clover/app/src/main/res/drawable-xxhdpi/ic_more.png create mode 100644 Clover/app/src/main/res/drawable-xxxhdpi/ic_more.png create mode 100644 Clover/app/src/main/res/drawable/gray_background_selector.xml create mode 100644 Clover/app/src/main/res/layout/image_view_layout.xml create mode 100644 Clover/app/src/main/res/layout/root_layout.xml create mode 100644 Clover/app/src/main/res/layout/settings_layout.xml create mode 100644 Clover/app/src/main/res/layout/toolbar_menu_item.xml create mode 100644 docs/fonts/Roboto-Black.ttf create mode 100644 docs/fonts/Roboto-BlackItalic.ttf create mode 100644 docs/fonts/Roboto-Bold.ttf create mode 100644 docs/fonts/Roboto-BoldItalic.ttf create mode 100644 docs/fonts/Roboto-Italic.ttf create mode 100644 docs/fonts/Roboto-Light.ttf create mode 100644 docs/fonts/Roboto-LightItalic.ttf create mode 100644 docs/fonts/Roboto-Medium.ttf create mode 100644 docs/fonts/Roboto-MediumItalic.ttf create mode 100644 docs/fonts/Roboto-Regular.ttf create mode 100644 docs/fonts/Roboto-Thin.ttf create mode 100644 docs/fonts/Roboto-ThinItalic.ttf create mode 100644 docs/fonts/RobotoCondensed-Bold.ttf create mode 100644 docs/fonts/RobotoCondensed-BoldItalic.ttf create mode 100644 docs/fonts/RobotoCondensed-Italic.ttf create mode 100644 docs/fonts/RobotoCondensed-Light.ttf create mode 100644 docs/fonts/RobotoCondensed-LightItalic.ttf create mode 100644 docs/fonts/RobotoCondensed-Regular.ttf diff --git a/Clover/app/src/main/assets/font/Roboto-Medium.ttf b/Clover/app/src/main/assets/font/Roboto-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a3c1a1f1702ebf8f5771ae96197539ddc507f04a GIT binary patch literal 127488 zcmeFacVJXi`agWmxp!ug5CZAFOnT2GlQzkuO(wk;(#xdx-a~*8LdQ@?M2HBA zh=_=Yh%73LjYVVuS;ajOeN>Ikd&v2RHig5NB_%tygGb1N*$o3xy=jbD}=}1D(9EG!+-RlT{cM2gkG9$+? z;5B29lL+B@D6h&>rWLe))0=^i(K3Vt*Q;78+Sgy%;ERw2JU6&e*E~?We|_LMLVl2b z>#nb_~F?mJPV;1F6* z6zDm&1NjJMz~{MWy$HjzgOHX$d0ReLu^c?RjFQAP__#Vuwzh`zPA!(kV}w@Q3`n;>9C!skNGDu6Wl{$R*3AGTSy=BY;p*N z5`qpwZY{_{J;V+zVdtT0_9!Y7Y(b^aPfI+Bma)I0B4#lv6)I6avlczU+(7wk3tGV6 ze*yEZhZaB|%gKEthZI6KqjvUnw2t`>#lc*Sk=dw)T!#7G3;no(N(Gltq0kFu3iDAN z%(+6eSaY9MqBOx`SiAEuZ(pNj!naT&yBF25Yf&tWJA-{1jR3DW5G%Br-H686YUD^V zkfY#9w1!=V=8KM?8c`T3Ci-Y4xum%%Sb+-IZ75x^8_i?yp+4dY^Cd@x%x$!u8AqMW z5R7*QT1XC~ev*bfm`~9lV~>(ZGa5liZ2@T*p-JFebtL5axK?dLO3_)UqmGUVcyF`d zO=KyU3;Ys_9N0CeS@06t40(d!2b3(>kCcL9)CB97$DTzSSOX}t4CS&aq++d51?@At z19g%8=y|pgnX$$&?>_@SE2+AJ(a{?P6^feu zW1`Jyqv!$JD6~Qr!rx##BD6-3iJC;YXgm80^miUAW>=%tOc7el-bZf2US!9bp){EP zC2T9K!zN@eWFUVU?%^}wF(<*tFvgGJz7~}W$IuetT{K@{0p;4DTsS zZle+10Q8*bH>g((YxpC~$y`(p{E{xPLbK_dPu*vT;-F3?>gMatMD6grmQ6+_tOm8f z`qoaZ^;}r*-3UE!=Kk>@jkYf^0e)NxvebyyFgsz*UWGMwfH8T)7+yeQ!VM@;unR?s zK7=`b0nOJ*fOYsMnlJPO-qoNAQ2_E4bRbtj4`5Ii>J}_Ti-pdxE@w371!Ab9AB_Q~ zY^MAmSO%E22j+Ya$lkjskja5H6QOM3PpDq_H&iCfL2`jR@?+VPUYk^z*arL*b?|`0bEPqdM5zyhZKDQ z-92D#C1@eD1+n-Ps=_Cb6kkLEcppk(Oi(jpjiMPH6u_3EX!aRYLf%KqmWw*@ZDnP@zr`^zT0O+fB2LouNr(XMl|F0E`g8nr=n9z)!vGFks6d>SeA&dI#?J z0RLV?y?}d*nZF`uwirHt4fTRN^|F71bPCG1@Odg*0(l^cWW%A{D6IEfq$?C5ceV_f z0-p<@e=)2rDulEFk^)jBBnhNC)&_9E7RewLK+1rA^*~>TA!#ub<_@kiAf-W~*C~+V zp)YFyvos&pUlu`Am1W< z8SEZ?(5GPgP<_hVKVXBvIM#zMT``r<;qx766`y~NR?&5!^Yh1??b5tK*M-j4A9HFy z@ivS$&zQOoJgM0O7_}gngduZzd|BT8!l;LeODwALzQXZYcqc^F{PzcaJqADL6tk9SB%=AqNAxPp+f?7D`^DS6V5@5FFU?P|6V1@2 zdIRiGYCBHj8b8NipV9fDcB&9;OM3mNjVXMbmZ@o+<836cCFB4mK{eL+Lv3Sp3HvY-8Sj1)G@Kt^C}>T7#TrP&=C1y?^ZIWB0S!T+OD( z?*B8+{~7*IeLg+jKhHIPWm7a`fA*Z`JF1@)JT~|L5nms>cVovi&pmekuXsGYkNUsz zKjP}tzH55_^=J7s@BLf86ZjhvZ?^#dQu*cg86XS%K7+~wtO?!6(6yS*soYT+nC6XX zTbt@Qfk<AMwjn8$7?mzGix@r+M`eA5dJVL4?}L)Yc>} zXfg0`2{Hj{3iJW^W=w&aBQu~D$Q-C8ve5j7tdJ#8Yh(q~23Z5OMK+pW zksY!HYLD!IIv{(XV&tIt1vw%yP$%RF)EPMebwSRW`^Xi!0ChvIK;4lWP!FL0M4reU zs2B18>Ww^s`XDb2>{gLCP(S1YRDyhg`XfKh1PVYBpi<-yG!O*<4MI}QJrs-rfrg+U zprI%js0@W@enMd=6lge-0hOaLpbQXcUqIjYbhbV^E~#2c$qzKx0ufP$h~1 z8iy2`?@>I81)6}AKoe0M&?FSE`3@zc1fVG>5ojt(0-A=BHFr@uN&%XIQh{coG@w~P zzeU+79cT{90Gf+3fzCl$nr~1Z$_AQ`a)1_~T%d($j^-bz2;~7SM)^QXPyx_VRH(Uw z%1{x|a#Rddg-U={pi<4(s1lU{twQBMtC0$54ba=D7F7VPLzO`5Q5DbzRIRy%8c_{U zKvAI0s19fgs@L2^t*8NL8)^jFj+%gWpk~eAQ738v+J#zycB3|+J*ZuC1NEW~pna$l zXg}%#Iso)5G>E!^&P6>yhfpujd8kiw9nDAmKo_6^pu=bo=t4AC^CfslhJY?Y^MH<` z`9K$=1)6JU2^t2v6fFe042=L?juvVDhE||apexa0psUalpicn3ipJ1VpsUd`pli@_ zpli_z%@=4LS_$+?vrXtq1xn zdJ5r5x9J+=+2Kpts4D>qsE6}gd70tWo2Kof( z-_fT)Z=%nD-a?;i&Z67s3!q=4t3dCdzXAORx~6#teS^LP`YpN+^e*}e=y&Lb<_!8C z{T=8J=qAt~(Ji1q0eu_YL$`rWps#`cjP3yaC;ErxG`f$z0s0I27U-|&F3{i5cbd1* z1N1%6hv)~OljujFYV?!l6u7JJ0o9-hpa?PAq%#UgPycM-6SyL-NJu>@Q~^vqp*Pw; zjL6j0bUyv$kGZx^hV?@%c-e(W1R6vaG{+3k7&Adr%mNKz2%5ndG=eE;0&~y+mVo@$ z&<|TcdVAym=^#F z0?I}Mx+(xwm4K%4fTD?jp2>ikseqR0fRdSjj@f{UxqyawfPw{peno(KC4hEifO0CB z3tgf|^oap6C04|ah=~JnB+kT%xDYquLEMQa@ghFNoA?qxA|V09pGZj%2_zvTn1m7; zZU!`~1Z1qkt$-cpct> z$MKK&A=VH}VhuA>NKTM<$$R8)WP-sA!{{=z7$;^0vx-^A>|n;3*O>R1i_AyN71o9I zW8>HiwvugQ2iPU-MfMZ+EB2oPESN1Yw!?N&c8l$PvYW8`%|6XxhJ&etor9x;tAoTr z>Ja82cThTXJ1lV6<*?V`fLI_l7MqD3#javsahW4=6gtjwG;*|Zv~%=wOmtK^*0`Mi zp4@-?A+ zWS6mjVLxSWu=fQ-U}%TzBJGClw%gsa`x(ZH9E=^z9UL57V5|WSvPWZm4#xVzzl^nb zYOIEjjkOxaieao6#ySaO#jH12KnS!aqPU=W6U06aQZytvq%cUKkb)sm?`i;)!Q9W> z*SH7f=_qL4L9T*}M*G#5)w|Va)Q8m1sduWks5hz?L&;IiB08ID0bEy4{x*3ZJkv)Z zy$ES1q-PQ04nsNw*U;*%hFcytKf9%Wv*niFO}nr15c(?ZtJJTOzl!<0-&eA~yL~13 z3Sa*Pq3ht^z20-Z{(9~8;xAWU&%EyZ<-%)wzwG(4`^%Ou8^0|3Qt_qNm+oJjF&@*q z2yD9^Z+wJhH=#Q;!FU11DR(0b-egSUmeYxj89d5-92JVdm5R^)lwc)&gj4AocvxEu z+C=y|v<_iS;t2_AC1)UAhIB_;=66c2{(k-Y6}bRqA^mrMWY1JNz+KFN#0fy=MSvE7 zV`u@vfMaGdj z5ydUCjyy>=kf+H;@(f`Ji`J1Dpm&}}`x$-o0yC2_U}jN-VaynF#sVE=EEy|wh_PmD z&^Tj@USjOfVaA?uKrb_5ut<(Fj({M}j0@w+xH0aG2YQq7V!Y8Q#uF@z(_mSg0gK`+ zdYAD5JK{a2m64(km_T%q31Who5GE8*>=OEr31ixraP$!)M;|j0OeDI@M1iGn1%1Lq zGcxok6T>K&SVqajG4V_S*b1LBiA)mc`rCjCcbH_*?%$wqnH13J-+?awfk{O_GHFaY z`iaRv_n1tkoykIKCL3`~4wK8wVe&wbV3WnApwZP$6&YH32%zS16GmIl} zBw(;Bj>6Hbm~~`lvxck_>x^TVl{{WQ0VuYbS%VcgmNjCHncd7Dti*B5US=Qb!Fu9& z=6TkO+0VQ{^%U#FdgDG;0NM#O6VuGJ{7<|5PrLk2yHLdbpLY45cKM%n`TtqFJZ>^s zz>aSZcE%g<9{dqOVByF~9;pWVWCb}-t}-S}Ewh9<3%b;uUB!OR-e&I!gaRvpuV6^1 zCv+Fq3U>)F3hxLxky2D5suzuk4v0>Ou8Dru3Dl|7*`xERuDh;Ww^nyZ_gUSOx>t1X zf{WBZ&p|IxPobBsw@dH(47(XcGd9jRsc)bkpr5HfU;kPCefroj;7Bzw++=vlDAZ`!XqC~k##YAe z#+Ak+#)pmXn+Q$3P2x=|O_rE!G`V7O!{ldEp{a#wrRfUO1E!pruh|lFVqRjt!u+a* zjYYggwne+eDvMnfmn`)xGc8Y9UbC{Ysr2+RZ5(V8Z8~k1 z*&MSuYjfG=nk}+5v-P$Ov#qynw;i$FXnWoEzMYYsvz@|jh23#`WS?oj%l-uH!5SS7 zI9wLb76*u1#plF7I|exRJDza7>15_q>GZ7A_s$CE`OcSISeJO06)tC7?z!r@%3T{> zH@e<+BW|v4d2YMhPPpB4bkQA7dn$(}vpInlB zF!@G`FvTGyETuYSC}m^HN2vy>!Ks<4wW+&OPo$npy^|(PGfN9iOHW&pb~x=`x1Wb!WRMK23`s_HMsLQ!jLA%^%#zHVnXhE&Wo2h=%-WyzdA41)Ji9mhc=p8{ zha6Q-YtF8mGdUmSaJde->ABZ(Z_lxtQ#)sW9-Eh#SDQDQw>|Gf-tBzH{P_GO`DY9C z3&INK7o0D+T4+)jUszkXtMFKnUQuFENzullE5$~|mBrhOua;Pps}jJ8(o`PTTokF+g>|VyR3G7?e^LOwa04D)LyE+T6?GV=Q>oUUuRM0TqmiM*Cp2F z)~V{6>-y_P>(>QuD@9SdHwDB zd-Ys{UV~YKV}ox)SVMe6c0*Z1V?%GlNW)meriMKYhZ{~doNu_&aHHY-h6jzpMx#c% zM$g9JMnz+KV^L#mV`t<1#ubel8h17xY&_n0wh1-qH(4|}H%Xf0O^HppO{%7iO}m=L zn@%*HYr5QYz3Fb#ugz?;Z*y34d~N$XhahSsgEds+{+zS4TK^=#|K)+?eT5p>2&Dy?hNfzc4l^}I-5KDJ4ZX$bZ+e2-g%(&Sm&9}OPyCc z?{xm$g}QXQW_MY1Id@6ALc5e*nO!Aa^^CeuKUyOJKaC`pdOtblOBg2 z@1D>eWlv^LNl$%Gch7Los-BHKyL!fZPW4>sx!QB5=jUG3tKVzU>)b2pjqc6tE$OZA z?d~1!UDdm>cUSLt?}^@Xy_b8h_ulROwNIzdqR+Wc(kJgr?91&_^)>hP_l@?g>3gA%!} zwf|24&jV;cf52kEc|bBCA4nX?9Z(JI8F*#j^uWb|&j)S~+#BEq^#;ub9S3~}!v^CA zvj@ut8wYy_M+V0RHw<2wOXk|l4W3&xcX;lax!dO+n0svQnYowdUY&bqNN31(D08TP zXn1JV&<6N-983&?YM7r%v=ELTf`1!f#>r4BpadH`tvrZ*fC&1yc|?CXCA7R8if$B) zgQwdb$~!Sm#!f+EK1ms=F(gwbJ^?j$A}@TEx;OP}EC`#xhUt6Lucz-zzw~|B-yWv! z?fWY1E4+!T#hbQb**3hLQ_-}Idv_~WOJG9}l|!`6hjkTh22VJ`fxg7emkA1tBq2dk zQ#;1g2|jUh1n0Ji9WjS{69XbLbqazqQuss~5X?xeWF-pKv#s)=$`yIr=PWG?xMv%j z?jMvYwXl$t$5(7>4hyYaTevNAeP`T+S6se#XCEH&j1LPi7iToCEX`e67p?dCRsE#C z9W_za@ve43$=;qynbguLqhUp9!OEHl-M3HchPCYNNvoAxwnChsN^@7RT<{J=1iYZn z`QVL5#?WDDfE}^L&_5?fUozFZND@JZ#18=f7i9!)*u2Ukqbn(;E8W8*y)!wvE7N1Y zp;Mr!hSh?7&0qajD;k$2~@8zWn?C6erx z$h3%-EQutmCBiP$)5^*-)Go~~)XU1sE7WfCgNRo8s5K%zvN_w&FS|K%`ce9H;a?Nk z&P3oN24b#v1%`q%5Z?#~Yn2~&h<3f`@agKz!Z;Gc@P#6Cv6}&=lOYHQ4#6S=SUfwz zn3$P|M8XPdXC==)v0CmF>Sh*FJ(i!dq)ciPnB-lBEo~h>mMgAsx3e=(*0zq%Rp1*D zJ%QY5dk;TLk$psAYN%gLw6E~T&YnYn}ULM_0oa5!4ossF8zr0);SlFuwEKHAc zNaT*lmZUDcR-~?w%OYAgF7$NAeP;IZAYUU%cBB)tKcFs5ZtCXgZ*LjkW?}6Y0sNTF zOyU{5tY{?#&#>C|nqUz(06sv zuZ>?~cc>V9T-)fm%(8shOIIF@(H#ax)+CMT26;As8GN8Gx)Hz)omz3=XK6iL00X)IEmAAKQ zQ>%6^_dzE#f};?%LSiRFL6D1-Xzp>FNvJ)$cJKpwHXK9r53$kEM`wgQa1j1oZ~z#m z`bno1i`nQ&AN;vI!_U|@Br`CoG{Sx&X8y6>mi;{u-s#QJ-0Ni5ULsJ`%}I6%m=o#b zo=_F8dVYSAtp2HD<>JxBvD_}cy$qNWQ)mzHA4>yZ%z>^vc>POjS{Qx|=7Om>dttm} zO;kywT}!(pTkbd!H}{2_`U7*7Hlewpv87?w$%|gDFMn|~eZn!aAh=`1HNGlx4U-@a z%MPgTvJFb{%UjnZ531i>>s^$SWNDsISQ}isy+sySx2Z;wDYNhIj4TSb;nyt?=A;1D zRFCIo&_t9A-GHmah9c&U)aLXY#@$vzCpQH5^@%R=V|KXL<7nM>n=csFSR` z<&G)&iwk_b(wk!6*DwY3KqCW#{o zg3DSIz0Xxt?CMo~>==Yi5X1Q$j*IYku)(@7a3YDs38Lr;x=mdxH>mIWA4 zs(HW~0l)KW;Q|aFqE%VbeWwbY^#aMGkEWFt?K+Mfd8su}7_xRw)0PHlRR5ul{+IjY zHj3tLpFUxNS+~Rp$!yn{JV{7in7xfG|Lv%@jRj!|nM+?%HN3VWvwY_#{TU7Ck8Es> zd=GzN;Th&wp1-8XSCZ2fC2!4zulgZ~d5!?qZDF(ET*2&z2+k(V);wgg;B#NV)MfCw z8+>vbqBTNf0E~s3BS6P*bw^CBJy23xOdQGvS zv%175pf81c&qm^G0>Ez*lAKKWnV@1WT==nI4dAE>`eq=2R;P>-=p^l#IV02xSV%A^ zT98FdL_$#IROUdlGfs}qt}q~hG=McT!a_6tb7NXkC_K-Yd&idcr4$d^N|z=lw;X_cZt6 zg(@9kWYdNd*N5N29%suZg)@`0OeGQ@BOg$X)y7&FpepX!~Jl$sjV^<4GH$4_P& z+PRsjKZs28njO)&I%gbPz0=rth5Ky$HSVi->Sh>Q8!k5&o6VT_9nSw^*{_#h7>HT; z9e0@fXxS4+p2`6Hoz4uZTL_$MK>`KHx(JXI_<#{p2!)(^TYaII`;EB%ow#wos-MMs zKE^rRi-Pe7nZ%xyz`G2b5JT*9@U98$58M%w8j5)YeS`uSKG13p7qHQR8@%v(Jkfh% zB(Aw0l#^ZDqSt#NIBywQ9_`RM%w7Gpck$ej`+{*l)l;?A+r15A3L0e%+nR#OT2AH= zo*mFUbiApu8P*W^vKFxCTi`dUwdn%+QX^XnHRevf@PPp}$EH@5{Z=n??1LpsK6@rR z_u0QKA33)&W5%S8TViW^-oiXD*Ub3^*&WFqy7=>^qoc_cFLPUaw`h)3rYt_z64#pM z9y9kySAWal1@W%Q&GB^3=(s8-DXlvqDsGPm|@OJF{S2~lwd%%x})|m=*@-~$rwW|2Ue68M3UY|^4 zUQ(xmA9w{9eO@qrfuqI)oDGL}{NWwCo8zr8`WEwr`V}>^MXe&qq(S{OEu8^n8)3eF zg!%FV8;qYXOuK2MRXP7YV-yY8AI-BD9vxhMYgcjhrfW|uKD#V^=C8V*NlnRxi}Jk! zD^?c7ltz2ZqJydMXs|4!sE)}BKg*bVAAFJewTck-`O z@3OAY4>&pELRU?S*OI3uTh-@bt)`v}8{rb{Ze2o$YYbY|7;=0S@50aFgh@*-`zV)V z3B!80i!FJOiLaB9hg;bS^#ab}Q5^zvxP-M9J7L;E3^Qh~R`<~vd&ufO`~_Z~U}pk< zf@}!vV69Ez92#h4!fUg~)*Bl=vTLVg#3)E$m%sJe(k0h+6c%o~wq)s-+w#?ZMT>H> zMvDFYOBQAAJ4z1k;eLCyvhr0d+LOEo>l~}BJjVUHhYY?xKPG1W>w~F-ClKDctzEQ$05+8vuT>MD1@A0NNLcpH0`3x)BsD1Q?UfBl+$CbG{<~m2 zZfJjXV2?_ zJ0B6hSUbYS%dT(B=Z@{mT3GBKP&|_LJdE!z+=qp4um9)Fe;eX`?q3GT_k+sbH5lV; zI0J~Z(l-Tu*c9-S$2~es@&b2TP0+kEOXHuIj}y3`)Tc44tW`QXDC!b9HSt&fMKCUF z+ts}8ognp&nPk#Ax*#aDIK~Nh5>869D`2eDK?3&Dluv|LrN4Vb%%&V7k6Yr*6IO8p z<@MtW5>u9)?3#OKY2u_TS5Nvr6qhr#p6ZG4At@3k$Nf z&um;#Sg^iXo-nwhIH)kj*)BNQE34Vpxu9)je#w({A+oLm-Emb>woNbvRQ_NN#$gV4 z3xX~)V8%EX_K=HY7XiN6us7*>APlFK@3L<|IY3MNNK8TIr>$B;_Ki8~&vx{@yDodu zBegj;wk5>_1T|pZiTP1c^H0oUE=~rdcO*MGC3mDV7qx9uy=4n^@;0iNTB+nII811! z{`sHk`(UO-uw4%_;W=#|%v#|sxTdmgEX7XLsFfOHsX*`gLvH*Y_bqp8f*XH$1DjpO z^=#sUOxEq;XCO_-S%7SKlEP3_Sss z3FTKod60LCDM8dkGsPo}-orEGC$$M{fByU~Hu}PLTK6b-g4hVh_;mnapjv>~?B31Q zj1U<}kkKmux2 zZk%jlo_Z5b>%lXB_Culz6~Hro(R9yq_pmAZAqK@n<+}(l{UO+!UT{JdiNWikRqoUj z4~NafltDRdK^mHvoAL&G5SU`TXNl39DX<7n$nncsRuQ6@|7y=OWh$?Df9r{;?%lP; zo7y8i)N0S9aPOImmM$~%jxr9kv-B1l`{(z@f$i;=kXvK!7Hl8gR~qV9y0VmWSeI{j z$jsT#;zC)TIMf3a4ejF)@N)o+!4$0TDW8F>n}}+$sU4hFI)x%btqB>#281+jt*crm z)xSC+U0K?&wJCVQJhU`^PP46TOI~b=%z_y8pI?`qpy&}I^+)pPtaa}XBn+!$DN&pv zDm|op7%W)~--@`){F9#>#1C5f|1x}t%sWlyL~bz&eOrs>y*5`apL=Xx<&Lfx<`%IJ zN%v1yg_@d%mLIhCSGuRKs0+_qf40r3?d(%|^5zXiUg=?u5tZ|!ofPvb!3zLmral2? zddyl+rVE-Cq~V4OOiP;+g8mpk`%`Ad<^(Tt_V%FR-RLkRJ$KE z0|y_q{B1GyI()QycTnXM1x|4eLK8o$+@h;jnI%VRi&mBS>*<_i*@8K>Mx;m3%j+1xC@OEj#?;zq4&7LqF;WsRQBjuKBA%FF&cq~n zS8Q%oCz9jsrSbB~hwwHiI$+c_g1>@%&ziE78a<>Y| z0(f`Y{)AD$9foi0AmM;k9;f;ZPyJ?j~~yCZkB z*nh%Wn&6YuV2a@)(I>seW`Ygth>Fk1Pc8DP*x00wB`4)+zD9+Kf*As}A9-DtE-{S` znf#Fjc%*&c*A4nWZQy@zoiRhv5>|U09QE`oGhnpKZdK6`KAZ< zZ@4N6Y1&d#x+-wux*mW`-PWd%d$#g2MP|L7U0sf%RBl6zXp?f8S|j+pH7Yi9?fY|+ zhE*X78RsC2lU0mB!!TxOmrS?&m~1*3{)seZgtcz3EgO>>d^#arR#>yGHEhC0UaH8b zb8x6nRg^{85ZAdE)@CXqnjW~~$>6Y@Cjr|QR>)#Q@!jBPXqGZO&p+e{v}*`AJf!fF zHpNNgWeb0+f>!H^J3^0vY||ipri5jca-@3x|J9Nx>ct0H8|5N+$QN&DZ zAo7q&zAy|ZPRd_hD+_Dctg6}39zJ0qN$~Sk`k0wW^J2=Lstbj)hiW!ML|KO^ONkueXY%1{H?7$9c>Lvg3}w568rNdQMC(W1C*Y| z+A->A?vhSHj8>v4U~2t;A*NGPYY4b%h+lm3&DnMymX>}qZPS9~bt#^Q4+~|9to$C0^+y zN%eLNaW`X(@}6vz8#+oIz5J{L;ym-$)P)*328expY=aVEvrEttZasd6P1EXDV=1r! zuOfqZ&A9(V3r>SYi7`~U#w3yr4>>_dXi7?GNJ;^dCHf`2F+TEJNo<}XR7n5W7 z;n&%g4n`u;j?Fr;1AD6KceaK7Qyh}$<`FNo)5ReqZ#<^W`}}#KcSM~1YwzBxd#)`! ztZ{u&NUFrtPM$Bj_b#vs%wehKmLP-u3BIidb{LNwV7j~Ufr{V|w~#6D4KW8chNvQV z7p9$v(|&(8BIQ_J?W-vZpI9mh43n;TVl4SkW#voBYsc10L#2Kz$0qxu+cwOZ^Hf`O zblX#N=4@z-Zj#3; zN_O-qm3=#4yAlY0)Aa?ee2TX7{M zOZdo=2_!@6;EgMu=03(gPp{ZW&coQ3fef|qG6W!MY$&y)lC3d-43q-jte6bKs>GQDY^Ryb7FrUNB^JgDJ9_iykn)j`xcp>xQt zD#}vCC5|5NRE+f1rg+{9%&iZMs#G}HCl8gh9UP1<$cj-WjK1F0dtx+x!ZEfgMp+eY zZ=Eo=qGNm@O8tRXhEr&ub;6L!Gu)LJV0V?7hk!gq-3R6z_RK&D=LFRTejEx&xvxek;FKAMFeYPEW7nmz;o(YUcz7&62b5kZ1$hBV zWdJ1qsT`y>AN+DRU|!uIN>lthX7Ca?<<)uIZ50eC!8@>7m^YTfiik!81MFB>4Y>DZ z*)qTUp|q@pMgIOp3$xPZ<@-HbQXon4x3u(6k`xxHXO(QfGMKetq&d~oGqrhSL)PGx z?InZKl98=PD=Uv~87Yy%^%i=)6|PBcMsK=XXzl8}zPzzonH$`xa$I6dLi$5X6)sto zi$`zmQ+a20tlUvhuwzAgrdRpio1=|y?O9qL7+Ah^&s&Xfy-XDts9Lt?EuN2_ATKlY zoQN|;9XuU#0l1pl$qT^^sdd?#8p4SX7ud_eKAs-)F(axOLi@@@d1;h*-;1@f@R+0* zy|X(L6ium~`Uc!~_gw25?J1T&VvRB!YS@PM=dRW@2$K zW?z8QQq${Z4d3C-hVR?*Cq5ouhbf_Wv`zvlr_-?zst(P}O`Uv6Agr{jlN+xPKoY!g z{+}(y;D1^;eBx`xY>@joowCGHTD z4N>bDd7-^+dwi&?!hH5jcTd+D!3oMGvuwR=odfJ=!rUws7%^p2dY8^jnc9PVA~4!Y zWn%$zh^&P5HKz9Z^j={KDahkMK^Jv_XQC=NxGcfLBcUufSe59x(!|ln$_kt>#$s

9AxHZ~IbT3h=$85=u+hltSGXC~N5kVn{GK7RUy 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 }9naMW3e6&}Zp3`W)R(pQk(M3v{RU3Vo5jL|>-6 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 H=d=F@iuu`M literal 0 HcmV?d00001 diff --git a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java index f3c5a7dd..b6159857 100644 --- a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java +++ b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java @@ -18,6 +18,7 @@ package org.floens.chan; import android.app.Application; +import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.view.ViewConfiguration; @@ -36,6 +37,7 @@ import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.net.BitmapLruImageCache; import org.floens.chan.database.DatabaseManager; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.FileCache; import org.floens.chan.utils.IconCache; import org.floens.chan.utils.Logger; @@ -53,6 +55,8 @@ public class ChanApplication extends Application { private static final int VOLLEY_LRU_CACHE_SIZE = 8 * 1024 * 1024; // 8mb private static final int VOLLEY_CACHE_SIZE = 10 * 1024 * 1024; // 8mb + public static Context con; + private static ChanApplication instance; private static RequestQueue volleyRequestQueue; private static com.android.volley.toolbox.ImageLoader imageLoader; @@ -113,6 +117,8 @@ public class ChanApplication extends Application { public void onCreate() { super.onCreate(); + con = this; + // Force the overflow button to show, even on devices that have a // physical button. try { @@ -130,6 +136,8 @@ public class ChanApplication extends Application { // StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build()); } + AndroidUtils.init(); + ChanUrls.loadScheme(ChanPreferences.getNetworkHttps()); IconCache.createIcons(this); diff --git a/Clover/app/src/main/java/org/floens/chan/controller/Controller.java b/Clover/app/src/main/java/org/floens/chan/controller/Controller.java new file mode 100644 index 00000000..c4d803fc --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/controller/Controller.java @@ -0,0 +1,44 @@ +package org.floens.chan.controller; + +import android.content.Context; +import android.content.res.Configuration; +import android.view.LayoutInflater; +import android.view.View; + +import org.floens.chan.ui.toolbar.NavigationItem; + +public abstract class Controller { + public Context context; + public View view; + + public Controller stackSiblingController; + public NavigationController navigationController; + public NavigationItem navigationItem = new NavigationItem(); + + public Controller(Context context) { + this.context = context; + } + + public void onCreate() { + } + + public void onShow() { + } + + public void onHide() { + } + + public void onDestroy() { + } + + public View inflateRes(int resId) { + return LayoutInflater.from(context).inflate(resId, null); + } + + public void onConfigurationChanged(Configuration newConfig) { + } + + public boolean onBack() { + return false; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/controller/ControllerTransition.java b/Clover/app/src/main/java/org/floens/chan/controller/ControllerTransition.java new file mode 100644 index 00000000..e778f8e1 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/controller/ControllerTransition.java @@ -0,0 +1,22 @@ +package org.floens.chan.controller; + +public abstract class ControllerTransition { + private Callback callback; + + public Controller from; + public Controller to; + + public abstract void perform(); + + public void onCompleted() { + this.callback.onControllerTransitionCompleted(); + } + + public void setCallback(Callback callback) { + this.callback = callback; + } + + public interface Callback { + public void onControllerTransitionCompleted(); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java b/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java new file mode 100644 index 00000000..5f20d468 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java @@ -0,0 +1,173 @@ +package org.floens.chan.controller; + +import android.content.Context; +import android.content.res.Configuration; +import android.support.v4.widget.DrawerLayout; +import android.view.View; +import android.widget.FrameLayout; + +import org.floens.chan.ui.toolbar.Toolbar; +import org.floens.chan.utils.AndroidUtils; + +import java.util.ArrayList; +import java.util.List; + +public abstract class NavigationController extends Controller implements ControllerTransition.Callback, Toolbar.ToolbarCallback { + public Toolbar toolbar; + public FrameLayout container; + public DrawerLayout drawerLayout; + public FrameLayout drawer; + + private List controllerList = new ArrayList<>(); + private ControllerTransition controllerTransition; + private boolean blockingInput = true; + + public NavigationController(Context context, final Controller startController) { + super(context); + } + + public boolean pushController(final Controller to) { + if (blockingInput) return false; + + if (this.controllerTransition != null) { + throw new IllegalArgumentException("Cannot push controller while a transition is in progress."); + } + + blockingInput = true; + + final Controller from = controllerList.get(controllerList.size() - 1); + + to.stackSiblingController = from; + to.navigationController = this; + to.onCreate(); + + controllerList.add(to); + + this.controllerTransition = new PushControllerTransition(); + container.addView(to.view, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); + AndroidUtils.waitForMeasure(to.view, new AndroidUtils.OnMeasuredCallback() { + @Override + public void onMeasured(View view, int width, int height) { + to.onShow(); + + doTransition(true, from, to, controllerTransition); + } + }); + + return true; + } + + public boolean popController() { + if (blockingInput) return false; + + if (this.controllerTransition != null) { + throw new IllegalArgumentException("Cannot pop controller while a transition is in progress."); + } + + if (controllerList.size() == 1) { + throw new IllegalArgumentException("Cannot pop with 1 controller left"); + } + + blockingInput = true; + + final Controller from = controllerList.get(controllerList.size() - 1); + final Controller to = controllerList.get(controllerList.size() - 2); + + this.controllerTransition = new PopControllerTransition(); + container.addView(to.view, 0, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + AndroidUtils.waitForMeasure(to.view, new AndroidUtils.OnMeasuredCallback() { + @Override + public void onMeasured(View view, int width, int height) { + to.onShow(); + + doTransition(false, from, to, controllerTransition); + } + }); + + return true; + } + + @Override + public void onControllerTransitionCompleted() { + if (controllerTransition instanceof PushControllerTransition) { + controllerTransition.from.onHide(); + container.removeView(controllerTransition.from.view); + } else if (controllerTransition instanceof PopControllerTransition) { + controllerList.remove(controllerTransition.from); + + controllerTransition.from.onHide(); + container.removeView(controllerTransition.from.view); + controllerTransition.from.onDestroy(); + } + this.controllerTransition = null; + blockingInput = false; + } + + public boolean onBack() { + if (blockingInput) return true; + + if (controllerList.size() > 0) { + Controller top = controllerList.get(controllerList.size() - 1); + if (top.onBack()) { + return true; + } else { + if (controllerList.size() > 1) { + popController(); + return true; + } else { + return false; + } + } + } else { + return false; + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + for (Controller controller : controllerList) { + controller.onConfigurationChanged(newConfig); + } + } + + public void initWithController(final Controller controller) { + controllerList.add(controller); + controller.navigationController = this; + controller.onCreate(); + toolbar.setNavigationItem(false, true, controller.navigationItem); + container.addView(controller.view, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); + + AndroidUtils.waitForMeasure(controller.view, new AndroidUtils.OnMeasuredCallback() { + @Override + public void onMeasured(View view, int width, int height) { + onCreate(); + onShow(); + + controller.onShow(); + blockingInput = false; + } + }); + } + + public void onMenuClicked() { + + } + + private void doTransition(boolean pushing, Controller from, Controller to, ControllerTransition transition) { + transition.setCallback(this); + transition.from = from; + transition.to = to; + transition.perform(); + + toolbar.setNavigationItem(true, pushing, to.navigationItem); + } + + @Override + public void onMenuBackClicked(boolean isArrow) { + if (isArrow) { + onBack(); + } else { + onMenuClicked(); + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/controller/PopControllerTransition.java b/Clover/app/src/main/java/org/floens/chan/controller/PopControllerTransition.java new file mode 100644 index 00000000..01c72dea --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/controller/PopControllerTransition.java @@ -0,0 +1,38 @@ +package org.floens.chan.controller; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +public class PopControllerTransition extends ControllerTransition { + @Override + public void perform() { + Animator toAlpha = ObjectAnimator.ofFloat(to.view, View.ALPHA, to.view.getAlpha(), 1f); + toAlpha.setInterpolator(new DecelerateInterpolator()); // new PathInterpolator(0f, 0f, 0.2f, 1f) + toAlpha.setDuration(250); + + Animator fromY = ObjectAnimator.ofFloat(from.view, View.Y, 0f, from.view.getHeight() * 0.05f); + fromY.setInterpolator(new AccelerateInterpolator(2.5f)); + fromY.setDuration(250); + + fromY.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + onCompleted(); + } + }); + + Animator fromAlpha = ObjectAnimator.ofFloat(from.view, View.ALPHA, from.view.getAlpha(), 0f); + fromAlpha.setInterpolator(new AccelerateInterpolator(2f)); + fromAlpha.setStartDelay(100); + fromAlpha.setDuration(150); + + AnimatorSet set = new AnimatorSet(); + set.playTogether(toAlpha, fromY, fromAlpha); + set.start(); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/controller/PushControllerTransition.java b/Clover/app/src/main/java/org/floens/chan/controller/PushControllerTransition.java new file mode 100644 index 00000000..74eb91ca --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/controller/PushControllerTransition.java @@ -0,0 +1,37 @@ +package org.floens.chan.controller; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +public class PushControllerTransition extends ControllerTransition { + @Override + public void perform() { + Animator fromAlpha = ObjectAnimator.ofFloat(from.view, View.ALPHA, 1f, 0.7f); + fromAlpha.setDuration(217); + fromAlpha.setInterpolator(new AccelerateDecelerateInterpolator()); // new PathInterpolator(0.4f, 0f, 0.2f, 1f) + + Animator toAlpha = ObjectAnimator.ofFloat(to.view, View.ALPHA, 0f, 1f); + toAlpha.setDuration(200); + toAlpha.setInterpolator(new DecelerateInterpolator(2f)); + + Animator toY = ObjectAnimator.ofFloat(to.view, View.Y, to.view.getHeight() * 0.08f, 0f); + toY.setDuration(350); + toY.setInterpolator(new DecelerateInterpolator(2.5f)); + + toY.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + onCompleted(); + } + }); + + AnimatorSet set = new AnimatorSet(); + set.playTogether(fromAlpha, toAlpha, toY); + set.start(); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/loader/Loader.java b/Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java similarity index 92% rename from Clover/app/src/main/java/org/floens/chan/core/loader/Loader.java rename to Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java index 67fe619f..0655ba5b 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/loader/Loader.java +++ b/Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java @@ -29,9 +29,9 @@ import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; import org.floens.chan.core.net.ChanReaderRequest; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.Logger; import org.floens.chan.utils.Time; -import org.floens.chan.utils.Utils; import java.util.ArrayList; import java.util.List; @@ -40,13 +40,13 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -public class Loader { +public class ChanLoader { private static final String TAG = "Loader"; private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private static final int[] watchTimeouts = {10, 15, 20, 30, 60, 90, 120, 180, 240, 300, 600, 1800, 3600}; - private final List listeners = new ArrayList<>(); + private final List listeners = new ArrayList<>(); private final Loadable loadable; private final SparseArray postsById = new SparseArray<>(); private ChanThread thread; @@ -60,7 +60,7 @@ public class Loader { private long lastLoadTime; private ScheduledFuture pendingFuture; - public Loader(Loadable loadable) { + public ChanLoader(Loadable loadable) { this.loadable = loadable; } @@ -69,7 +69,7 @@ public class Loader { * * @param l the listener to add */ - public void addListener(LoaderListener l) { + public void addListener(ChanLoaderCallback l) { listeners.add(l); } @@ -79,7 +79,7 @@ public class Loader { * @param l the listener to remove * @return true if there are no more listeners, false otherwise */ - public boolean removeListener(LoaderListener l) { + public boolean removeListener(ChanLoaderCallback l) { listeners.remove(l); if (listeners.size() == 0) { clearTimer(); @@ -228,7 +228,7 @@ public class Loader { Runnable pendingRunnable = new Runnable() { @Override public void run() { - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { pendingFuture = null; @@ -260,13 +260,13 @@ public class Loader { new Response.Listener>() { @Override public void onResponse(List list) { - Loader.this.request = null; + ChanLoader.this.request = null; onData(list); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - Loader.this.request = null; + ChanLoader.this.request = null; onError(error); } } @@ -329,8 +329,8 @@ public class Loader { post.title = loadable.title; } - for (LoaderListener l : listeners) { - l.onData(thread); + for (ChanLoaderCallback l : listeners) { + l.onChanLoaderData(thread); } lastLoadTime = Time.get(); @@ -353,16 +353,16 @@ public class Loader { error = new EndOfLineException(); } - for (LoaderListener l : listeners) { - l.onError(error); + for (ChanLoaderCallback l : listeners) { + l.onChanLoaderError(error); } clearTimer(); } - public static interface LoaderListener { - public void onData(ChanThread result); + public static interface ChanLoaderCallback { + public void onChanLoaderData(ChanThread result); - public void onError(VolleyError error); + public void onChanLoaderError(VolleyError error); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/loader/LoaderPool.java b/Clover/app/src/main/java/org/floens/chan/core/loader/LoaderPool.java index 84e576de..26519f09 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/loader/LoaderPool.java +++ b/Clover/app/src/main/java/org/floens/chan/core/loader/LoaderPool.java @@ -27,7 +27,7 @@ public class LoaderPool { private static LoaderPool instance; - private static Map loaders = new HashMap<>(); + private static Map loaders = new HashMap<>(); public static LoaderPool getInstance() { if (instance == null) { @@ -37,33 +37,33 @@ public class LoaderPool { return instance; } - public Loader obtain(Loadable loadable, Loader.LoaderListener listener) { - Loader loader = loaders.get(loadable); - if (loader == null) { - loader = new Loader(loadable); - loaders.put(loadable, loader); + public ChanLoader obtain(Loadable loadable, ChanLoader.ChanLoaderCallback listener) { + ChanLoader chanLoader = loaders.get(loadable); + if (chanLoader == null) { + chanLoader = new ChanLoader(loadable); + loaders.put(loadable, chanLoader); } - loader.addListener(listener); + chanLoader.addListener(listener); - return loader; + return chanLoader; } - public void release(Loader loader, Loader.LoaderListener listener) { - Loader foundLoader = null; + public void release(ChanLoader chanLoader, ChanLoader.ChanLoaderCallback listener) { + ChanLoader foundChanLoader = null; for (Loadable l : loaders.keySet()) { - if (loader.getLoadable().equals(l)) { - foundLoader = loaders.get(l); + if (chanLoader.getLoadable().equals(l)) { + foundChanLoader = loaders.get(l); break; } } - if (foundLoader == null) { + if (foundChanLoader == null) { throw new RuntimeException("The released loader does not exist"); } - if (loader.removeListener(listener)) { - loaders.remove(loader.getLoadable()); + if (chanLoader.removeListener(listener)) { + loaders.remove(chanLoader.getLoadable()); } } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java index f0c4adbe..a8e9ed7f 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java @@ -27,8 +27,8 @@ import org.floens.chan.core.model.Pass; import org.floens.chan.core.model.Reply; import org.floens.chan.core.model.SavedReply; import org.floens.chan.ui.activity.ImagePickActivity; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.Logger; -import org.floens.chan.utils.Utils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; @@ -559,7 +559,7 @@ public class ReplyManager { try { final CloseableHttpResponse response = client.execute(post); final String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { listener.onResponse(responseString, client, response); @@ -567,7 +567,7 @@ public class ReplyManager { }); } catch (IOException e) { e.printStackTrace(); - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { listener.onResponse(null, client, null); diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java index e6cd20b0..df36acd7 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java @@ -41,7 +41,7 @@ import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.ChanPreferences; -import org.floens.chan.core.loader.Loader; +import org.floens.chan.core.loader.ChanLoader; import org.floens.chan.core.loader.LoaderPool; import org.floens.chan.core.manager.ReplyManager.DeleteListener; import org.floens.chan.core.manager.ReplyManager.DeleteResponse; @@ -54,18 +54,20 @@ import org.floens.chan.core.model.SavedReply; import org.floens.chan.ui.activity.ReplyActivity; import org.floens.chan.ui.fragment.PostRepliesFragment; import org.floens.chan.ui.fragment.ReplyFragment; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.Logger; -import org.floens.chan.utils.Utils; import java.util.ArrayList; import java.util.List; +import static org.floens.chan.utils.AndroidUtils.dp; + /** * All PostView's need to have this referenced. This manages some things like * pages, starting and stopping of loading, handling linkables, replies popups * etc. onDestroy, onStart and onStop must be called from the activity/fragment */ -public class ThreadManager implements Loader.LoaderListener { +public class ThreadManager implements ChanLoader.ChanLoaderCallback { public static enum ViewMode { LIST, GRID } @@ -80,7 +82,7 @@ public class ThreadManager implements Loader.LoaderListener { private int lastPost = -1; private String highlightedId = null; - private Loader loader; + private ChanLoader chanLoader; public ThreadManager(Activity activity, final ThreadManagerListener listener) { this.activity = activity; @@ -92,36 +94,36 @@ public class ThreadManager implements Loader.LoaderListener { } public void onStart() { - if (loader != null) { + if (chanLoader != null) { if (isWatching()) { - loader.setAutoLoadMore(true); - loader.requestMoreDataAndResetTimer(); + chanLoader.setAutoLoadMore(true); + chanLoader.requestMoreDataAndResetTimer(); } } } public void onStop() { - if (loader != null) { - loader.setAutoLoadMore(false); + if (chanLoader != null) { + chanLoader.setAutoLoadMore(false); } } public void bindLoader(Loadable loadable) { - if (loader != null) { + if (chanLoader != null) { unbindLoader(); } - loader = LoaderPool.getInstance().obtain(loadable, this); + chanLoader = LoaderPool.getInstance().obtain(loadable, this); if (isWatching()) { - loader.setAutoLoadMore(true); + chanLoader.setAutoLoadMore(true); } } public void unbindLoader() { - if (loader != null) { - loader.setAutoLoadMore(false); - LoaderPool.getInstance().release(loader, this); - loader = null; + if (chanLoader != null) { + chanLoader.setAutoLoadMore(false); + LoaderPool.getInstance().release(chanLoader, this); + chanLoader = null; } else { Logger.e(TAG, "Loader already unbinded"); } @@ -132,11 +134,11 @@ public class ThreadManager implements Loader.LoaderListener { } public void bottomPostViewed() { - if (loader.getLoadable().isThreadMode() && loader.getThread() != null && loader.getThread().posts.size() > 0) { - loader.getLoadable().lastViewed = loader.getThread().posts.get(loader.getThread().posts.size() - 1).no; + if (chanLoader.getLoadable().isThreadMode() && chanLoader.getThread() != null && chanLoader.getThread().posts.size() > 0) { + chanLoader.getLoadable().lastViewed = chanLoader.getThread().posts.get(chanLoader.getThread().posts.size() - 1).no; } - Pin pin = ChanApplication.getWatchManager().findPinByLoadable(loader.getLoadable()); + Pin pin = ChanApplication.getWatchManager().findPinByLoadable(chanLoader.getLoadable()); if (pin != null) { pin.onBottomPostViewed(); ChanApplication.getWatchManager().onPinsChanged(); @@ -144,11 +146,11 @@ public class ThreadManager implements Loader.LoaderListener { } public boolean isWatching() { - if (!loader.getLoadable().isThreadMode()) { + if (!chanLoader.getLoadable().isThreadMode()) { return false; } else if (!ChanPreferences.getThreadAutoRefresh()) { return false; - } else if (loader.getThread() != null && loader.getThread().closed) { + } else if (chanLoader.getThread() != null && chanLoader.getThread().closed) { return false; } else { return true; @@ -156,8 +158,8 @@ public class ThreadManager implements Loader.LoaderListener { } public void requestData() { - if (loader != null) { - loader.requestData(); + if (chanLoader != null) { + chanLoader.requestData(); } else { Logger.e(TAG, "Loader null in requestData"); } @@ -167,22 +169,22 @@ public class ThreadManager implements Loader.LoaderListener { * Called by postadapter and threadwatchcounterview.onclick */ public void requestNextData() { - if (loader != null) { - loader.requestMoreData(); + if (chanLoader != null) { + chanLoader.requestMoreData(); } else { Logger.e(TAG, "Loader null in requestData"); } } @Override - public void onError(VolleyError error) { + public void onChanLoaderError(VolleyError error) { threadManagerListener.onThreadLoadError(error); } @Override - public void onData(ChanThread thread) { + public void onChanLoaderData(ChanThread thread) { if (!isWatching()) { - loader.setAutoLoadMore(false); + chanLoader.setAutoLoadMore(false); } if (thread.posts.size() > 0) { @@ -193,23 +195,23 @@ public class ThreadManager implements Loader.LoaderListener { } public boolean hasLoader() { - return loader != null; + return chanLoader != null; } public Post findPostById(int id) { - if (loader == null) + if (chanLoader == null) return null; - return loader.findPostById(id); + return chanLoader.findPostById(id); } public Loadable getLoadable() { - if (loader == null) + if (chanLoader == null) return null; - return loader.getLoadable(); + return chanLoader.getLoadable(); } - public Loader getLoader() { - return loader; + public ChanLoader getChanLoader() { + return chanLoader; } public void onThumbnailClicked(Post post) { @@ -217,7 +219,7 @@ public class ThreadManager implements Loader.LoaderListener { } public void onPostClicked(Post post) { - if (loader != null) { + if (chanLoader != null) { threadManagerListener.onPostClicked(post); } } @@ -225,11 +227,11 @@ public class ThreadManager implements Loader.LoaderListener { public void showPostOptions(final Post post, PopupMenu popupMenu) { Menu menu = popupMenu.getMenu(); - if (loader.getLoadable().isBoardMode() || loader.getLoadable().isCatalogMode()) { + if (chanLoader.getLoadable().isBoardMode() || chanLoader.getLoadable().isCatalogMode()) { menu.add(Menu.NONE, 9, Menu.NONE, activity.getString(R.string.action_pin)); } - if (loader.getLoadable().isThreadMode()) { + if (chanLoader.getLoadable().isThreadMode()) { menu.add(Menu.NONE, 10, Menu.NONE, activity.getString(R.string.post_quick_reply)); } @@ -274,7 +276,7 @@ public class ThreadManager implements Loader.LoaderListener { copyToClipboard(post.comment.toString()); break; case 5: // Report - Utils.openLink(activity, ChanUrls.getReportUrl(post.board, post.no)); + AndroidUtils.openLink(ChanUrls.getReportUrl(post.board, post.no)); break; case 6: // Id highlightedId = post.id; @@ -296,15 +298,15 @@ public class ThreadManager implements Loader.LoaderListener { } public void openReply(boolean startInActivity) { - if (loader == null) + if (chanLoader == null) return; if (startInActivity) { - ReplyActivity.setLoadable(loader.getLoadable()); + ReplyActivity.setLoadable(chanLoader.getLoadable()); Intent i = new Intent(activity, ReplyActivity.class); activity.startActivity(i); } else { - ReplyFragment reply = ReplyFragment.newInstance(loader.getLoadable(), true); + ReplyFragment reply = ReplyFragment.newInstance(chanLoader.getLoadable(), true); reply.show(activity.getFragmentManager(), "replyDialog"); } } @@ -326,7 +328,7 @@ public class ThreadManager implements Loader.LoaderListener { } public boolean isPostLastSeen(Post post) { - return post.no == loader.getLoadable().lastViewed && post.no != lastPost; + return post.no == chanLoader.getLoadable().lastViewed && post.no != lastPost; } private void copyToClipboard(String comment) { @@ -341,7 +343,7 @@ public class ThreadManager implements Loader.LoaderListener { if (post.hasImage) { text += "File: " + post.filename + "." + post.ext + " \nDimensions: " + post.imageWidth + "x" - + post.imageHeight + "\nSize: " + Utils.getReadableFileSize(post.fileSize, false) + "\n\n"; + + post.imageHeight + "\nSize: " + AndroidUtils.getReadableFileSize(post.fileSize, false) + "\n\n"; } text += "Time: " + post.date; @@ -440,14 +442,14 @@ public class ThreadManager implements Loader.LoaderListener { .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - Utils.openLink(activity, (String) linkable.value); + AndroidUtils.openLink((String) linkable.value); } }) .setTitle(R.string.open_link_confirmation) .setMessage((String) linkable.value) .show(); } else { - Utils.openLink(activity, (String) linkable.value); + AndroidUtils.openLink((String) linkable.value); } } else if (linkable.type == PostLinkable.Type.THREAD) { final PostLinkable.ThreadLink link = (PostLinkable.ThreadLink) linkable.value; @@ -476,8 +478,8 @@ public class ThreadManager implements Loader.LoaderListener { currentPopupFragment.dismissNoCallback(); } - PostRepliesFragment popup = PostRepliesFragment.newInstance(repliesPopup, this); - +// PostRepliesFragment popup = PostRepliesFragment.newInstance(repliesPopup, this); + PostRepliesFragment popup = null; FragmentTransaction ft = activity.getFragmentManager().beginTransaction(); ft.add(popup, "postPopup"); ft.commitAllowingStateLoss(); @@ -492,8 +494,8 @@ public class ThreadManager implements Loader.LoaderListener { popupQueue.remove(popupQueue.size() - 1); if (popupQueue.size() > 0) { - PostRepliesFragment popup = PostRepliesFragment.newInstance(popupQueue.get(popupQueue.size() - 1), this); - +// PostRepliesFragment popup = PostRepliesFragment.newInstance(popupQueue.get(popupQueue.size() - 1), this); + PostRepliesFragment popup = null; FragmentTransaction ft = activity.getFragmentManager().beginTransaction(); ft.add(popup, "postPopup"); ft.commit(); @@ -519,7 +521,7 @@ public class ThreadManager implements Loader.LoaderListener { LinearLayout wrapper = new LinearLayout(activity); wrapper.addView(checkBox); - int padding = Utils.dp(8f); + int padding = dp(8f); wrapper.setPadding(padding, padding, padding, padding); new AlertDialog.Builder(activity).setTitle(R.string.delete_confirm).setView(wrapper) diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java index f9eade21..827268fc 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java @@ -26,8 +26,8 @@ import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Post; import org.floens.chan.ui.service.WatchNotifier; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.Logger; -import org.floens.chan.utils.Utils; import java.util.ArrayList; import java.util.Collections; @@ -314,7 +314,7 @@ public class WatchManager implements ChanApplication.ForegroundChangedListener { ScheduledFuture scheduledFuture = executor.schedule(new Runnable() { @Override public void run() { - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { timerFired(); diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java b/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java index 690d09b3..2bb9cc40 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java @@ -176,6 +176,14 @@ public class Loadable { title = Post.generateTitle(post); } + public void generateTitle() { + if (mode == Mode.CATALOG) { + title = "/" + board + "/"; + } else { + title = "/" + board + "/" + no; + } + } + public static class Mode { public static final int INVALID = -1; public static final int THREAD = 0; diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java new file mode 100644 index 00000000..2572e1d1 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java @@ -0,0 +1,291 @@ +package org.floens.chan.core.presenter; + +import android.text.TextUtils; +import android.view.Menu; + +import com.android.volley.VolleyError; + +import org.floens.chan.ChanApplication; +import org.floens.chan.R; +import org.floens.chan.chan.ChanUrls; +import org.floens.chan.core.ChanPreferences; +import org.floens.chan.core.loader.ChanLoader; +import org.floens.chan.core.loader.LoaderPool; +import org.floens.chan.core.model.ChanThread; +import org.floens.chan.core.model.Loadable; +import org.floens.chan.core.model.Post; +import org.floens.chan.core.model.PostLinkable; +import org.floens.chan.core.model.SavedReply; +import org.floens.chan.ui.adapter.PostAdapter; +import org.floens.chan.ui.view.PostView; +import org.floens.chan.utils.AndroidUtils; + +import java.util.ArrayList; +import java.util.List; + +public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostView.PostViewCallback { + private ThreadPresenterCallback threadPresenterCallback; + + private Loadable loadable; + private ChanLoader chanLoader; + + public ThreadPresenter(ThreadPresenterCallback threadPresenterCallback) { + this.threadPresenterCallback = threadPresenterCallback; + } + + public void bindLoadable(Loadable loadable) { + if (!loadable.equals(this.loadable)) { + if (this.loadable != null) { + unbindLoadable(); + } + + this.loadable = loadable; + + chanLoader = LoaderPool.getInstance().obtain(loadable, this); + } + } + + public void unbindLoadable() { + threadPresenterCallback.showLoading(); + } + + public void requestData() { + threadPresenterCallback.showLoading(); + chanLoader.requestData(); + } + + @Override + public Loadable getLoadable() { + return loadable; + } + + /* + * ChanLoader callbacks + */ + @Override + public void onChanLoaderData(ChanThread result) { + threadPresenterCallback.showPosts(result); + } + + @Override + public void onChanLoaderError(VolleyError error) { + threadPresenterCallback.showError(error); + } + + /* + * PostAdapter callbacks + */ + @Override + public void onFilteredResults(String filter, int count, boolean all) { + + } + + @Override + public void onListScrolledToBottom() { + + } + + @Override + public void onListStatusClicked() { + + } + + @Override + public void scrollTo(int position) { + + } + + /* + * PostView callbacks + */ + @Override + public void onPostClicked(Post post) { + if (loadable.mode == Loadable.Mode.CATALOG) { + Loadable threadLoadable = new Loadable(post.board, post.no); + threadLoadable.generateTitle(post); + threadPresenterCallback.showThread(threadLoadable); + } + } + + @Override + public void onThumbnailClicked(Post post) { + + } + + @Override + public void onPopulatePostOptions(Post post, Menu menu) { + if (chanLoader.getLoadable().isBoardMode() || chanLoader.getLoadable().isCatalogMode()) { + menu.add(Menu.NONE, 9, Menu.NONE, AndroidUtils.getRes().getString(R.string.action_pin)); + } + + if (chanLoader.getLoadable().isThreadMode()) { + menu.add(Menu.NONE, 10, Menu.NONE, AndroidUtils.getRes().getString(R.string.post_quick_reply)); + } + + String[] baseOptions = AndroidUtils.getRes().getStringArray(R.array.post_options); + for (int i = 0; i < baseOptions.length; i++) { + menu.add(Menu.NONE, i, Menu.NONE, baseOptions[i]); + } + + if (!TextUtils.isEmpty(post.id)) { + menu.add(Menu.NONE, 6, Menu.NONE, AndroidUtils.getRes().getString(R.string.post_highlight_id)); + } + + // Only add the delete option when the post is a saved reply + if (ChanApplication.getDatabaseManager().isSavedReply(post.board, post.no)) { + menu.add(Menu.NONE, 7, Menu.NONE, AndroidUtils.getRes().getString(R.string.delete)); + } + + if (ChanPreferences.getDeveloper()) { + menu.add(Menu.NONE, 8, Menu.NONE, "Make this a saved reply"); + } + } + + public void onPostOptionClicked(Post post, int id) { + switch (id) { + case 10: // Quick reply +// openReply(false); TODO + // Pass through + case 0: // Quote + ChanApplication.getReplyManager().quote(post.no); + break; + case 1: // Quote inline + ChanApplication.getReplyManager().quoteInline(post.no, post.comment.toString()); + break; + case 2: // Info + showPostInfo(post); + break; + case 3: // Show clickables + if (post.linkables.size() > 0) { + threadPresenterCallback.showPostLinkables(post.linkables); + } + break; + case 4: // Copy text + threadPresenterCallback.clipboardPost(post); + break; + case 5: // Report + AndroidUtils.openLink(ChanUrls.getReportUrl(post.board, post.no)); + break; + case 6: // Id + //TODO +// highlightedId = post.id; +// threadManagerListener.onRefreshView(); + break; + case 7: // Delete +// deletePost(post); TODO + break; + case 8: // Save reply (debug) + ChanApplication.getDatabaseManager().saveReply(new SavedReply(post.board, post.no, "foo")); + break; + case 9: // Pin + ChanApplication.getWatchManager().addPin(post); + break; + } + } + + @Override + public void onPostLinkableClicked(PostLinkable linkable) { + if (linkable.type == PostLinkable.Type.QUOTE) { + Post post = findPostById((Integer) linkable.value); + + List list = new ArrayList<>(1); + list.add(post); + threadPresenterCallback.showPostsPopup(linkable.post, list); + } else if (linkable.type == PostLinkable.Type.LINK) { + threadPresenterCallback.openLink((String) linkable.value); + } else if (linkable.type == PostLinkable.Type.THREAD) { + PostLinkable.ThreadLink link = (PostLinkable.ThreadLink) linkable.value; + Loadable thread = new Loadable(link.board, link.threadId); + + threadPresenterCallback.showThread(thread); + } + } + + @Override + public void onShowPostReplies(Post post) { + List posts = new ArrayList<>(); + for (int no : post.repliesFrom) { + Post replyPost = findPostById(no); + if (replyPost != null) { + posts.add(replyPost); + } + } + if (posts.size() > 0) { + threadPresenterCallback.showPostsPopup(post, posts); + } + } + + @Override + public boolean isPostHightlighted(Post post) { + return false; + } + + public void highlightPost(int no) { + } + + public void scrollToPost(int no) { + } + + @Override + public boolean isPostLastSeen(Post post) { + return false; + } + + private void showPostInfo(Post post) { + String text = ""; + + if (post.hasImage) { + text += "File: " + post.filename + "." + post.ext + " \nDimensions: " + post.imageWidth + "x" + + post.imageHeight + "\nSize: " + AndroidUtils.getReadableFileSize(post.fileSize, false) + "\n\n"; + } + + text += "Time: " + post.date; + + if (!TextUtils.isEmpty(post.id)) { + text += "\nId: " + post.id; + } + + if (!TextUtils.isEmpty(post.tripcode)) { + text += "\nTripcode: " + post.tripcode; + } + + if (!TextUtils.isEmpty(post.countryName)) { + text += "\nCountry: " + post.countryName; + } + + if (!TextUtils.isEmpty(post.capcode)) { + text += "\nCapcode: " + post.capcode; + } + + threadPresenterCallback.showPostInfo(text); + } + + private Post findPostById(int id) { + for (Post post : chanLoader.getThread().posts) { + if (post.no == id) { + return post; + } + } + return null; + } + + public interface ThreadPresenterCallback { + public void showPosts(ChanThread thread); + + public void showError(VolleyError error); + + public void showLoading(); + + public void showPostInfo(String info); + + public void showPostLinkables(List linkables); + + public void clipboardPost(Post post); + + public void showThread(Loadable threadLoadable); + + public void openLink(String link); + + public void showPostsPopup(Post forPost, List posts); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java b/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java index 3354990f..1045b4cf 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java +++ b/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java @@ -20,22 +20,22 @@ package org.floens.chan.core.watch; import com.android.volley.VolleyError; import org.floens.chan.ChanApplication; -import org.floens.chan.core.loader.Loader; +import org.floens.chan.core.loader.ChanLoader; import org.floens.chan.core.loader.LoaderPool; import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Post; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.Logger; -import org.floens.chan.utils.Utils; import java.util.ArrayList; import java.util.List; -public class PinWatcher implements Loader.LoaderListener { +public class PinWatcher implements ChanLoader.ChanLoaderCallback { private static final String TAG = "PinWatcher"; private final Pin pin; - private Loader loader; + private ChanLoader chanLoader; private final List posts = new ArrayList<>(); private final List quotes = new ArrayList<>(); @@ -45,19 +45,19 @@ public class PinWatcher implements Loader.LoaderListener { public PinWatcher(Pin pin) { this.pin = pin; - loader = LoaderPool.getInstance().obtain(pin.loadable, this); + chanLoader = LoaderPool.getInstance().obtain(pin.loadable, this); } public void destroy() { - if (loader != null) { - LoaderPool.getInstance().release(loader, this); - loader = null; + if (chanLoader != null) { + LoaderPool.getInstance().release(chanLoader, this); + chanLoader = null; } } public void update() { if (!pin.isError) { - loader.loadMoreIfTime(); + chanLoader.loadMoreIfTime(); } } @@ -104,19 +104,19 @@ public class PinWatcher implements Loader.LoaderListener { } public long getTimeUntilNextLoad() { - return loader.getTimeUntilLoadMore(); + return chanLoader.getTimeUntilLoadMore(); } public boolean isLoading() { - return loader.isLoading(); + return chanLoader.isLoading(); } @Override - public void onError(VolleyError error) { + public void onChanLoaderError(VolleyError error) { Logger.e(TAG, "PinWatcher onError: ", error); pin.isError = true; - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { ChanApplication.getWatchManager().onPinsChanged(); @@ -125,7 +125,7 @@ public class PinWatcher implements Loader.LoaderListener { } @Override - public void onData(ChanThread thread) { + public void onChanLoaderData(ChanThread thread) { pin.isError = false; if (pin.thumbnailUrl == null && thread.op != null && thread.op.hasImage) { @@ -189,7 +189,7 @@ public class PinWatcher implements Loader.LoaderListener { pin.watchLastCount, pin.watchNewCount, wereNewPosts, pin.quoteLastCount, pin.quoteNewCount, wereNewQuotes)); } - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { ChanApplication.getWatchManager().onPinsChanged(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/SwipeDismissListViewTouchListener.java b/Clover/app/src/main/java/org/floens/chan/ui/SwipeDismissListViewTouchListener.java index bf06cf50..987a325a 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/SwipeDismissListViewTouchListener.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/SwipeDismissListViewTouchListener.java @@ -38,7 +38,7 @@ import java.util.List; * because by default it handles touches for its list items... i.e. it's in * charge of drawing the pressed state (the list selector), handling list item * clicks, etc. - * + *

*

* After creating the listener, the caller should also call * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)} @@ -48,11 +48,11 @@ import java.util.List; * {@link SwipeDismissListViewTouchListener} is paused during list view * scrolling. *

- * + *

*

* Example usage: *

- * + *

*

  * SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(listView,
  *         new SwipeDismissListViewTouchListener.OnDismissCallback() {
@@ -66,7 +66,7 @@ import java.util.List;
  * listView.setOnTouchListener(touchListener);
  * listView.setOnScrollListener(touchListener.makeScrollListener());
  * 
- * + *

*

* This class Requires API level 12 or later due to use of * {@link android.view.ViewPropertyAnimator}. diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/BaseActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/BaseActivity.java index 4abe6451..fb9f1e52 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/BaseActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/BaseActivity.java @@ -54,8 +54,10 @@ import org.floens.chan.ui.SwipeDismissListViewTouchListener; import org.floens.chan.ui.SwipeDismissListViewTouchListener.DismissCallbacks; import org.floens.chan.ui.ThemeActivity; import org.floens.chan.ui.adapter.PinnedAdapter; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.ThemeHelper; -import org.floens.chan.utils.Utils; + +import static org.floens.chan.utils.AndroidUtils.dp; public abstract class BaseActivity extends ThemeActivity implements PanelSlideListener, WatchManager.PinListener { public static boolean doRestartOnResume = false; @@ -141,7 +143,7 @@ public abstract class BaseActivity extends ThemeActivity implements PanelSlideLi private void initPane() { threadPane.setPanelSlideListener(this); - threadPane.setParallaxDistance(Utils.dp(100)); + threadPane.setParallaxDistance(dp(100)); threadPane.setShadowResource(R.drawable.panel_shadow); TypedArray ta = obtainStyledAttributes(null, R.styleable.BoardPane, R.attr.board_pane_style, 0); @@ -281,7 +283,7 @@ public abstract class BaseActivity extends ThemeActivity implements PanelSlideLi } }).setTitle(R.string.drawer_pinned_change_title).setView(text).create(); - Utils.requestKeyboardFocus(titleDialog, text); + AndroidUtils.requestKeyboardFocus(titleDialog, text); titleDialog.show(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardActivity.java index d2b26651..0c58d36a 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardActivity.java @@ -18,10 +18,13 @@ package org.floens.chan.ui.activity; import android.app.Activity; -import android.content.Intent; +import android.content.res.Configuration; import android.os.Bundle; -import org.floens.chan.utils.Logger; +import org.floens.chan.controller.NavigationController; +import org.floens.chan.ui.controller.BrowseController; +import org.floens.chan.ui.controller.RootNavigationController; +import org.floens.chan.utils.ThemeHelper; /** * Not called StartActivity because than the launcher icon would disappear. @@ -30,6 +33,37 @@ import org.floens.chan.utils.Logger; public class BoardActivity extends Activity { private static final String TAG = "StartActivity"; + private RootNavigationController rootNavigationController; + private NavigationController navigationController; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ThemeHelper.getInstance().reloadPostViewColors(this); + + rootNavigationController = new RootNavigationController(this, new BrowseController(this)); + setContentView(rootNavigationController.view); + + // Prevent overdraw + // Do this after setContentView, or the decor creating will reset the background to a default non-null drawable + getWindow().setBackgroundDrawable(null); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + rootNavigationController.onConfigurationChanged(newConfig); + } + + @Override + public void onBackPressed() { + if (!rootNavigationController.onBack()) { + super.onBackPressed(); + } + } + + /* @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -51,5 +85,5 @@ public class BoardActivity extends Activity { Intent intent = new Intent(this, ChanActivity.class); startActivity(intent); finish(); - } + }*/ } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardEditor.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardEditor.java index 74543edf..ab196bcd 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardEditor.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardEditor.java @@ -52,7 +52,7 @@ import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.model.Board; import org.floens.chan.ui.SwipeDismissListViewTouchListener; import org.floens.chan.ui.ThemeActivity; -import org.floens.chan.utils.Utils; +import org.floens.chan.utils.AndroidUtils; import java.util.ArrayList; import java.util.List; @@ -261,7 +261,7 @@ public class BoardEditor extends ThemeActivity { } }).setTitle(R.string.board_add).setView(text).create(); - Utils.requestKeyboardFocus(dialog, text); + AndroidUtils.requestKeyboardFocus(dialog, text); dialog.show(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/ChanActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/ChanActivity.java index c0607835..63b64b74 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/ChanActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/ChanActivity.java @@ -44,7 +44,7 @@ import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.ChanPreferences; -import org.floens.chan.core.loader.Loader; +import org.floens.chan.core.loader.ChanLoader; import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.ThreadManager; import org.floens.chan.core.model.Board; @@ -54,10 +54,11 @@ import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Post; import org.floens.chan.ui.fragment.ThreadFragment; import org.floens.chan.utils.Logger; -import org.floens.chan.utils.Utils; import java.util.List; +import static org.floens.chan.utils.AndroidUtils.dp; + public class ChanActivity extends BaseActivity implements AdapterView.OnItemSelectedListener, BoardManager.BoardChangeListener { private static final String TAG = "ChanActivity"; @@ -303,25 +304,25 @@ public class ChanActivity extends BaseActivity implements AdapterView.OnItemSele // Nexus 10 is 800 x 1280 dp if (ChanPreferences.getForcePhoneLayout()) { - leftParams.width = width - Utils.dp(30); + leftParams.width = width - dp(30); rightParams.width = width; isSlidable = true; } else { - if (width < Utils.dp(400)) { - leftParams.width = width - Utils.dp(30); + if (width < dp(400)) { + leftParams.width = width - dp(30); rightParams.width = width; isSlidable = true; - } else if (width < Utils.dp(800)) { - leftParams.width = width - Utils.dp(60); + } else if (width < dp(800)) { + leftParams.width = width - dp(60); rightParams.width = width; isSlidable = true; - } else if (width < Utils.dp(1000)) { - leftParams.width = Utils.dp(300); - rightParams.width = width - Utils.dp(300); + } else if (width < dp(1000)) { + leftParams.width = dp(300); + rightParams.width = width - dp(300); isSlidable = false; } else { - leftParams.width = Utils.dp(400); - rightParams.width = width - Utils.dp(400); + leftParams.width = dp(400); + rightParams.width = width - dp(400); isSlidable = false; } } @@ -335,10 +336,10 @@ public class ChanActivity extends BaseActivity implements AdapterView.OnItemSele LayoutParams drawerParams = pinDrawerView.getLayoutParams(); - if (width < Utils.dp(340)) { - drawerParams.width = Utils.dp(280); + if (width < dp(340)) { + drawerParams.width = dp(280); } else { - drawerParams.width = Utils.dp(320); + drawerParams.width = dp(320); } pinDrawerView.setLayoutParams(drawerParams); @@ -485,13 +486,13 @@ public class ChanActivity extends BaseActivity implements AdapterView.OnItemSele return true; case R.id.action_pin: if (threadFragment.hasLoader()) { - Loader loader = threadFragment.getLoader(); - if (loader != null && loader.getLoadable().isThreadMode() && loader.getThread() != null) { + ChanLoader chanLoader = threadFragment.getLoader(); + if (chanLoader != null && chanLoader.getLoadable().isThreadMode() && chanLoader.getThread() != null) { Pin pin = ChanApplication.getWatchManager().findPinByLoadable(threadLoadable); if (pin != null) { ChanApplication.getWatchManager().removePin(pin); } else { - ChanApplication.getWatchManager().addPin(loader.getLoadable(), loader.getThread().op); + ChanApplication.getWatchManager().addPin(chanLoader.getLoadable(), chanLoader.getThread().op); } updateActionBarState(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java index 8018c971..26a9681a 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java @@ -29,8 +29,8 @@ import android.widget.Toast; import org.floens.chan.ChanApplication; import org.floens.chan.R; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.IOUtils; -import org.floens.chan.utils.Utils; import java.io.File; import java.io.FileInputStream; @@ -96,6 +96,8 @@ public class ImagePickActivity extends Activity { FileInputStream is = new FileInputStream(fileDescriptor.getFileDescriptor()); FileOutputStream os = new FileOutputStream(cacheFile); IOUtils.copy(is, os); + IOUtils.closeQuietly(is); + IOUtils.closeQuietly(os); runOnUiThread(new Runnable() { @Override @@ -107,7 +109,7 @@ public class ImagePickActivity extends Activity { } catch (IOException | SecurityException e) { e.printStackTrace(); - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { ChanApplication.getReplyManager()._onPickedFile("", null); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java index 79f233b4..e7a0b1a7 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java @@ -175,7 +175,7 @@ public class ImageViewActivity extends ThemeActivity implements ViewPager.OnPage Post post = adapter.getPost(position); if (!threadManager.arePostRepliesOpen()) { - postAdapter.scrollToPost(post.no); +// postAdapter.scrollToPost(post.no); //TODO } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java index a7b1f0a2..5396649f 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java @@ -42,7 +42,7 @@ import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.ReplyManager.PassResponse; import org.floens.chan.core.model.Pass; import org.floens.chan.ui.ThemeActivity; -import org.floens.chan.utils.Utils; +import org.floens.chan.utils.AndroidUtils; public class PassSettingsActivity extends ThemeActivity implements OnCheckedChangeListener { private SwitchCompat onSwitch; @@ -118,7 +118,7 @@ public class PassSettingsActivity extends ThemeActivity implements OnCheckedChan link.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Utils.openLink(v.getContext(), v.getContext().getString(R.string.pass_info_link)); + AndroidUtils.openLink(v.getContext().getString(R.string.pass_info_link)); } }); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java index 75044185..bdce22ac 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java @@ -35,11 +35,12 @@ import org.floens.chan.R; import org.floens.chan.core.ChanPreferences; import org.floens.chan.core.model.Pin; import org.floens.chan.ui.view.CustomNetworkImageView; -import org.floens.chan.utils.Utils; import java.util.ArrayList; import java.util.List; +import static org.floens.chan.utils.AndroidUtils.dp; + public class PinnedAdapter extends BaseAdapter { private final static int VIEW_TYPE_ITEM = 0; private final static int VIEW_TYPE_HEADER = 1; @@ -122,7 +123,7 @@ public class PinnedAdapter extends BaseAdapter { if (pin.thumbnailUrl != null) { imageView.setVisibility(View.VISIBLE); imageView.setFadeIn(0); - imageView.forceImageDimensions(Utils.dp(48), Utils.dp(48)); + imageView.forceImageDimensions(dp(48), dp(48)); imageView.setImageUrl(pin.thumbnailUrl, ChanApplication.getVolleyImageLoader()); } else { imageView.setVisibility(View.GONE); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java index 41e52508..aeda67c4 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java @@ -18,13 +18,11 @@ package org.floens.chan.ui.adapter; import android.content.Context; -import android.os.Handler; import android.text.TextUtils; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; -import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.Filter; import android.widget.Filterable; @@ -33,19 +31,17 @@ import android.widget.ProgressBar; import android.widget.TextView; import org.floens.chan.R; -import org.floens.chan.core.loader.Loader; -import org.floens.chan.core.manager.ThreadManager; import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; -import org.floens.chan.ui.ScrollerRunnable; import org.floens.chan.ui.view.PostView; -import org.floens.chan.utils.Utils; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import static org.floens.chan.utils.AndroidUtils.dp; + public class PostAdapter extends BaseAdapter implements Filterable { private static final int VIEW_TYPE_ITEM = 0; private static final int VIEW_TYPE_STATUS = 1; @@ -53,10 +49,9 @@ public class PostAdapter extends BaseAdapter implements Filterable { private final Object lock = new Object(); private final Context context; - private final AbsListView listView; - private final ThreadManager threadManager; - private final PostAdapterListener listener; + private final PostAdapterCallback postAdapterCallback; + private final PostView.PostViewCallback postViewCallback; /** * The list with the original data @@ -75,11 +70,10 @@ public class PostAdapter extends BaseAdapter implements Filterable { private int pendingScrollToPost = -1; private String statusPrefix = ""; - public PostAdapter(Context activity, ThreadManager threadManager, AbsListView listView, PostAdapterListener listener) { - context = activity; - this.threadManager = threadManager; - this.listView = listView; - this.listener = listener; + public PostAdapter(Context context, PostAdapterCallback postAdapterCallback, PostView.PostViewCallback postViewCallback) { + this.postAdapterCallback = postAdapterCallback; + this.context = context; + this.postViewCallback = postViewCallback; } @Override @@ -130,7 +124,7 @@ public class PostAdapter extends BaseAdapter implements Filterable { } PostView postView = (PostView) convertView; - postView.setPost(getItem(position), threadManager); + postView.setPost(getItem(position), postViewCallback); return postView; } @@ -185,16 +179,11 @@ public class PostAdapter extends BaseAdapter implements Filterable { displayList.addAll((List) results.values); } notifyDataSetChanged(); - listener.onFilterResults(filter, ((List) results.values).size(), TextUtils.isEmpty(filter)); + postAdapterCallback.onFilteredResults(filter, ((List) results.values).size(), TextUtils.isEmpty(filter)); if (pendingScrollToPost >= 0) { final int to = pendingScrollToPost; pendingScrollToPost = -1; - listView.post(new Runnable() { - @Override - public void run() { - scrollToPost(to); - } - }); + postAdapterCallback.scrollTo(to); } } }; @@ -239,6 +228,7 @@ public class PostAdapter extends BaseAdapter implements Filterable { notifyDataSetChanged(); } + /* TODO public void scrollToPost(int no) { if (isFiltering()) { pendingScrollToPost = no; @@ -260,7 +250,7 @@ public class PostAdapter extends BaseAdapter implements Filterable { } } } - } + }*/ public void setStatusMessage(String loadMessage) { this.statusMessage = loadMessage; @@ -271,20 +261,20 @@ public class PostAdapter extends BaseAdapter implements Filterable { } private void onGetBottomView() { - if (threadManager.getLoadable().isBoardMode() && !endOfLine) { + /*if (postAdapterCallback.getLoadable().isBoardMode() && !endOfLine) { // Try to load more posts threadManager.requestNextData(); - } + }*/ if (lastPostCount != sourceList.size()) { lastPostCount = sourceList.size(); - threadManager.bottomPostViewed(); + postAdapterCallback.onListScrolledToBottom(); notifyDataSetChanged(); } } private boolean showStatusView() { - Loadable l = threadManager.getLoadable(); + Loadable l = postAdapterCallback.getLoadable(); if (l != null) { return l.isBoardMode() || l.isThreadMode(); } else { @@ -296,8 +286,16 @@ public class PostAdapter extends BaseAdapter implements Filterable { return !TextUtils.isEmpty(filter); } - public interface PostAdapterListener { - public void onFilterResults(String filter, int count, boolean all); + public interface PostAdapterCallback { + public void onFilteredResults(String filter, int count, boolean all); + + public Loadable getLoadable(); + + public void onListScrolledToBottom(); + + public void onListStatusClicked(); + + public void scrollTo(int position); } public class StatusView extends LinearLayout { @@ -319,20 +317,22 @@ public class PostAdapter extends BaseAdapter implements Filterable { } public void init() { - Loader loader = threadManager.getLoader(); - if (loader == null) + // TODO + /* + ChanLoader chanLoader = threadManager.getChanLoader(); + if (chanLoader == null) return; setGravity(Gravity.CENTER); - Loadable loadable = loader.getLoadable(); + Loadable loadable = chanLoader.getLoadable(); if (loadable.isThreadMode()) { String error = getStatusMessage(); if (error != null) { setText(error); } else { if (threadManager.isWatching()) { - long time = loader.getTimeUntilLoadMore() / 1000L; + long time = chanLoader.getTimeUntilLoadMore() / 1000L; if (time == 0) { setText(statusPrefix + context.getString(R.string.thread_refresh_now)); } else { @@ -348,7 +348,7 @@ public class PostAdapter extends BaseAdapter implements Filterable { } }, 1000); } else { - if (loader.getTimeUntilLoadMore() == 0) { + if (chanLoader.getTimeUntilLoadMore() == 0) { setText(statusPrefix + context.getString(R.string.thread_refresh_now)); } else { setText(statusPrefix + context.getString(R.string.thread_refresh_bar_inactive)); @@ -358,9 +358,9 @@ public class PostAdapter extends BaseAdapter implements Filterable { setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - Loader loader = threadManager.getLoader(); - if (loader != null) { - loader.requestMoreDataAndResetTimer(); + ChanLoader chanLoader = threadManager.getChanLoader(); + if (chanLoader != null) { + chanLoader.requestMoreDataAndResetTimer(); setText(context.getString(R.string.thread_refresh_now)); } @@ -376,7 +376,7 @@ public class PostAdapter extends BaseAdapter implements Filterable { } else { setProgressBar(); } - } + }*/ } @Override @@ -389,7 +389,7 @@ public class PostAdapter extends BaseAdapter implements Filterable { TextView text = new TextView(context); text.setText(string); text.setGravity(Gravity.CENTER); - addView(text, new LayoutParams(LayoutParams.MATCH_PARENT, Utils.dp(48))); + addView(text, new LayoutParams(LayoutParams.MATCH_PARENT, dp(48))); } private void setProgressBar() { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java new file mode 100644 index 00000000..1ca7916f --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java @@ -0,0 +1,102 @@ +package org.floens.chan.ui.controller; + +import android.content.Context; + +import org.floens.chan.R; +import org.floens.chan.chan.ChanUrls; +import org.floens.chan.controller.Controller; +import org.floens.chan.core.model.Loadable; +import org.floens.chan.ui.layout.ThreadLayout; +import org.floens.chan.ui.toolbar.ToolbarMenu; +import org.floens.chan.ui.toolbar.ToolbarMenuItem; +import org.floens.chan.ui.toolbar.ToolbarMenuSubItem; +import org.floens.chan.ui.toolbar.ToolbarMenuSubMenu; +import org.floens.chan.utils.AndroidUtils; + +import java.util.ArrayList; +import java.util.List; + +public class BrowseController extends Controller implements ToolbarMenuItem.ToolbarMenuItemCallback, ThreadLayout.ThreadLayoutCallback { + private static final int REFRESH_ID = 1; + private static final int POST_ID = 2; + private static final int SEARCH_ID = 101; + private static final int SHARE_ID = 102; + private static final int SETTINGS_ID = 103; + + private ThreadLayout threadLayout; + + public BrowseController(Context context) { + super(context); + } + + @Override + public void onCreate() { + super.onCreate(); + + navigationItem.title = "Hello world"; + ToolbarMenu menu = new ToolbarMenu(context); + navigationItem.menu = menu; + navigationItem.hasBack = false; + + menu.addItem(new ToolbarMenuItem(context, this, REFRESH_ID, R.drawable.ic_action_refresh)); + menu.addItem(new ToolbarMenuItem(context, this, POST_ID, R.drawable.ic_action_write)); + + ToolbarMenuItem overflow = menu.createOverflow(this); + + List items = new ArrayList<>(); + items.add(new ToolbarMenuSubItem(SEARCH_ID, context.getString(R.string.action_search))); + items.add(new ToolbarMenuSubItem(SHARE_ID, context.getString(R.string.action_share))); + items.add(new ToolbarMenuSubItem(SETTINGS_ID, context.getString(R.string.action_settings))); + + overflow.setSubMenu(new ToolbarMenuSubMenu(context, overflow.getView(), items)); + + threadLayout = new ThreadLayout(context); + threadLayout.setCallback(this); + + view = threadLayout; + + Loadable loadable = new Loadable("g"); + loadable.mode = Loadable.Mode.CATALOG; + loadable.generateTitle(); + navigationItem.title = loadable.title; + + threadLayout.getPresenter().bindLoadable(loadable); + threadLayout.getPresenter().requestData(); + } + + @Override + public void onMenuItemClicked(ToolbarMenuItem item) { + switch (item.getId()) { + case REFRESH_ID: + threadLayout.getPresenter().requestData(); + break; + case POST_ID: + // TODO + break; + } + } + + @Override + public void onSubMenuItemClicked(ToolbarMenuItem parent, ToolbarMenuSubItem item) { + switch (item.getId()) { + case SEARCH_ID: + // TODO + break; + case SHARE_ID: + String link = ChanUrls.getCatalogUrlDesktop(threadLayout.getPresenter().getLoadable().board); + AndroidUtils.shareLink(link); + break; + case SETTINGS_ID: + SettingsController settingsController = new SettingsController(context); + navigationController.pushController(settingsController); + break; + } + } + + @Override + public void openThread(Loadable threadLoadable) { + ViewThreadController viewThreadController = new ViewThreadController(context); + viewThreadController.setLoadable(threadLoadable); + navigationController.pushController(viewThreadController); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java new file mode 100644 index 00000000..888f9917 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java @@ -0,0 +1,65 @@ +package org.floens.chan.ui.controller; + +import android.content.Context; +import android.content.res.Configuration; +import android.support.v4.widget.DrawerLayout; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import org.floens.chan.R; +import org.floens.chan.controller.Controller; +import org.floens.chan.controller.NavigationController; +import org.floens.chan.ui.toolbar.Toolbar; +import org.floens.chan.utils.AndroidUtils; + +import static org.floens.chan.utils.AndroidUtils.dp; + +public class RootNavigationController extends NavigationController { + public RootNavigationController(Context context, Controller startController) { + super(context, startController); + + view = inflateRes(R.layout.root_layout); + toolbar = (Toolbar) view.findViewById(R.id.toolbar); + container = (FrameLayout) view.findViewById(R.id.container); + drawerLayout = (DrawerLayout) view.findViewById(R.id.drawer_layout); + drawer = (FrameLayout) view.findViewById(R.id.drawer); + + toolbar.setCallback(this); + + initWithController(startController); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + AndroidUtils.waitForLayout(drawer, new AndroidUtils.OnMeasuredCallback() { + @Override + public void onMeasured(View view, int width, int height) { + setDrawerWidth(); + } + }); + } + + @Override + public void onCreate() { + setDrawerWidth(); + } + + @Override + public void onMenuClicked() { + super.onMenuClicked(); + + drawerLayout.openDrawer(drawer); + } + + private void setDrawerWidth() { + int width = Math.min(view.getWidth() - dp(56), dp(56) * 6); + if (drawer.getWidth() != width) { + ViewGroup.LayoutParams params = drawer.getLayoutParams(); + params.width = width; + drawer.setLayoutParams(params); + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/SettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/SettingsController.java new file mode 100644 index 00000000..6608b1c0 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/SettingsController.java @@ -0,0 +1,21 @@ +package org.floens.chan.ui.controller; + +import android.content.Context; + +import org.floens.chan.R; +import org.floens.chan.controller.Controller; + +public class SettingsController extends Controller { + public SettingsController(Context context) { + super(context); + } + + @Override + public void onCreate() { + super.onCreate(); + + navigationItem.title = context.getString(R.string.action_settings); + + view = inflateRes(R.layout.settings_layout); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java new file mode 100644 index 00000000..d2c7cb2b --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java @@ -0,0 +1,54 @@ +package org.floens.chan.ui.controller; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; + +import org.floens.chan.R; +import org.floens.chan.controller.Controller; +import org.floens.chan.core.model.Loadable; +import org.floens.chan.ui.layout.ThreadLayout; + +public class ViewThreadController extends Controller implements ThreadLayout.ThreadLayoutCallback { + private ThreadLayout threadLayout; + private Loadable loadable; + + public ViewThreadController(Context context) { + super(context); + } + + public void setLoadable(Loadable loadable) { + this.loadable = loadable; + } + + @Override + public void onCreate() { + super.onCreate(); + + threadLayout = new ThreadLayout(context); + threadLayout.setCallback(this); + view = threadLayout; + view.setBackgroundColor(0xffffffff); + + threadLayout.getPresenter().bindLoadable(loadable); + threadLayout.getPresenter().requestData(); + + navigationItem.title = loadable.title; + } + + @Override + public void openThread(Loadable threadLoadable) { + // TODO implement, scroll to post and fix title + new AlertDialog.Builder(context) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { +// threadManagerListener.onOpenThread(thread, link.postId); + } + }) + .setTitle(R.string.open_thread_confirmation) + .setMessage("/" + threadLoadable.board + "/" + threadLoadable.no) + .show(); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/drawable/ArrowMenuDrawable.java b/Clover/app/src/main/java/org/floens/chan/ui/drawable/ArrowMenuDrawable.java new file mode 100644 index 00000000..edc110ff --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/drawable/ArrowMenuDrawable.java @@ -0,0 +1,153 @@ +package org.floens.chan.ui.drawable; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import static org.floens.chan.utils.AndroidUtils.dp; + +public class ArrowMenuDrawable extends Drawable { + private final Paint mPaint = new Paint(); + + // The angle in degress that the arrow head is inclined at. + private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45); + private final float mBarThickness; + // The length of top and bottom bars when they merge into an arrow + private final float mTopBottomArrowSize; + // The length of middle bar + private final float mBarSize; + // The length of the middle bar when arrow is shaped + private final float mMiddleArrowSize; + // The space between bars when they are parallel + private final float mBarGap; + // Use Path instead of canvas operations so that if color has transparency, overlapping sections + // wont look different + private final Path mPath = new Path(); + // The reported intrinsic size of the drawable. + private final int mSize; + // Whether we should mirror animation when animation is reversed. + private boolean mVerticalMirror = false; + // The interpolated version of the original progress + private float mProgress; + + public ArrowMenuDrawable() { + mPaint.setColor(0xffffffff); + mPaint.setAntiAlias(true); + mSize = dp(24f); + mBarSize = dp(18f); + mTopBottomArrowSize = dp(11.31f); + mBarThickness = dp(2f); + mBarGap = dp(3f); + mMiddleArrowSize = dp(16f); + + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeJoin(Paint.Join.ROUND); + mPaint.setStrokeCap(Paint.Cap.SQUARE); + mPaint.setStrokeWidth(mBarThickness); + + setProgress(0f); + } + + boolean isLayoutRtl() { + return false; + } + + @Override + public void draw(Canvas canvas) { + Rect bounds = getBounds(); + // Interpolated widths of arrow bars + final float arrowSize = lerp(mBarSize, mTopBottomArrowSize, mProgress); + final float middleBarSize = lerp(mBarSize, mMiddleArrowSize, mProgress); + // Interpolated size of middle bar + final float middleBarCut = lerp(0, mBarThickness / 2, mProgress); + // The rotation of the top and bottom bars (that make the arrow head) + final float rotation = lerp(0, ARROW_HEAD_ANGLE, mProgress); + + // The whole canvas rotates as the transition happens + final float canvasRotate = lerp(-180, 0, mProgress); + final float topBottomBarOffset = lerp(mBarGap + mBarThickness, 0, mProgress); + mPath.rewind(); + + final float arrowEdge = -middleBarSize / 2; + // draw middle bar + mPath.moveTo(arrowEdge + middleBarCut, 0); + mPath.rLineTo(middleBarSize - middleBarCut, 0); + + float arrowWidth = arrowSize * (float) Math.cos(rotation); + float arrowHeight = arrowSize * (float) Math.sin(rotation); + + if (mProgress == 0f || mProgress == 1f) { + arrowWidth = Math.round(arrowWidth); + arrowHeight = Math.round(arrowHeight); + } + + // top bar + mPath.moveTo(arrowEdge, topBottomBarOffset); + mPath.rLineTo(arrowWidth, arrowHeight); + + // bottom bar + mPath.moveTo(arrowEdge, -topBottomBarOffset); + mPath.rLineTo(arrowWidth, -arrowHeight); + mPath.moveTo(0, 0); + mPath.close(); + + canvas.save(); + // Rotate the whole canvas if spinning. + canvas.rotate(canvasRotate * ((mVerticalMirror) ? -1 : 1), + bounds.centerX(), bounds.centerY()); + canvas.translate(bounds.centerX(), bounds.centerY()); + canvas.drawPath(mPath, mPaint); + + canvas.restore(); + } + + @Override + public void setAlpha(int i) { + mPaint.setAlpha(i); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mPaint.setColorFilter(colorFilter); + } + + @Override + public int getIntrinsicHeight() { + return mSize; + } + + @Override + public int getIntrinsicWidth() { + return mSize; + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + public float getProgress() { + return mProgress; + } + + public void setProgress(float progress) { + if (progress == 1f) { + mVerticalMirror = true; + } else if (progress == 0f) { + mVerticalMirror = false; + } + mProgress = progress; + invalidateSelf(); + } + + /** + * Linear interpolate between a and b with parameter t. + */ + private static float lerp(float a, float b, float t) { + return a + (b - a) * t; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ImageViewFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ImageViewFragment.java index c509fcf1..c8e82bf3 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ImageViewFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ImageViewFragment.java @@ -42,11 +42,13 @@ import org.floens.chan.ui.activity.ImageViewActivity; import org.floens.chan.ui.adapter.ImageViewAdapter; import org.floens.chan.ui.view.ThumbnailImageView; import org.floens.chan.ui.view.ThumbnailImageView.ThumbnailImageViewCallback; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.ImageSaver; -import org.floens.chan.utils.Utils; import java.io.File; +import static org.floens.chan.utils.AndroidUtils.dp; + public class ImageViewFragment extends Fragment implements ThumbnailImageViewCallback { private Context context; private ImageViewActivity activity; @@ -238,7 +240,7 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal activity.invalidateActionBar(); break; case R.id.action_open_browser: - Utils.openLink(context, post.imageUrl); + AndroidUtils.openLink(post.imageUrl); break; case R.id.action_image_save: case R.id.action_share: @@ -254,7 +256,7 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal // Search if it was an ImageSearch item for (ImageSearch engine : ImageSearch.engines) { if (item.getItemId() == engine.getId()) { - Utils.openLink(context, engine.getUrl(post.imageUrl)); + AndroidUtils.openLink(engine.getUrl(post.imageUrl)); break; } } @@ -285,13 +287,13 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal TextView noticeText = new TextView(context); noticeText.setText(R.string.video_playback_warning); noticeText.setTextSize(16f); - notice.addView(noticeText, Utils.MATCH_WRAP_PARAMS); + notice.addView(noticeText, AndroidUtils.MATCH_WRAP_PARAMS); final CheckBox dontShowAgain = new CheckBox(context); dontShowAgain.setText(R.string.video_playback_ignore); - notice.addView(dontShowAgain, Utils.MATCH_WRAP_PARAMS); + notice.addView(dontShowAgain, AndroidUtils.MATCH_WRAP_PARAMS); - int padding = Utils.dp(16f); + int padding = dp(16f); notice.setPadding(padding, padding, padding, padding); new AlertDialog.Builder(context) diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/PostRepliesFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/PostRepliesFragment.java index 23fe0fa4..1029dd3e 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/PostRepliesFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/PostRepliesFragment.java @@ -31,8 +31,9 @@ import android.widget.TextView; import org.floens.chan.R; import org.floens.chan.core.ChanPreferences; -import org.floens.chan.core.manager.ThreadManager; import org.floens.chan.core.model.Post; +import org.floens.chan.core.presenter.ThreadPresenter; +import org.floens.chan.ui.helper.PostPopupHelper; import org.floens.chan.ui.view.PostView; import org.floens.chan.utils.ThemeHelper; @@ -44,20 +45,21 @@ public class PostRepliesFragment extends DialogFragment { private ListView listView; private Activity activity; - private ThreadManager.RepliesPopup repliesPopup; - private ThreadManager manager; - private boolean callback = true; + private PostPopupHelper.RepliesData repliesData; + private PostPopupHelper postPopupHelper; + private ThreadPresenter presenter; - public static PostRepliesFragment newInstance(ThreadManager.RepliesPopup repliesPopup, ThreadManager manager) { + public static PostRepliesFragment newInstance(PostPopupHelper.RepliesData repliesData, PostPopupHelper postPopupHelper, ThreadPresenter presenter) { PostRepliesFragment fragment = new PostRepliesFragment(); - fragment.repliesPopup = repliesPopup; - fragment.manager = manager; + fragment.repliesData = repliesData; + fragment.postPopupHelper = postPopupHelper; + fragment.presenter = presenter; return fragment; } public void dismissNoCallback() { - callback = false; + postPopupHelper = null; dismiss(); } @@ -72,8 +74,8 @@ public class PostRepliesFragment extends DialogFragment { public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); - if (callback && manager != null) { - manager.onPostRepliesPop(); + if (postPopupHelper != null) { + postPopupHelper.onPostRepliesPop(); } } @@ -98,8 +100,9 @@ public class PostRepliesFragment extends DialogFragment { container.findViewById(R.id.replies_close).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - manager.closeAllPostFragments(); - dismiss(); + if (postPopupHelper != null) { + postPopupHelper.closeAllPostFragments(); + } } }); @@ -117,7 +120,7 @@ public class PostRepliesFragment extends DialogFragment { activity = getActivity(); - if (repliesPopup == null) { + if (repliesData == null) { // Restoring from background. dismiss(); } else { @@ -133,15 +136,15 @@ public class PostRepliesFragment extends DialogFragment { final Post p = getItem(position); - postView.setPost(p, manager); - postView.setHighlightQuotesWithNo(repliesPopup.forNo); + postView.setPost(p, presenter); + postView.setHighlightQuotesWithNo(repliesData.forPost.no); postView.setOnClickListeners(new View.OnClickListener() { @Override public void onClick(View v) { - manager.closeAllPostFragments(); + if (postPopupHelper != null) { + postPopupHelper.postClicked(p); + } dismiss(); - manager.highlightPost(p.no); - manager.scrollToPost(p.no); } }); @@ -149,10 +152,10 @@ public class PostRepliesFragment extends DialogFragment { } }; - adapter.addAll(repliesPopup.posts); + adapter.addAll(repliesData.posts); listView.setAdapter(adapter); - listView.setSelectionFromTop(repliesPopup.listViewIndex, repliesPopup.listViewTop); + listView.setSelectionFromTop(repliesData.listViewIndex, repliesData.listViewTop); listView.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { @@ -160,10 +163,10 @@ public class PostRepliesFragment extends DialogFragment { @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - if (repliesPopup != null) { - repliesPopup.listViewIndex = view.getFirstVisiblePosition(); + if (repliesData != null) { + repliesData.listViewIndex = view.getFirstVisiblePosition(); View v = view.getChildAt(0); - repliesPopup.listViewTop = (v == null) ? 0 : v.getTop(); + repliesData.listViewTop = (v == null) ? 0 : v.getTop(); } } }); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java index 6a6da66a..c78fd297 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java @@ -61,13 +61,15 @@ import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Reply; import org.floens.chan.ui.ViewFlipperAnimations; import org.floens.chan.ui.view.LoadView; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.ImageDecoder; import org.floens.chan.utils.Logger; import org.floens.chan.utils.ThemeHelper; -import org.floens.chan.utils.Utils; import java.io.File; +import static org.floens.chan.utils.AndroidUtils.dp; + public class ReplyFragment extends DialogFragment { private static final String TAG = "ReplyFragment"; @@ -458,8 +460,8 @@ public class ReplyFragment extends DialogFragment { boolean probablyWebm = name.endsWith(".webm"); int maxSize = probablyWebm ? b.maxWebmSize : b.maxFileSize; if (file.length() > maxSize) { - String fileSize = Utils.getReadableFileSize((int) file.length(), false); - String maxSizeString = Utils.getReadableFileSize(maxSize, false); + String fileSize = AndroidUtils.getReadableFileSize((int) file.length(), false); + String maxSizeString = AndroidUtils.getReadableFileSize(maxSize, false); String text = getString(probablyWebm ? R.string.reply_webm_too_big : R.string.reply_file_too_big, fileSize, maxSizeString); fileStatusView.setVisibility(View.VISIBLE); fileStatusView.setText(text); @@ -510,11 +512,11 @@ public class ReplyFragment extends DialogFragment { private void noPreview(LoadView loadView) { TextView text = new TextView(context); - text.setLayoutParams(Utils.MATCH_WRAP_PARAMS); + text.setLayoutParams(AndroidUtils.MATCH_WRAP_PARAMS); text.setGravity(Gravity.CENTER); text.setText(R.string.reply_no_preview); text.setTextSize(16f); - int padding = Utils.dp(16); + int padding = dp(16); text.setPadding(padding, padding, padding, padding); loadView.setView(text); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java index 4c46e4a1..8e05f023 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java @@ -45,8 +45,8 @@ import com.android.volley.VolleyError; import org.floens.chan.R; import org.floens.chan.core.ChanPreferences; +import org.floens.chan.core.loader.ChanLoader; import org.floens.chan.core.loader.EndOfLineException; -import org.floens.chan.core.loader.Loader; import org.floens.chan.core.manager.ThreadManager; import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.Loadable; @@ -55,16 +55,19 @@ import org.floens.chan.ui.activity.BaseActivity; import org.floens.chan.ui.activity.ImageViewActivity; import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.view.LoadView; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.ImageSaver; import org.floens.chan.utils.ThemeHelper; -import org.floens.chan.utils.Utils; import java.util.ArrayList; import java.util.List; import javax.net.ssl.SSLException; -public class ThreadFragment extends Fragment implements ThreadManager.ThreadManagerListener, PostAdapter.PostAdapterListener { +import static org.floens.chan.utils.AndroidUtils.dp; +import static org.floens.chan.utils.AndroidUtils.setPressedDrawable; + +public class ThreadFragment extends Fragment implements ThreadManager.ThreadManagerListener, PostAdapter.PostAdapterCallback { private ThreadManager threadManager; private Loadable loadable; @@ -125,8 +128,8 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana this.viewMode = viewMode; } - public Loader getLoader() { - return threadManager.getLoader(); + public ChanLoader getLoader() { + return threadManager.getChanLoader(); } public void startFiltering() { @@ -193,7 +196,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana ((BaseActivity) getActivity()).onOPClicked(post); } else if (loadable.isThreadMode() && isFiltering) { filterView.clearSearch(); - postAdapter.scrollToPost(post.no); +// postAdapter.scrollToPost(post.no); } } @@ -240,7 +243,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana @Override public void onScrollTo(int post) { if (postAdapter != null) { - postAdapter.scrollToPost(post); +// postAdapter.scrollToPost(post); } } @@ -275,7 +278,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana if (highlightedPost >= 0) { threadManager.highlightPost(highlightedPost); - postAdapter.scrollToPost(highlightedPost); +// postAdapter.scrollToPost(highlightedPost); highlightedPost = -1; } @@ -299,7 +302,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana highlightedPost = -1; } - public void onFilterResults(String filter, int count, boolean all) { + public void onFilteredResults(String filter, int count, boolean all) { isFiltering = !all; if (filterView != null) { @@ -307,6 +310,26 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana } } + @Override + public Loadable getLoadable() { + return loadable; + } + + @Override + public void onListScrolledToBottom() { + + } + + @Override + public void onListStatusClicked() { + + } + + @Override + public void scrollTo(int position) { + + } + private RelativeLayout createView() { RelativeLayout compound = new RelativeLayout(getActivity()); @@ -315,12 +338,12 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana filterView = new FilterView(getActivity()); filterView.setVisibility(View.GONE); - listViewContainer.addView(filterView, Utils.MATCH_WRAP_PARAMS); + listViewContainer.addView(filterView, AndroidUtils.MATCH_WRAP_PARAMS); if (viewMode == ThreadManager.ViewMode.LIST) { ListView list = new ListView(getActivity()); listView = list; - postAdapter = new PostAdapter(getActivity(), threadManager, listView, this); +// postAdapter = new PostAdapter(getActivity(), threadManager, listView, this); listView.setAdapter(postAdapter); list.setSelectionFromTop(loadable.listViewIndex, loadable.listViewTop); } else if (viewMode == ThreadManager.ViewMode.GRID) { @@ -329,7 +352,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana int postGridWidth = getActivity().getResources().getDimensionPixelSize(R.dimen.post_grid_width); grid.setColumnWidth(postGridWidth); listView = grid; - postAdapter = new PostAdapter(getActivity(), threadManager, listView, this); +// postAdapter = new PostAdapter(getActivity(), threadManager, listView, this); listView.setAdapter(postAdapter); listView.setSelection(loadable.listViewIndex); } @@ -363,20 +386,20 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana } }); - listViewContainer.addView(listView, Utils.MATCH_PARAMS); + listViewContainer.addView(listView, AndroidUtils.MATCH_PARAMS); - compound.addView(listViewContainer, Utils.MATCH_PARAMS); + compound.addView(listViewContainer, AndroidUtils.MATCH_PARAMS); if (loadable.isThreadMode()) { skip = new ImageView(getActivity()); skip.setImageResource(R.drawable.skip_arrow_down); skip.setVisibility(View.GONE); - compound.addView(skip, Utils.WRAP_PARAMS); + compound.addView(skip, AndroidUtils.WRAP_PARAMS); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) skip.getLayoutParams(); params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); - params.setMargins(0, 0, Utils.dp(8), Utils.dp(8)); + params.setMargins(0, 0, dp(8), dp(8)); skip.setLayoutParams(params); skipLogic = new SkipLogic(skip, listView); @@ -418,19 +441,19 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana String errorMessage = getLoadErrorText(error); LinearLayout wrapper = new LinearLayout(getActivity()); - wrapper.setLayoutParams(Utils.MATCH_PARAMS); + wrapper.setLayoutParams(AndroidUtils.MATCH_PARAMS); wrapper.setGravity(Gravity.CENTER); wrapper.setOrientation(LinearLayout.VERTICAL); TextView text = new TextView(getActivity()); - text.setLayoutParams(Utils.WRAP_PARAMS); + text.setLayoutParams(AndroidUtils.WRAP_PARAMS); text.setText(errorMessage); text.setTextSize(24f); wrapper.addView(text); Button retry = new Button(getActivity()); retry.setText(R.string.thread_load_failed_retry); - retry.setLayoutParams(Utils.WRAP_PARAMS); + retry.setLayoutParams(AndroidUtils.WRAP_PARAMS); retry.setGravity(Gravity.CENTER); retry.setOnClickListener(new View.OnClickListener() { @Override @@ -444,7 +467,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana wrapper.addView(retry); LinearLayout.LayoutParams retryParams = (LinearLayout.LayoutParams) retry.getLayoutParams(); - retryParams.topMargin = Utils.dp(12); + retryParams.topMargin = dp(12); retry.setLayoutParams(retryParams); return wrapper; @@ -586,11 +609,11 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana searchViewContainer.addView(closeButton); closeButton.setImageResource(ThemeHelper.getInstance().getTheme().isLightTheme ? R.drawable.ic_action_cancel : R.drawable.ic_action_cancel_dark); LinearLayout.LayoutParams closeButtonParams = (LinearLayout.LayoutParams) closeButton.getLayoutParams(); - searchViewParams.width = Utils.dp(48); + searchViewParams.width = dp(48); searchViewParams.height = LayoutParams.MATCH_PARENT; closeButton.setLayoutParams(closeButtonParams); - Utils.setPressedDrawable(closeButton); - int padding = Utils.dp(8); + setPressedDrawable(closeButton); + int padding = dp(8); closeButton.setPadding(padding, padding, padding, padding); closeButton.setOnClickListener(new OnClickListener() { @@ -600,7 +623,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana } }); - addView(searchViewContainer, new LayoutParams(LayoutParams.MATCH_PARENT, Utils.dp(48))); + addView(searchViewContainer, new LayoutParams(LayoutParams.MATCH_PARENT, dp(48))); searchView.setQueryHint(getString(R.string.search_hint)); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @@ -619,7 +642,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana textView = new TextView(getContext()); textView.setGravity(Gravity.CENTER); - addView(textView, new LayoutParams(LayoutParams.MATCH_PARENT, Utils.dp(28))); + addView(textView, new LayoutParams(LayoutParams.MATCH_PARENT, dp(28))); } private void setText(String filter, int count, boolean all) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/helper/PostPopupHelper.java b/Clover/app/src/main/java/org/floens/chan/ui/helper/PostPopupHelper.java new file mode 100644 index 00000000..90a9a048 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/helper/PostPopupHelper.java @@ -0,0 +1,85 @@ +package org.floens.chan.ui.helper; + +import android.app.Activity; +import android.app.FragmentTransaction; +import android.content.Context; + +import org.floens.chan.core.model.Post; +import org.floens.chan.core.presenter.ThreadPresenter; +import org.floens.chan.ui.fragment.PostRepliesFragment; + +import java.util.ArrayList; +import java.util.List; + +public class PostPopupHelper { + private Context context; + private ThreadPresenter presenter; + + private final List dataQueue = new ArrayList<>(); + private PostRepliesFragment currentPopupFragment; + + public PostPopupHelper(Context context, ThreadPresenter presenter) { + this.context = context; + this.presenter = presenter; + } + + public void showPosts(Post forPost, List posts) { + RepliesData data = new RepliesData(forPost, posts); + + dataQueue.add(data); + + if (currentPopupFragment != null) { + currentPopupFragment.dismissNoCallback(); + } + + presentFragment(data); + } + + public void onPostRepliesPop() { + if (dataQueue.size() == 0) + return; + + dataQueue.remove(dataQueue.size() - 1); + + if (dataQueue.size() > 0) { + presentFragment(dataQueue.get(dataQueue.size() - 1)); + } else { + currentPopupFragment = null; + } + } + + public void closeAllPostFragments() { + dataQueue.clear(); + if (currentPopupFragment != null) { + currentPopupFragment.dismissNoCallback(); + currentPopupFragment = null; + } + } + + public void postClicked(Post p) { + closeAllPostFragments(); + presenter.highlightPost(p.no); + presenter.scrollToPost(p.no); + } + + private void presentFragment(RepliesData data) { + PostRepliesFragment fragment = PostRepliesFragment.newInstance(data, this, presenter); + // TODO fade animations on all platforms + FragmentTransaction ft = ((Activity) context).getFragmentManager().beginTransaction(); + ft.add(fragment, "postPopup"); + ft.commitAllowingStateLoss(); + currentPopupFragment = fragment; + } + + public static class RepliesData { + public List posts; + public Post forPost; + public int listViewIndex; + public int listViewTop; + + public RepliesData(Post forPost, List posts) { + this.forPost = forPost; + this.posts = posts; + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ImageViewLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ImageViewLayout.java new file mode 100644 index 00000000..d40d0c36 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ImageViewLayout.java @@ -0,0 +1,234 @@ +package org.floens.chan.ui.layout; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import org.floens.chan.R; + +import static org.floens.chan.utils.AndroidUtils.dp; +import static org.floens.chan.utils.AnimationUtils.calculateBoundsAnimation; + + +public class ImageViewLayout extends FrameLayout implements View.OnClickListener { + private ImageView imageView; + + private Callback callback; + private Drawable drawable; + + private int statusBarColorPrevious; + private AnimatorSet startAnimation; + private AnimatorSet endAnimation; + + public static ImageViewLayout attach(Window window) { + ImageViewLayout imageViewLayout = (ImageViewLayout) LayoutInflater.from(window.getContext()).inflate(R.layout.image_view_layout, null); + window.addContentView(imageViewLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + return imageViewLayout; + } + + public ImageViewLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + this.imageView = (ImageView) findViewById(R.id.image); + setOnClickListener(this); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + return true; + } + + @Override + public void onClick(View v) { + removeImage(); + } + + public void setImage(Callback callback, final Drawable drawable) { + this.callback = callback; + this.drawable = drawable; + + this.imageView.setImageDrawable(drawable); + + Rect startBounds = callback.getImageViewLayoutStartBounds(); + final Rect endBounds = new Rect(); + final Point globalOffset = new Point(); + getGlobalVisibleRect(endBounds, globalOffset); + float startScale = calculateBoundsAnimation(startBounds, endBounds, globalOffset); + + imageView.setPivotX(0f); + imageView.setPivotY(0f); + imageView.setX(startBounds.left); + imageView.setY(startBounds.top); + imageView.setScaleX(startScale); + imageView.setScaleY(startScale); + + Window window = ((Activity) getContext()).getWindow(); + if (Build.VERSION.SDK_INT >= 21) { + statusBarColorPrevious = window.getStatusBarColor(); + } + + startAnimation(startBounds, endBounds, startScale); + } + + public void removeImage() { + if (startAnimation != null || endAnimation != null) { + return; + } + + endAnimation(); +// endAnimationEmpty(); + } + + private void startAnimation(Rect startBounds, Rect finalBounds, float startScale) { + startAnimation = new AnimatorSet(); + + ValueAnimator backgroundAlpha = ValueAnimator.ofFloat(0f, 1f); + backgroundAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setBackgroundAlpha((float) animation.getAnimatedValue()); + } + }); + + startAnimation + .play(ObjectAnimator.ofFloat(imageView, View.X, startBounds.left, finalBounds.left)) + .with(ObjectAnimator.ofFloat(imageView, View.Y, startBounds.top, finalBounds.top)) + .with(ObjectAnimator.ofFloat(imageView, View.SCALE_X, startScale, 1f)) + .with(ObjectAnimator.ofFloat(imageView, View.SCALE_Y, startScale, 1f)) + .with(backgroundAlpha); + + startAnimation.setDuration(200); + startAnimation.setInterpolator(new DecelerateInterpolator()); + startAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + startAnimationEnd(); + startAnimation = null; + } + }); + startAnimation.start(); + } + + private void startAnimationEnd() { + imageView.setX(0f); + imageView.setY(0f); + imageView.setScaleX(1f); + imageView.setScaleY(1f); +// controller.setVisibility(false); + } + + private void endAnimation() { +// controller.setVisibility(true); + + Rect startBounds = callback.getImageViewLayoutStartBounds(); + final Rect endBounds = new Rect(); + final Point globalOffset = new Point(); + getGlobalVisibleRect(endBounds, globalOffset); + float startScale = calculateBoundsAnimation(startBounds, endBounds, globalOffset); + + endAnimation = new AnimatorSet(); + + ValueAnimator backgroundAlpha = ValueAnimator.ofFloat(1f, 0f); + backgroundAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setBackgroundAlpha((float) animation.getAnimatedValue()); + } + }); + + endAnimation + .play(ObjectAnimator.ofFloat(imageView, View.X, startBounds.left)) + .with(ObjectAnimator.ofFloat(imageView, View.Y, startBounds.top)) + .with(ObjectAnimator.ofFloat(imageView, View.SCALE_X, 1f, startScale)) + .with(ObjectAnimator.ofFloat(imageView, View.SCALE_Y, 1f, startScale)) + .with(backgroundAlpha); + + endAnimation.setDuration(200); + endAnimation.setInterpolator(new DecelerateInterpolator()); + endAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + endAnimationEnd(); + } + }); + endAnimation.start(); + } + + private void endAnimationEmpty() { + endAnimation = new AnimatorSet(); + + ValueAnimator backgroundAlpha = ValueAnimator.ofFloat(1f, 0f); + backgroundAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setBackgroundAlpha((float) animation.getAnimatedValue()); + } + }); + endAnimation + .play(ObjectAnimator.ofFloat(imageView, View.Y, imageView.getTop(), imageView.getTop() + dp(20))) + .with(ObjectAnimator.ofFloat(imageView, View.ALPHA, 1f, 0f)) + .with(backgroundAlpha); + + endAnimation.setDuration(200); + endAnimation.setInterpolator(new DecelerateInterpolator()); + endAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + endAnimationEnd(); + } + }); + endAnimation.start(); + } + + private void endAnimationEnd() { + Window window = ((Activity) getContext()).getWindow(); + if (Build.VERSION.SDK_INT >= 21) { + window.setStatusBarColor(statusBarColorPrevious); + } + + callback.onImageViewLayoutDestroy(); + } + + private void setBackgroundAlpha(float alpha) { + setBackgroundColor(Color.argb((int) (alpha * 255f), 0, 0, 0)); + + if (Build.VERSION.SDK_INT >= 21) { + Window window = ((Activity) getContext()).getWindow(); + + int r = (int) ((1f - alpha) * Color.red(statusBarColorPrevious)); + int g = (int) ((1f - alpha) * Color.green(statusBarColorPrevious)); + int b = (int) ((1f - alpha) * Color.blue(statusBarColorPrevious)); + + window.setStatusBarColor(Color.argb(255, r, g, b)); + } + } + + public interface Callback { + public Rect getImageViewLayoutStartBounds(); + + public void onImageViewLayoutDestroy(); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java new file mode 100644 index 00000000..bac90da9 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java @@ -0,0 +1,157 @@ +package org.floens.chan.ui.layout; + +import android.app.AlertDialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.util.AttributeSet; +import android.widget.Toast; + +import com.android.volley.VolleyError; + +import org.floens.chan.R; +import org.floens.chan.core.ChanPreferences; +import org.floens.chan.core.model.ChanThread; +import org.floens.chan.core.model.Loadable; +import org.floens.chan.core.model.Post; +import org.floens.chan.core.model.PostLinkable; +import org.floens.chan.core.presenter.ThreadPresenter; +import org.floens.chan.ui.helper.PostPopupHelper; +import org.floens.chan.ui.view.LoadView; +import org.floens.chan.utils.AndroidUtils; + +import java.util.List; + +/** + * Wrapper around ThreadListLayout, so that it cleanly manages between loadbar and listview. + */ +public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPresenterCallback { + private ThreadLayoutCallback callback; + private ThreadPresenter presenter; + + private ThreadListLayout threadListLayout; + private PostPopupHelper postPopupHelper; + private boolean visible; + + public ThreadLayout(Context context) { + super(context); + init(); + } + + public ThreadLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ThreadLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + presenter = new ThreadPresenter(this); + + threadListLayout = new ThreadListLayout(getContext()); + threadListLayout.setCallbacks(presenter, presenter); + + postPopupHelper = new PostPopupHelper(getContext(), presenter); + + switchVisible(false); + } + + public void setCallback(ThreadLayoutCallback callback) { + this.callback = callback; + } + + public ThreadPresenter getPresenter() { + return presenter; + } + + @Override + public void showPosts(ChanThread thread) { + threadListLayout.showPosts(thread, !visible); + switchVisible(true); + } + + @Override + public void showError(VolleyError error) { + switchVisible(true); + threadListLayout.showError(error); + } + + @Override + public void showLoading() { + switchVisible(false); + } + + public void showPostInfo(String info) { + new AlertDialog.Builder(getContext()) + .setTitle(R.string.post_info) + .setMessage(info) + .setPositiveButton(R.string.ok, null) + .show(); + } + + public void showPostLinkables(final List linkables) { + String[] keys = new String[linkables.size()]; + for (int i = 0; i < linkables.size(); i++) { + keys[i] = linkables.get(i).key; + } + + new AlertDialog.Builder(getContext()) + .setItems(keys, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + presenter.onPostLinkableClicked(linkables.get(which)); + } + }) + .show(); + } + + public void clipboardPost(Post post) { + ClipboardManager clipboard = (ClipboardManager) AndroidUtils.getAppRes().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Post text", post.comment.toString()); + clipboard.setPrimaryClip(clip); + Toast.makeText(getContext(), R.string.post_text_copied_to_clipboard, Toast.LENGTH_SHORT).show(); + } + + @Override + public void openLink(final String link) { + if (ChanPreferences.getOpenLinkConfirmation()) { + new AlertDialog.Builder(getContext()) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + AndroidUtils.openLink(link); + } + }) + .setTitle(R.string.open_link_confirmation) + .setMessage(link) + .show(); + } else { + AndroidUtils.openLink(link); + } + } + + @Override + public void showThread(Loadable threadLoadable) { + callback.openThread(threadLoadable); + } + + public void showPostsPopup(Post forPost, List posts) { + postPopupHelper.showPosts(forPost, posts); + } + + private void switchVisible(boolean visible) { + if (this.visible != visible) { + this.visible = visible; + setView(visible ? threadListLayout : null); + } + } + + public interface ThreadLayoutCallback { + public void openThread(Loadable threadLoadable); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java new file mode 100644 index 00000000..7860414e --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java @@ -0,0 +1,79 @@ +package org.floens.chan.ui.layout; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ListView; +import android.widget.RelativeLayout; + +import com.android.volley.VolleyError; + +import org.floens.chan.core.model.ChanThread; +import org.floens.chan.ui.adapter.PostAdapter; +import org.floens.chan.ui.view.PostView; + +/** + * A layout that wraps around a listview to manage showing posts. + */ +public class ThreadListLayout extends RelativeLayout { + private ListView listView; + private PostAdapter postAdapter; + private PostAdapter.PostAdapterCallback postAdapterCallback; + private PostView.PostViewCallback postViewCallback; + + private int restoreListViewIndex; + private int restoreListViewTop; + + public ThreadListLayout(Context context) { + super(context); + init(); + } + + public ThreadListLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ThreadListLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + restoreListViewIndex = listView.getFirstVisiblePosition(); + restoreListViewTop = listView.getChildAt(0) == null ? 0 : listView.getChildAt(0).getTop(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + listView.setSelectionFromTop(restoreListViewIndex, restoreListViewTop); + } + + private void init() { + listView = new ListView(getContext()); + addView(listView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + public void setCallbacks(PostAdapter.PostAdapterCallback postAdapterCallback, PostView.PostViewCallback postViewCallback) { + this.postAdapterCallback = postAdapterCallback; + this.postViewCallback = postViewCallback; + + postAdapter = new PostAdapter(getContext(), postAdapterCallback, postViewCallback); + listView.setAdapter(postAdapter); + } + + public void showPosts(ChanThread thread, boolean initial) { + if (initial) { + listView.setSelectionFromTop(0, 0); + restoreListViewIndex = 0; + restoreListViewTop = 0; + } + postAdapter.setThread(thread); + } + + public void showError(VolleyError error) { + + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java b/Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java index dd3de266..848bdd06 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java @@ -34,7 +34,7 @@ import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Post; import org.floens.chan.core.watch.PinWatcher; import org.floens.chan.ui.activity.ChanActivity; -import org.floens.chan.utils.Utils; +import org.floens.chan.utils.AndroidUtils; import java.util.ArrayList; import java.util.Collections; @@ -171,7 +171,7 @@ public class WatchNotifier extends Service { Collections.sort(notificationList, POST_AGE_COMPARER); List lines = new ArrayList<>(); for (Post post : notificationList) { - CharSequence prefix = Utils.ellipsize(post.title, 18); + CharSequence prefix = AndroidUtils.ellipsize(post.title, 18); CharSequence comment; if (post.comment.length() == 0) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java new file mode 100644 index 00000000..a194c7a8 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java @@ -0,0 +1,10 @@ +package org.floens.chan.ui.toolbar; + +import android.widget.LinearLayout; + +public class NavigationItem { + public String title = ""; + public ToolbarMenu menu; + public boolean hasBack = true; + public LinearLayout view; +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java new file mode 100644 index 00000000..a5752033 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java @@ -0,0 +1,220 @@ +package org.floens.chan.ui.toolbar; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.floens.chan.R; +import org.floens.chan.ui.drawable.ArrowMenuDrawable; +import org.floens.chan.utils.AndroidUtils; + +import java.util.ArrayList; +import java.util.List; + +import static org.floens.chan.utils.AndroidUtils.dp; +import static org.floens.chan.utils.AndroidUtils.getAttrDrawable; + +public class Toolbar extends LinearLayout implements View.OnClickListener { + private ImageView arrowMenuView; + private ArrowMenuDrawable arrowMenuDrawable; + + private FrameLayout navigationItemContainer; + + private ToolbarCallback callback; + private NavigationItem navigationItem; + + public Toolbar(Context context) { + super(context); + init(); + } + + public Toolbar(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public Toolbar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public void setNavigationItem(final boolean animate, final boolean pushing, final NavigationItem item) { + if (item.menu != null) { + AndroidUtils.waitForMeasure(this, new AndroidUtils.OnMeasuredCallback() { + @Override + public void onMeasured(View view, int width, int height) { + setNavigationItemView(animate, pushing, item); + } + }); + } else { + setNavigationItemView(animate, pushing, item); + } + } + + public void setCallback(ToolbarCallback callback) { + this.callback = callback; + } + + @Override + public void onClick(View v) { + if (v == arrowMenuView) { + if (callback != null) { + callback.onMenuBackClicked(arrowMenuDrawable.getProgress() == 1f); + } + } + } + + public void setArrowMenuProgress(float progress) { + arrowMenuDrawable.setProgress(progress); + } + + private void init() { + setOrientation(HORIZONTAL); + + FrameLayout leftButtonContainer = new FrameLayout(getContext()); + addView(leftButtonContainer, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + + arrowMenuView = new ImageView(getContext()); + arrowMenuView.setOnClickListener(this); + arrowMenuView.setFocusable(true); + arrowMenuView.setScaleType(ImageView.ScaleType.CENTER); + arrowMenuDrawable = new ArrowMenuDrawable(); + arrowMenuView.setImageDrawable(arrowMenuDrawable); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + //noinspection deprecation + arrowMenuView.setBackgroundDrawable(getAttrDrawable(android.R.attr.selectableItemBackgroundBorderless)); + } else { + //noinspection deprecation + arrowMenuView.setBackgroundResource(R.drawable.gray_background_selector); + } + + leftButtonContainer.addView(arrowMenuView, new FrameLayout.LayoutParams(dp(56), FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER_VERTICAL)); + + navigationItemContainer = new FrameLayout(getContext()); + addView(navigationItemContainer, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)); + } + + private void setNavigationItemView(boolean animate, boolean pushing, NavigationItem toItem) { + toItem.view = createNavigationItemView(toItem); + + navigationItemContainer.addView(toItem.view, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + final NavigationItem fromItem = navigationItem; + + final int duration = 300; + final int offset = dp(16); + + if (animate) { + toItem.view.setAlpha(0f); + + List animations = new ArrayList<>(5); + + if (fromItem != null && fromItem.hasBack != toItem.hasBack) { + ValueAnimator arrowAnimation = ValueAnimator.ofFloat(fromItem.hasBack ? 1f : 0f, toItem.hasBack ? 1f : 0f); + arrowAnimation.setDuration(duration); + arrowAnimation.setInterpolator(new DecelerateInterpolator(2f)); + arrowAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setArrowMenuProgress((float) animation.getAnimatedValue()); + } + }); + animations.add(arrowAnimation); + } else { + setArrowMenuProgress(toItem.hasBack ? 1f : 0f); + } + + Animator toYAnimation = ObjectAnimator.ofFloat(toItem.view, View.TRANSLATION_Y, pushing ? offset : -offset, 0f); + toYAnimation.setDuration(duration); + toYAnimation.setInterpolator(new DecelerateInterpolator(2f)); + toYAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (fromItem != null) { + removeNavigationItem(fromItem); + } + } + }); + animations.add(toYAnimation); + + Animator toAlphaAnimation = ObjectAnimator.ofFloat(toItem.view, View.ALPHA, 0f, 1f); + toAlphaAnimation.setDuration(duration); + toAlphaAnimation.setInterpolator(new DecelerateInterpolator(2f)); + animations.add(toAlphaAnimation); + + if (fromItem != null) { + Animator fromYAnimation = ObjectAnimator.ofFloat(fromItem.view, View.TRANSLATION_Y, 0f, pushing ? -offset : offset); + fromYAnimation.setDuration(duration); + fromYAnimation.setInterpolator(new DecelerateInterpolator(2f)); + animations.add(fromYAnimation); + + Animator fromAlphaAnimation = ObjectAnimator.ofFloat(fromItem.view, View.ALPHA, 1f, 0f); + fromAlphaAnimation.setDuration(duration); + fromAlphaAnimation.setInterpolator(new DecelerateInterpolator(2f)); + animations.add(fromAlphaAnimation); + } + + AnimatorSet set = new AnimatorSet(); + set.setStartDelay(pushing ? 0 : 100); + set.playTogether(animations); + set.start(); + } else { + // No animation + if (fromItem != null) { + removeNavigationItem(fromItem); + } + setArrowMenuProgress(toItem.hasBack ? 1f : 0f); + } + + navigationItem = toItem; + } + + private void removeNavigationItem(NavigationItem item) { + item.view.removeAllViews(); + navigationItemContainer.removeView(item.view); + item.view = null; + } + + private LinearLayout createNavigationItemView(NavigationItem item) { + LinearLayout wrapper = new LinearLayout(getContext()); + + TextView titleView = new TextView(getContext()); + titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20); +// titleView.setTextColor(Color.argb((int)(0.87 * 255.0), 0, 0, 0)); + titleView.setTextColor(Color.argb(255, 255, 255, 255)); + titleView.setGravity(Gravity.CENTER_VERTICAL); + titleView.setSingleLine(true); + titleView.setLines(1); + titleView.setEllipsize(TextUtils.TruncateAt.END); + titleView.setPadding(dp(16), 0, 0, 0); + titleView.setTypeface(AndroidUtils.ROBOTO_MEDIUM); + titleView.setText(item.title); + wrapper.addView(titleView, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)); + + if (item.menu != null) { + wrapper.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); + } + + return wrapper; + } + + public interface ToolbarCallback { + public void onMenuBackClicked(boolean isArrow); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenu.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenu.java new file mode 100644 index 00000000..bfacad69 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenu.java @@ -0,0 +1,71 @@ +package org.floens.chan.ui.toolbar; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import org.floens.chan.R; + +import java.util.ArrayList; +import java.util.List; + +import static org.floens.chan.utils.AndroidUtils.dp; + +public class ToolbarMenu extends LinearLayout { + private List items = new ArrayList<>(); + + public ToolbarMenu(Context context) { + super(context); + init(); + } + + public ToolbarMenu(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ToolbarMenu(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + setOrientation(HORIZONTAL); + setGravity(Gravity.CENTER_VERTICAL); + +// overflowItem = new ToolbarMenuItem(getContext(), this, 100, R.drawable.ic_more_vert_white_24dp, 10 + 32); +// +// List subItems = new ArrayList<>(); +// subItems.add(new ToolbarMenuItemSubMenu.SubItem(1, "Sub 1")); +// subItems.add(new ToolbarMenuItemSubMenu.SubItem(2, "Sub 2")); +// subItems.add(new ToolbarMenuItemSubMenu.SubItem(3, "Sub 3")); +// +// ToolbarMenuItemSubMenu sub = new ToolbarMenuItemSubMenu(getContext(), overflowItem.getView(), subItems); +// overflowItem.setSubMenu(sub); +// +// addItem(overflowItem); + } + + public ToolbarMenuItem addItem(ToolbarMenuItem item) { + items.add(item); + ImageView icon = item.getView(); + if (icon != null) { + int viewIndex = Math.min(getChildCount(), item.getId()); + addView(icon, viewIndex); + } + return item; + } + + public ToolbarMenuItem createOverflow(ToolbarMenuItem.ToolbarMenuItemCallback callback) { + ToolbarMenuItem overflow = addItem(new ToolbarMenuItem(getContext(), callback, 100, R.drawable.ic_more)); + ImageView overflowImage = overflow.getView(); + // 36dp + overflowImage.setLayoutParams(new LinearLayout.LayoutParams(dp(36), dp(54))); + int p = dp(16); + overflowImage.setPadding(0, 0, p, 0); + + return overflow; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java new file mode 100644 index 00000000..f6155486 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java @@ -0,0 +1,86 @@ +package org.floens.chan.ui.toolbar; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import org.floens.chan.R; + +import static org.floens.chan.utils.AndroidUtils.dp; +import static org.floens.chan.utils.AndroidUtils.getAttrDrawable; + +public class ToolbarMenuItem implements View.OnClickListener, ToolbarMenuSubMenu.ToolbarMenuItemSubMenuCallback { + private ToolbarMenuItemCallback callback; + private int id; + private ToolbarMenuSubMenu subMenu; + + private ImageView imageView; + + public ToolbarMenuItem(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, int id, int drawable) { + this(context, callback, id, context.getResources().getDrawable(drawable)); + } + + public ToolbarMenuItem(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, int id, Drawable drawable) { + this.id = id; + this.callback = callback; + + if (drawable != null) { + imageView = new ImageView(context); + imageView.setOnClickListener(this); + imageView.setFocusable(true); + imageView.setScaleType(ImageView.ScaleType.CENTER); + imageView.setLayoutParams(new LinearLayout.LayoutParams(dp(56), dp(56))); + int p = dp(16); + imageView.setPadding(p, p, p, p); + + imageView.setImageDrawable(drawable); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + //noinspection deprecation + imageView.setBackgroundDrawable(getAttrDrawable(android.R.attr.selectableItemBackgroundBorderless)); + } else { + //noinspection deprecation + imageView.setBackgroundResource(R.drawable.gray_background_selector); + } + } + } + + public void setSubMenu(ToolbarMenuSubMenu subMenu) { + this.subMenu = subMenu; + subMenu.setCallback(this); + } + + public void showSubMenu() { + subMenu.show(); + } + + @Override + public void onClick(View v) { + if (subMenu != null) { + subMenu.show(); + } + callback.onMenuItemClicked(this); + } + + public int getId() { + return id; + } + + public ImageView getView() { + return imageView; + } + + @Override + public void onSubMenuItemClicked(ToolbarMenuSubItem item) { + callback.onSubMenuItemClicked(this, item); + } + + public interface ToolbarMenuItemCallback { + public void onMenuItemClicked(ToolbarMenuItem item); + + public void onSubMenuItemClicked(ToolbarMenuItem parent, ToolbarMenuSubItem item); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubItem.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubItem.java new file mode 100644 index 00000000..4966b782 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubItem.java @@ -0,0 +1,19 @@ +package org.floens.chan.ui.toolbar; + +public class ToolbarMenuSubItem { + private int id; + private String text; + + public ToolbarMenuSubItem(int id, String text) { + this.id = id; + this.text = text; + } + + public int getId() { + return id; + } + + public String getText() { + return text; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubMenu.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubMenu.java new file mode 100644 index 00000000..48703a59 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubMenu.java @@ -0,0 +1,112 @@ +package org.floens.chan.ui.toolbar; + +import android.content.Context; +import android.support.v7.widget.ListPopupWindow; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.PopupWindow; +import android.widget.TextView; + +import org.floens.chan.R; +import org.floens.chan.utils.AndroidUtils; + +import java.util.ArrayList; +import java.util.List; + +import static org.floens.chan.utils.AndroidUtils.dp; + +public class ToolbarMenuSubMenu { + private final Context context; + private final View anchor; + private List items; + private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener; + + private ToolbarMenuItemSubMenuCallback callback; + + public ToolbarMenuSubMenu(Context context, View anchor, List items) { + this.context = context; + this.anchor = anchor; + this.items = items; + } + + public void setCallback(ToolbarMenuItemSubMenuCallback callback) { + this.callback = callback; + } + + public void show() { + final ListPopupWindow popupWindow = new ListPopupWindow(context); + popupWindow.setAnchorView(anchor); + popupWindow.setModal(true); + popupWindow.setDropDownGravity(Gravity.RIGHT | Gravity.TOP); + popupWindow.setVerticalOffset(-anchor.getHeight() + dp(5)); + popupWindow.setHorizontalOffset(-dp(5)); + popupWindow.setContentWidth(dp(3 * 56)); + + List stringItems = new ArrayList<>(items.size()); + for (ToolbarMenuSubItem item : items) { + stringItems.add(item.getText()); + } + + popupWindow.setAdapter(new SubMenuArrayAdapter(context, R.layout.toolbar_menu_item, stringItems)); + popupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (position >= 0 && position < items.size()) { + callback.onSubMenuItemClicked(items.get(position)); + popupWindow.dismiss(); + } + } + }); + + globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (popupWindow.isShowing()) { + // Recalculate anchor position + popupWindow.show(); + } + } + }; + anchor.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener); + + popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + if (anchor.getViewTreeObserver().isAlive()) { + anchor.getViewTreeObserver().removeGlobalOnLayoutListener(globalLayoutListener); + } + globalLayoutListener = null; + } + }); + + popupWindow.show(); + } + + public interface ToolbarMenuItemSubMenuCallback { + public void onSubMenuItemClicked(ToolbarMenuSubItem item); + } + + private static class SubMenuArrayAdapter extends ArrayAdapter { + public SubMenuArrayAdapter(Context context, int resource, List objects) { + super(context, resource, objects); + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.toolbar_menu_item, parent, false); + } + + TextView textView = (TextView) convertView; + textView.setText(getItem(position)); + textView.setTypeface(AndroidUtils.ROBOTO_MEDIUM); + + return textView; + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java index a21c81c1..aac31352 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java @@ -20,7 +20,7 @@ import android.util.AttributeSet; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; -import org.floens.chan.utils.Utils; +import org.floens.chan.utils.AndroidUtils; public class CustomScaleImageView extends SubsamplingScaleImageView { private InitedCallback initCallback; @@ -41,7 +41,7 @@ public class CustomScaleImageView extends SubsamplingScaleImageView { protected void onImageReady() { super.onImageReady(); - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { if (initCallback != null) { @@ -55,7 +55,7 @@ public class CustomScaleImageView extends SubsamplingScaleImageView { protected void onOutOfMemory() { super.onOutOfMemory(); - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { if (initCallback != null) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/HackyViewPager.java b/Clover/app/src/main/java/org/floens/chan/ui/view/HackyViewPager.java index cb16e6d7..71b870e1 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/HackyViewPager.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/HackyViewPager.java @@ -25,11 +25,11 @@ import android.view.MotionEvent; /** * Hacky fix for Issue #4 and * http://code.google.com/p/android/issues/detail?id=18990 - * + *

* ScaleGestureDetector seems to mess up the touch events, which means that * ViewGroups which make use of onInterceptTouchEvent throw a lot of * IllegalArgumentException: pointerIndex out of range. - * + *

* There's not much I can do in my code for now, but we can mask the result by * just catching the problem and ignoring it. * diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java index 5065c562..ac49fb5c 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java @@ -18,22 +18,27 @@ package org.floens.chan.ui.view; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.widget.FrameLayout; -import android.widget.LinearLayout; import android.widget.ProgressBar; -import org.floens.chan.utils.SimpleAnimatorListener; +import java.util.HashMap; +import java.util.Map; /** * Container for a view with an ProgressBar. Toggles between the view and a * ProgressBar. */ public class LoadView extends FrameLayout { - public int fadeDuration = 100; + private int fadeDuration = 200; + private Map animatorsIn = new HashMap<>(); + private Map animatorsOut = new HashMap<>(); public LoadView(Context context) { super(context); @@ -54,6 +59,10 @@ public class LoadView extends FrameLayout { setView(null, false); } + public void setFadeDuration(int fadeDuration) { + this.fadeDuration = fadeDuration; + } + /** * Set the content of this container. It will fade the old one out with the * new one. Set view to null to show the progressbar. @@ -64,40 +73,86 @@ public class LoadView extends FrameLayout { setView(view, true); } - public void setView(View view, boolean animation) { - if (view == null) { - LinearLayout layout = new LinearLayout(getContext()); - layout.setGravity(Gravity.CENTER); + public void setView(View newView, boolean animate) { + // Passing null means showing a progressbar + if (newView == null) { + FrameLayout progressBar = new FrameLayout(getContext()); + progressBar.addView(new ProgressBar(getContext()), new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER)); + newView = progressBar; + } - ProgressBar pb = new ProgressBar(getContext()); - layout.addView(pb); - view = layout; + // Readded while still running a add/remove animation for the new view + // This also removes the new view from this view + AnimatorSet out = animatorsOut.remove(newView); + if (out != null) { + out.cancel(); } - while (getChildCount() > 1) { - removeViewAt(0); + AnimatorSet in = animatorsIn.remove(newView); + if (in != null) { + in.cancel(); } - View currentView = getChildAt(0); - if (currentView != null) { - if (animation) { - final View tempView = currentView; - currentView.animate().setDuration(fadeDuration).alpha(0).setListener(new SimpleAnimatorListener() { - @Override - public void onAnimationEnd(Animator animation) { - removeView(tempView); - } - }); - } else { - removeView(currentView); + // Add fade out animations for all remaining view + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child != null) { + AnimatorSet inSet = animatorsIn.remove(child); + if (inSet != null) { + inSet.cancel(); + } + + if (!animatorsOut.containsKey(child)) { + animateViewOut(child); + } } } - addView(view); + addView(newView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - if (animation) { - view.setAlpha(0f); - view.animate().setDuration(fadeDuration).alpha(1f); + if (animate) { + // Fade view in + if (newView.getAlpha() == 1f) { + newView.setAlpha(0f); + } + animateViewIn(newView); + } else { + newView.setAlpha(1f); } } + + private void animateViewOut(final View view) { + // Cancel any fade in animations + AnimatorSet fadeIn = animatorsIn.remove(view); + if (fadeIn != null) { + fadeIn.cancel(); + } + + final AnimatorSet set = new AnimatorSet(); + set.setDuration(fadeDuration); + set.play(ObjectAnimator.ofFloat(view, View.ALPHA, 0f)); + animatorsOut.put(view, set); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + removeView(view); + animatorsOut.remove(set); + } + }); + set.start(); + } + + private void animateViewIn(View view) { + final AnimatorSet set = new AnimatorSet(); + set.setDuration(fadeDuration); + set.play(ObjectAnimator.ofFloat(view, View.ALPHA, 1f)); + animatorsIn.put(view, set); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animatorsIn.remove(set); + } + }); + set.start(); + } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java index 5473f4c7..b4569c67 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java @@ -31,6 +31,8 @@ import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.util.AttributeSet; import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; @@ -43,13 +45,14 @@ import com.android.volley.toolbox.NetworkImageView; import org.floens.chan.ChanApplication; import org.floens.chan.R; -import org.floens.chan.core.manager.ThreadManager; +import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostLinkable; import org.floens.chan.utils.IconCache; import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.Time; -import org.floens.chan.utils.Utils; + +import static org.floens.chan.utils.AndroidUtils.setPressedDrawable; public class PostView extends LinearLayout implements View.OnClickListener { private final static LinearLayout.LayoutParams matchParams = new LinearLayout.LayoutParams( @@ -63,8 +66,9 @@ public class PostView extends LinearLayout implements View.OnClickListener { private final Activity context; - private ThreadManager manager; private Post post; + private PostViewCallback callback; + private Loadable loadable; private int highlightQuotesNo = -1; private boolean isBuild = false; @@ -113,13 +117,14 @@ public class PostView extends LinearLayout implements View.OnClickListener { } } - public void setPost(final Post post, final ThreadManager manager) { + public void setPost(final Post post, final PostViewCallback callback) { this.post = post; - this.manager = manager; + this.callback = callback; + this.loadable = callback.getLoadable(); highlightQuotesNo = -1; - boolean boardCatalogMode = manager.getLoadable().isBoardMode() || manager.getLoadable().isCatalogMode(); + boolean boardCatalogMode = loadable.isBoardMode() || loadable.isCatalogMode(); TypedArray ta = context.obtainStyledAttributes(null, R.styleable.PostView, R.attr.post_style, 0); @@ -167,7 +172,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { commentView.setText(post.comment); - if (manager.getLoadable().isThreadMode()) { + if (loadable.isThreadMode()) { post.setLinkableListener(this); commentView.setMovementMethod(new PostViewMovementMethod()); commentView.setOnClickListener(this); @@ -199,11 +204,11 @@ public class PostView extends LinearLayout implements View.OnClickListener { } } - if (manager.getLoadable().isThreadMode()) { + if (loadable.isThreadMode()) { repliesCountView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - manager.showPostReplies(post); + callback.onShowPostReplies(post); } }); } @@ -236,13 +241,13 @@ public class PostView extends LinearLayout implements View.OnClickListener { if (post.isSavedReply) { full.setBackgroundColor(savedReplyColor); - } else if (manager.isPostHightlighted(post)) { + } else if (callback.isPostHightlighted(post)) { full.setBackgroundColor(highlightedColor); } else { full.setBackgroundColor(0x00000000); } - if (manager.isPostLastSeen(post)) { + if (callback.isPostLastSeen(post)) { lastSeen.setVisibility(View.VISIBLE); } else { lastSeen.setVisibility(View.GONE); @@ -318,7 +323,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { imageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - manager.onThumbnailClicked(post); + callback.onThumbnailClicked(post); } }); @@ -382,7 +387,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { if (isList()) { commentView.setPadding(postPadding, commentPadding, postPadding, commentPadding); - if (manager.getLoadable().isBoardMode() || manager.getLoadable().isCatalogMode()) { + if (loadable.isBoardMode() || loadable.isCatalogMode()) { commentView.setMaxHeight(postListMaxHeight); } } else if (isGrid()) { @@ -398,7 +403,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { } repliesCountView = new TextView(context); - Utils.setPressedDrawable(repliesCountView); + setPressedDrawable(repliesCountView); repliesCountView.setTextColor(replyCountColor); repliesCountView.setPadding(postPadding, postPadding, postPadding, postPadding); repliesCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, repliesCountSize); @@ -410,21 +415,28 @@ public class PostView extends LinearLayout implements View.OnClickListener { lastSeen.setBackgroundColor(0xffff0000); contentContainer.addView(lastSeen, new LayoutParams(LayoutParams.MATCH_PARENT, lastSeenHeight)); - if (!manager.getLoadable().isThreadMode()) { - Utils.setPressedDrawable(contentContainer); + if (!loadable.isThreadMode()) { + setPressedDrawable(contentContainer); } full.addView(contentContainer, matchWrapParams); optionsView = new ImageView(context); optionsView.setImageResource(R.drawable.ic_overflow); - Utils.setPressedDrawable(optionsView); + setPressedDrawable(optionsView); optionsView.setPadding(optionsLeftPadding, optionsTopPadding, optionsRightPadding, optionsBottomPadding); optionsView.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { PopupMenu popupMenu = new PopupMenu(context, v); - manager.showPostOptions(post, popupMenu); + callback.onPopulatePostOptions(post, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + callback.onPostOptionClicked(post, item.getItemId()); + return true; + } + }); popupMenu.show(); if (ThemeHelper.getInstance().getTheme().isLightTheme) { optionsView.setImageResource(R.drawable.ic_overflow_black); @@ -454,20 +466,44 @@ public class PostView extends LinearLayout implements View.OnClickListener { } public void onLinkableClick(PostLinkable linkable) { - manager.onPostLinkableClicked(linkable); + callback.onPostLinkableClicked(linkable); } @Override public void onClick(View v) { - manager.onPostClicked(post); + callback.onPostClicked(post); } private boolean isList() { - return manager.getViewMode() == ThreadManager.ViewMode.LIST; + return true; + // TODO +// return callback.getViewMode() == ThreadManager.ViewMode.LIST; } private boolean isGrid() { - return manager.getViewMode() == ThreadManager.ViewMode.GRID; + return false; + // TODO +// return callback.getViewMode() == ThreadManager.ViewMode.GRID; + } + + public interface PostViewCallback { + public Loadable getLoadable(); + + public void onPostClicked(Post post); + + public void onThumbnailClicked(Post post); + + public void onShowPostReplies(Post post); + + public void onPopulatePostOptions(Post post, Menu menu); + + public void onPostOptionClicked(Post post, int id); + + public void onPostLinkableClicked(PostLinkable linkable); + + public boolean isPostHightlighted(Post post); + + public boolean isPostLastSeen(Post post); } private class PostViewMovementMethod extends LinkMovementMethod { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java index 773b1141..ad6bb938 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java @@ -38,9 +38,9 @@ import com.koushikdutta.async.future.Future; import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.core.ChanPreferences; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.FileCache; import org.floens.chan.utils.Logger; -import org.floens.chan.utils.Utils; import java.io.File; import java.io.IOException; @@ -107,7 +107,7 @@ public class ThumbnailImageView extends LoadView implements View.OnClickListener if (response.getBitmap() != null && thumbnailNeeded) { ImageView thumbnail = new ImageView(getContext()); thumbnail.setImageBitmap(response.getBitmap()); - thumbnail.setLayoutParams(Utils.MATCH_PARAMS); + thumbnail.setLayoutParams(AndroidUtils.MATCH_PARAMS); setView(thumbnail, false); } } @@ -222,7 +222,7 @@ public class ThumbnailImageView extends LoadView implements View.OnClickListener GifImageView view = new GifImageView(getContext()); view.setImageDrawable(drawable); - view.setLayoutParams(Utils.MATCH_PARAMS); + view.setLayoutParams(AndroidUtils.MATCH_PARAMS); setView(view, false); } @@ -273,7 +273,7 @@ public class ThumbnailImageView extends LoadView implements View.OnClickListener videoView.setZOrderOnTop(true); videoView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - videoView.setLayoutParams(Utils.MATCH_PARAMS); + videoView.setLayoutParams(AndroidUtils.MATCH_PARAMS); LayoutParams par = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); par.gravity = Gravity.CENTER; videoView.setLayoutParams(par); diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java new file mode 100644 index 00000000..4e37e25c --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java @@ -0,0 +1,193 @@ +package org.floens.chan.utils; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.inputmethod.InputMethodManager; +import android.widget.Toast; + +import org.floens.chan.ChanApplication; +import org.floens.chan.R; + +import java.util.HashMap; + +public class AndroidUtils { + public final static ViewGroup.LayoutParams MATCH_PARAMS = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + public final static ViewGroup.LayoutParams WRAP_PARAMS = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + public final static ViewGroup.LayoutParams MATCH_WRAP_PARAMS = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + public final static ViewGroup.LayoutParams WRAP_MATCH_PARAMS = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); + private static HashMap typefaceCache = new HashMap<>(); + + public static Typeface ROBOTO_MEDIUM; + + public static void init() { + ROBOTO_MEDIUM = getTypeface("Roboto-Medium.ttf"); + } + + public static Resources getRes() { + return ChanApplication.con.getResources(); + } + + public static Context getAppRes() { + return ChanApplication.con; + } + + public static void openLink(String link) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (intent.resolveActivity(getAppRes().getPackageManager()) != null) { + getAppRes().startActivity(intent); + } else { + Toast.makeText(getAppRes(), R.string.open_link_failed, Toast.LENGTH_LONG).show(); + } + } + + public static void shareLink(String link) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, link); + Intent chooser = Intent.createChooser(intent, getRes().getString(R.string.action_share)); + chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (chooser.resolveActivity(getAppRes().getPackageManager()) != null) { + getAppRes().startActivity(chooser); + } else { + Toast.makeText(getAppRes(), R.string.open_link_failed, Toast.LENGTH_LONG).show(); + } + } + + public static int getAttrPixel(int attr) { + TypedArray typedArray = ChanApplication.con.getTheme().obtainStyledAttributes(new int[]{attr}); + int pixels = typedArray.getDimensionPixelSize(0, 0); + typedArray.recycle(); + return pixels; + } + + public static Drawable getAttrDrawable(int attr) { + TypedArray typedArray = ChanApplication.con.getTheme().obtainStyledAttributes(new int[]{attr}); + Drawable drawable = typedArray.getDrawable(0); + typedArray.recycle(); + return drawable; + } + + public static int dp(float dp) { + return (int) (dp * getRes().getDisplayMetrics().density); + } + + public static Typeface getTypeface(String name) { + if (!typefaceCache.containsKey(name)) { + Typeface typeface = Typeface.createFromAsset(getRes().getAssets(), "font/" + name); + typefaceCache.put(name, typeface); + } + return typefaceCache.get(name); + } + + /** + * Causes the runnable to be added to the message queue. The runnable will + * be run on the ui thread. + * + * @param runnable + */ + public static void runOnUiThread(Runnable runnable) { + new Handler(Looper.getMainLooper()).post(runnable); + } + + public static void requestKeyboardFocus(Dialog dialog, final View view) { + view.requestFocus(); + dialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(view, 0); + } + }); + } + + public static String getReadableFileSize(int bytes, boolean si) { + int unit = si ? 1000 : 1024; + if (bytes < unit) + return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); + return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); + } + + public static CharSequence ellipsize(CharSequence text, int max) { + if (text.length() <= max) { + return text; + } else { + return text.subSequence(0, max) + "\u2026"; + } + } + + public interface OnMeasuredCallback { + void onMeasured(View view, int width, int height); + } + + /** + * Waits for a measure. Calls callback immediately if the view width and height are more than 0. + * Otherwise it registers an onpredrawlistener and rechedules a layout. + * Warning: the view you give must be attached to the view root!!! + */ + public static void waitForMeasure(final View view, final OnMeasuredCallback callback) { + waitForMeasure(true, view, callback); + } + + public static void waitForLayout(final View view, final OnMeasuredCallback callback) { + waitForMeasure(false, view, callback); + } + + private static void waitForMeasure(boolean returnIfZero, final View view, final OnMeasuredCallback callback) { + int width = view.getWidth(); + int height = view.getHeight(); + + if (returnIfZero && width > 0 && height > 0) { + callback.onMeasured(view, width, height); + } else { + view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + final ViewTreeObserver observer = view.getViewTreeObserver(); + if (observer.isAlive()) { + observer.removeOnPreDrawListener(this); + } + + try { + callback.onMeasured(view, view.getWidth(), view.getHeight()); + } catch (Exception e) { + Log.i("AndroidUtils", "Exception in onMeasured", e); + } + + return true; + } + }); + } + } + + public static void setPressedDrawable(View view) { + TypedArray arr = view.getContext().obtainStyledAttributes(new int[]{android.R.attr.selectableItemBackground}); + + Drawable drawable = arr.getDrawable(0); + + arr.recycle(); + view.setBackgroundDrawable(drawable); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java new file mode 100644 index 00000000..c938476e --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java @@ -0,0 +1,51 @@ +package org.floens.chan.utils; + +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; +import android.widget.ImageView; + +public class AnimationUtils { + /** + * On your start view call startView.getGlobalVisibleRect(startBounds) + * and on your end container call endContainer.getGlobalVisibleRect(endBounds, globalOffset);
+ * startBounds and endBounds will be adjusted appropriately and the starting scale will be returned. + * + * @param startBounds your startBounds + * @param endBounds your endBounds + * @param globalOffset your globalOffset + * @return the starting scale + */ + public static float calculateBoundsAnimation(Rect startBounds, Rect endBounds, Point globalOffset) { + startBounds.offset(-globalOffset.x, -globalOffset.y); + endBounds.offset(-globalOffset.x, -globalOffset.y); + + float startScale; + if ((float) endBounds.width() / endBounds.height() > (float) startBounds.width() / startBounds.height()) { + // Extend start bounds horizontally + startScale = (float) startBounds.height() / endBounds.height(); + float startWidth = startScale * endBounds.width(); + float deltaWidth = (startWidth - startBounds.width()) / 2; + startBounds.left -= deltaWidth; + startBounds.right += deltaWidth; + } else { + // Extend start bounds vertically + startScale = (float) startBounds.width() / endBounds.width(); + float startHeight = startScale * endBounds.height(); + float deltaHeight = (startHeight - startBounds.height()) / 2; + startBounds.top -= deltaHeight; + startBounds.bottom += deltaHeight; + } + + return startScale; + } + + public static void adjustImageViewBoundsToDrawableBounds(ImageView imageView, Rect bounds) { + float[] f = new float[9]; + imageView.getImageMatrix().getValues(f); + bounds.left += f[Matrix.MTRANS_X]; + bounds.top += f[Matrix.MTRANS_Y]; + bounds.right = (bounds.left + (int) (imageView.getDrawable().getIntrinsicWidth() * f[Matrix.MSCALE_X])); + bounds.bottom = (bounds.top + (int) (imageView.getDrawable().getIntrinsicHeight() * f[Matrix.MSCALE_Y])); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java b/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java index 3ea6ee97..233b1bc6 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java @@ -60,7 +60,7 @@ public class FileCache { .progress(new ProgressCallback() { @Override public void onProgress(final long downloaded, final long total) { - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { callback.onProgress(downloaded, total, false); diff --git a/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java index 1e7ff38d..9e051692 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java @@ -17,6 +17,7 @@ */ package org.floens.chan.utils; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -26,43 +27,47 @@ import java.io.StringWriter; import java.io.Writer; public class IOUtils { + private static final int DEFAULT_BUFFER_SIZE = 4096; + public static String readString(InputStream is) { - StringWriter sw = new StringWriter(); + InputStreamReader reader = new InputStreamReader(is); + StringWriter writer = new StringWriter(); try { - copy(new InputStreamReader(is), sw); - is.close(); - sw.close(); + copy(reader, writer); } catch (IOException e) { e.printStackTrace(); + } finally { + closeQuietly(writer); + closeQuietly(reader); } - return sw.toString(); + return writer.toString(); } - /** - * Copies the inputstream to the outputstream and closes both streams. - * - * @param is - * @param os - * @throws IOException - */ public static void copy(InputStream is, OutputStream os) throws IOException { int read; - byte[] buffer = new byte[4096]; + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; while ((read = is.read(buffer)) != -1) { os.write(buffer, 0, read); } - - is.close(); - os.close(); } public static void copy(Reader input, Writer output) throws IOException { - char[] buffer = new char[4096]; + char[] buffer = new char[DEFAULT_BUFFER_SIZE]; int read; while ((read = input.read(buffer)) != -1) { output.write(buffer, 0, read); } } + + public static void closeQuietly(Closeable stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + // ignore + } + } + } } diff --git a/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java b/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java index d0631708..0639dad6 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java @@ -49,17 +49,12 @@ public class ImageDecoder { try { IOUtils.copy(fis, baos); - bitmap = decode(baos.toByteArray(), maxWidth, maxHeight); } catch (IOException | OutOfMemoryError e) { e.printStackTrace(); } finally { - try { - fis.close(); - baos.close(); - } catch (IOException e) { - e.printStackTrace(); - } + IOUtils.closeQuietly(fis); + IOUtils.closeQuietly(baos); } return bitmap; diff --git a/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java b/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java index b9e9c526..db95a2b8 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java @@ -38,6 +38,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -120,7 +122,7 @@ public class ImageSaver { } private void showToast(final Context context, final String message) { - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(context, message, Toast.LENGTH_LONG).show(); @@ -250,11 +252,16 @@ public class ImageSaver { private boolean storeImage(final File source, final File destination) { boolean res = true; - + InputStream is = null; + OutputStream os = null; try { + is = new FileInputStream(source); + os = new FileOutputStream(destination); IOUtils.copy(new FileInputStream(source), new FileOutputStream(destination)); } catch (IOException e) { res = false; + IOUtils.closeQuietly(is); + IOUtils.closeQuietly(os); } return res; @@ -265,7 +272,7 @@ public class ImageSaver { new MediaScannerConnection.OnScanCompletedListener() { @Override public void onScanCompleted(String unused, final Uri uri) { - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { Logger.i(TAG, "Media scan succeeded: " + uri); diff --git a/Clover/app/src/main/java/org/floens/chan/utils/SimpleAnimatorListener.java b/Clover/app/src/main/java/org/floens/chan/utils/SimpleAnimatorListener.java deleted file mode 100644 index ecafd1fd..00000000 --- a/Clover/app/src/main/java/org/floens/chan/utils/SimpleAnimatorListener.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Clover - 4chan browser https://github.com/Floens/Clover/ - * Copyright (C) 2014 Floens - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.floens.chan.utils; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; - -/** - * Extends AnimatorListener with no-op methods. - */ -public class SimpleAnimatorListener implements AnimatorListener { - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - - @Override - public void onAnimationStart(Animator animation) { - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/utils/Utils.java b/Clover/app/src/main/java/org/floens/chan/utils/Utils.java deleted file mode 100644 index 6bbef463..00000000 --- a/Clover/app/src/main/java/org/floens/chan/utils/Utils.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Clover - 4chan browser https://github.com/Floens/Clover/ - * Copyright (C) 2014 Floens - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.floens.chan.utils; - -import android.app.Dialog; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnShowListener; -import android.content.Intent; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.util.DisplayMetrics; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.Toast; - -import org.floens.chan.ChanApplication; -import org.floens.chan.R; - -public class Utils { - private static DisplayMetrics displayMetrics; - - public final static ViewGroup.LayoutParams MATCH_PARAMS = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - public final static ViewGroup.LayoutParams WRAP_PARAMS = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - public final static ViewGroup.LayoutParams MATCH_WRAP_PARAMS = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - public final static ViewGroup.LayoutParams WRAP_MATCH_PARAMS = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); - - public static int dp(float dp) { - // return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); - if (displayMetrics == null) { - displayMetrics = ChanApplication.getInstance().getResources().getDisplayMetrics(); - } - - return (int) (dp * displayMetrics.density); - } - - /** - * Sets the android.R.attr.selectableItemBackground as background drawable - * on the view. - * - * @param view - */ - @SuppressWarnings("deprecation") - public static void setPressedDrawable(View view) { - Drawable drawable = Utils.getSelectableBackgroundDrawable(view.getContext()); - view.setBackgroundDrawable(drawable); - } - - public static Drawable getSelectableBackgroundDrawable(Context context) { - TypedArray arr = context.obtainStyledAttributes(new int[]{android.R.attr.selectableItemBackground}); - - Drawable drawable = arr.getDrawable(0); - - arr.recycle(); - - return drawable; - } - - /** - * Causes the runnable to be added to the message queue. The runnable will - * be run on the ui thread. - * - * @param runnable - */ - public static void runOnUiThread(Runnable runnable) { - new Handler(Looper.getMainLooper()).post(runnable); - } - - public static void requestKeyboardFocus(Dialog dialog, final View view) { - view.requestFocus(); - dialog.setOnShowListener(new OnShowListener() { - @Override - public void onShow(DialogInterface dialog) { - InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService( - Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(view, 0); - } - }); - } - - public static void openLink(Context context, String link) { - try { - context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(link))); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - Toast.makeText(context, R.string.open_link_failed, Toast.LENGTH_LONG).show(); - } - } - - public static String getReadableFileSize(int bytes, boolean si) { - int unit = si ? 1000 : 1024; - if (bytes < unit) - return bytes + " B"; - int exp = (int) (Math.log(bytes) / Math.log(unit)); - String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); - return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); - } - - public static CharSequence ellipsize(CharSequence text, int max) { - if (text.length() <= max) { - return text; - } else { - return text.subSequence(0, max) + "\u2026"; - } - } -} diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_more.png b/Clover/app/src/main/res/drawable-hdpi/ic_more.png new file mode 100644 index 0000000000000000000000000000000000000000..fdc4a5ad2677ebf97418aef7ae47e5e6a9b3c286 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;Lb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZK3>dAqwX{BQ3+vmeOgEbxddW?$M34UQE3J=p25@A K&t;ucLK6TzyF#4+ literal 0 HcmV?d00001 diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_more.png b/Clover/app/src/main/res/drawable-mdpi/ic_more.png new file mode 100644 index 0000000000000000000000000000000000000000..1d8ad18a0c5d891d971e991e143c3df0a9e15644 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G&@^*J&_}|`tWO=~G=WkM1 zMTcj-fMuPav=7~~Ah7&(%{n}Mb=c)I$ztaD0e0st7?IAs6; literal 0 HcmV?d00001 diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_more.png b/Clover/app/src/main/res/drawable-xhdpi/ic_more.png new file mode 100644 index 0000000000000000000000000000000000000000..1b04eda04ac26a1ef4748b50971e9ce2a7e6cbf5 GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tg=CK)Uj~LMH3o);76yi2K%s^g z3=E|}g|8AA7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+;1OBOz`!jG!i)^F=12eq zTRmMILn02ponpw3e=mU3>1=b9@{%=^O5ariISQh`ZfrR?hxs_miKSeSdnFZM>z< zz59>q{f@@!IrskAElpYJq&KPiiPBCFhMgWj))Fp8FZP&!(u-Yx-6=N#I*7s3)z4*} HQ$iB}uzOqu literal 0 HcmV?d00001 diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_more.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_more.png new file mode 100644 index 0000000000000000000000000000000000000000..2955c02ecd66deb2349ba92839bb00afbfb3791d GIT binary patch literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g$s z6x`_P;uw-~@9i~ju4V@ThKn~{-wT`+bp1H3PE5)2)Pr@KI9d|Y#67rIzT>uGYg^}H zbzRM|iG@>0<;C>{vl?6H&Yd|=ORD7Yx!*Y^6BqnCP&rAja6#jxlEMW`Ic7a_s1}^U zy0=WP>6k@8Jj>R?1zm5OH@nsSb}3d^8?)Ho-SxPOn(?K&?^bcuj!iml-4~NgE`X^o wx44B;45NC)79Y(3Q^!LmC;;vKYyE=p+>yE$dwEZ00v*rb>FVdQ&MBb@02ESjI(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g&? zz`)4p>EaktaqI03N3LcA5!Z`nR&{GWiVB_7n5}uqRb!XX+$W`X7D-B;SpI!?qs)aP zG3x2R3xQ_Afd>Btt&GbHomcOXif)KnyiZ+g#U`c|U$5QtvE;C1SiJ67G{cr%^D6#t zFR)k8Fm?XfpvxI?ie1L@0H1<&f-lpvPmFe)FHW(Wbam!_$$Y4=`<_R&w0hyvmnA>- zZP)xc6Ut?;9~3e_`KM+5a%tYEE!o%Cy!%%^pPLu#fCbzaTVFVpDDD!7WMzo$&~p)A y1QgkE0phw{0#Koep$r$G>S8;fS|LjQGyh=NbH+>|^^#^ONW|0C&t;ucLK6TZw}Sit literal 0 HcmV?d00001 diff --git a/Clover/app/src/main/res/drawable/gray_background_selector.xml b/Clover/app/src/main/res/drawable/gray_background_selector.xml new file mode 100644 index 00000000..9693dac2 --- /dev/null +++ b/Clover/app/src/main/res/drawable/gray_background_selector.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Clover/app/src/main/res/layout/image_view_layout.xml b/Clover/app/src/main/res/layout/image_view_layout.xml new file mode 100644 index 00000000..7a14f1e6 --- /dev/null +++ b/Clover/app/src/main/res/layout/image_view_layout.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/Clover/app/src/main/res/layout/root_layout.xml b/Clover/app/src/main/res/layout/root_layout.xml new file mode 100644 index 00000000..a9224edb --- /dev/null +++ b/Clover/app/src/main/res/layout/root_layout.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/Clover/app/src/main/res/layout/settings_layout.xml b/Clover/app/src/main/res/layout/settings_layout.xml new file mode 100644 index 00000000..e53a7844 --- /dev/null +++ b/Clover/app/src/main/res/layout/settings_layout.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/Clover/app/src/main/res/layout/toolbar_menu_item.xml b/Clover/app/src/main/res/layout/toolbar_menu_item.xml new file mode 100644 index 00000000..b0bb324a --- /dev/null +++ b/Clover/app/src/main/res/layout/toolbar_menu_item.xml @@ -0,0 +1,9 @@ + + diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index c0968006..dc0816aa 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -103,7 +103,7 @@ along with this program. If not, see . images post posts - Info + Post info Quote Quote text diff --git a/Clover/app/src/main/res/values/styles.xml b/Clover/app/src/main/res/values/styles.xml index 627003da..531aaf6d 100644 --- a/Clover/app/src/main/res/values/styles.xml +++ b/Clover/app/src/main/res/values/styles.xml @@ -22,9 +22,17 @@ along with this program. If not, see . @color/primary_dark @color/accent_material_light + @android:color/white + + @style/ToolbarDropDownListViewStyle true + + +