Start of basis controller based system.

tempwork
Floens 11 years ago
parent 3d70e4edec
commit bfef68a78b
  1. BIN
      Clover/app/src/main/assets/font/Roboto-Medium.ttf
  2. 8
      Clover/app/src/main/java/org/floens/chan/ChanApplication.java
  3. 44
      Clover/app/src/main/java/org/floens/chan/controller/Controller.java
  4. 22
      Clover/app/src/main/java/org/floens/chan/controller/ControllerTransition.java
  5. 173
      Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java
  6. 38
      Clover/app/src/main/java/org/floens/chan/controller/PopControllerTransition.java
  7. 37
      Clover/app/src/main/java/org/floens/chan/controller/PushControllerTransition.java
  8. 32
      Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java
  9. 30
      Clover/app/src/main/java/org/floens/chan/core/loader/LoaderPool.java
  10. 6
      Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java
  11. 104
      Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java
  12. 4
      Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java
  13. 8
      Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java
  14. 291
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  15. 30
      Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java
  16. 8
      Clover/app/src/main/java/org/floens/chan/ui/SwipeDismissListViewTouchListener.java
  17. 8
      Clover/app/src/main/java/org/floens/chan/ui/activity/BaseActivity.java
  18. 40
      Clover/app/src/main/java/org/floens/chan/ui/activity/BoardActivity.java
  19. 4
      Clover/app/src/main/java/org/floens/chan/ui/activity/BoardEditor.java
  20. 37
      Clover/app/src/main/java/org/floens/chan/ui/activity/ChanActivity.java
  21. 6
      Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java
  22. 2
      Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java
  23. 4
      Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java
  24. 5
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java
  25. 78
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java
  26. 102
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  27. 65
      Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java
  28. 21
      Clover/app/src/main/java/org/floens/chan/ui/controller/SettingsController.java
  29. 54
      Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java
  30. 153
      Clover/app/src/main/java/org/floens/chan/ui/drawable/ArrowMenuDrawable.java
  31. 14
      Clover/app/src/main/java/org/floens/chan/ui/fragment/ImageViewFragment.java
  32. 49
      Clover/app/src/main/java/org/floens/chan/ui/fragment/PostRepliesFragment.java
  33. 12
      Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java
  34. 73
      Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java
  35. 85
      Clover/app/src/main/java/org/floens/chan/ui/helper/PostPopupHelper.java
  36. 234
      Clover/app/src/main/java/org/floens/chan/ui/layout/ImageViewLayout.java
  37. 157
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  38. 79
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  39. 4
      Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java
  40. 10
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java
  41. 220
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
  42. 71
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenu.java
  43. 86
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java
  44. 19
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubItem.java
  45. 112
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubMenu.java
  46. 6
      Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java
  47. 4
      Clover/app/src/main/java/org/floens/chan/ui/view/HackyViewPager.java
  48. 111
      Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java
  49. 80
      Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java
  50. 8
      Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java
  51. 193
      Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
  52. 51
      Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java
  53. 2
      Clover/app/src/main/java/org/floens/chan/utils/FileCache.java
  54. 39
      Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java
  55. 9
      Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java
  56. 13
      Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java
  57. 42
      Clover/app/src/main/java/org/floens/chan/utils/SimpleAnimatorListener.java
  58. 130
      Clover/app/src/main/java/org/floens/chan/utils/Utils.java
  59. BIN
      Clover/app/src/main/res/drawable-hdpi/ic_more.png
  60. BIN
      Clover/app/src/main/res/drawable-mdpi/ic_more.png
  61. BIN
      Clover/app/src/main/res/drawable-xhdpi/ic_more.png
  62. BIN
      Clover/app/src/main/res/drawable-xxhdpi/ic_more.png
  63. BIN
      Clover/app/src/main/res/drawable-xxxhdpi/ic_more.png
  64. 14
      Clover/app/src/main/res/drawable/gray_background_selector.xml
  65. 12
      Clover/app/src/main/res/layout/image_view_layout.xml
  66. 42
      Clover/app/src/main/res/layout/root_layout.xml
  67. 13
      Clover/app/src/main/res/layout/settings_layout.xml
  68. 9
      Clover/app/src/main/res/layout/toolbar_menu_item.xml
  69. 2
      Clover/app/src/main/res/values/strings.xml
  70. 8
      Clover/app/src/main/res/values/styles.xml
  71. BIN
      docs/fonts/Roboto-Black.ttf
  72. BIN
      docs/fonts/Roboto-BlackItalic.ttf
  73. BIN
      docs/fonts/Roboto-Bold.ttf
  74. BIN
      docs/fonts/Roboto-BoldItalic.ttf
  75. BIN
      docs/fonts/Roboto-Italic.ttf
  76. BIN
      docs/fonts/Roboto-Light.ttf
  77. BIN
      docs/fonts/Roboto-LightItalic.ttf
  78. BIN
      docs/fonts/Roboto-Medium.ttf
  79. BIN
      docs/fonts/Roboto-MediumItalic.ttf
  80. BIN
      docs/fonts/Roboto-Regular.ttf
  81. BIN
      docs/fonts/Roboto-Thin.ttf
  82. BIN
      docs/fonts/Roboto-ThinItalic.ttf
  83. BIN
      docs/fonts/RobotoCondensed-Bold.ttf
  84. BIN
      docs/fonts/RobotoCondensed-BoldItalic.ttf
  85. BIN
      docs/fonts/RobotoCondensed-Italic.ttf
  86. BIN
      docs/fonts/RobotoCondensed-Light.ttf
  87. BIN
      docs/fonts/RobotoCondensed-LightItalic.ttf
  88. BIN
      docs/fonts/RobotoCondensed-Regular.ttf

@ -18,6 +18,7 @@
package org.floens.chan; package org.floens.chan;
import android.app.Application; import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.view.ViewConfiguration; 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.manager.WatchManager;
import org.floens.chan.core.net.BitmapLruImageCache; import org.floens.chan.core.net.BitmapLruImageCache;
import org.floens.chan.database.DatabaseManager; import org.floens.chan.database.DatabaseManager;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.FileCache; import org.floens.chan.utils.FileCache;
import org.floens.chan.utils.IconCache; import org.floens.chan.utils.IconCache;
import org.floens.chan.utils.Logger; 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_LRU_CACHE_SIZE = 8 * 1024 * 1024; // 8mb
private static final int VOLLEY_CACHE_SIZE = 10 * 1024 * 1024; // 8mb private static final int VOLLEY_CACHE_SIZE = 10 * 1024 * 1024; // 8mb
public static Context con;
private static ChanApplication instance; private static ChanApplication instance;
private static RequestQueue volleyRequestQueue; private static RequestQueue volleyRequestQueue;
private static com.android.volley.toolbox.ImageLoader imageLoader; private static com.android.volley.toolbox.ImageLoader imageLoader;
@ -113,6 +117,8 @@ public class ChanApplication extends Application {
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
con = this;
// Force the overflow button to show, even on devices that have a // Force the overflow button to show, even on devices that have a
// physical button. // physical button.
try { try {
@ -130,6 +136,8 @@ public class ChanApplication extends Application {
// StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build()); // StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build());
} }
AndroidUtils.init();
ChanUrls.loadScheme(ChanPreferences.getNetworkHttps()); ChanUrls.loadScheme(ChanPreferences.getNetworkHttps());
IconCache.createIcons(this); IconCache.createIcons(this);

@ -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;
}
}

@ -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();
}
}

@ -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<Controller> 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();
}
}
}

@ -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();
}
}

@ -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();
}
}

@ -29,9 +29,9 @@ import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.net.ChanReaderRequest; import org.floens.chan.core.net.ChanReaderRequest;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Time; import org.floens.chan.utils.Time;
import org.floens.chan.utils.Utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -40,13 +40,13 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class Loader { public class ChanLoader {
private static final String TAG = "Loader"; private static final String TAG = "Loader";
private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); 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 static final int[] watchTimeouts = {10, 15, 20, 30, 60, 90, 120, 180, 240, 300, 600, 1800, 3600};
private final List<LoaderListener> listeners = new ArrayList<>(); private final List<ChanLoaderCallback> listeners = new ArrayList<>();
private final Loadable loadable; private final Loadable loadable;
private final SparseArray<Post> postsById = new SparseArray<>(); private final SparseArray<Post> postsById = new SparseArray<>();
private ChanThread thread; private ChanThread thread;
@ -60,7 +60,7 @@ public class Loader {
private long lastLoadTime; private long lastLoadTime;
private ScheduledFuture<?> pendingFuture; private ScheduledFuture<?> pendingFuture;
public Loader(Loadable loadable) { public ChanLoader(Loadable loadable) {
this.loadable = loadable; this.loadable = loadable;
} }
@ -69,7 +69,7 @@ public class Loader {
* *
* @param l the listener to add * @param l the listener to add
*/ */
public void addListener(LoaderListener l) { public void addListener(ChanLoaderCallback l) {
listeners.add(l); listeners.add(l);
} }
@ -79,7 +79,7 @@ public class Loader {
* @param l the listener to remove * @param l the listener to remove
* @return true if there are no more listeners, false otherwise * @return true if there are no more listeners, false otherwise
*/ */
public boolean removeListener(LoaderListener l) { public boolean removeListener(ChanLoaderCallback l) {
listeners.remove(l); listeners.remove(l);
if (listeners.size() == 0) { if (listeners.size() == 0) {
clearTimer(); clearTimer();
@ -228,7 +228,7 @@ public class Loader {
Runnable pendingRunnable = new Runnable() { Runnable pendingRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
pendingFuture = null; pendingFuture = null;
@ -260,13 +260,13 @@ public class Loader {
new Response.Listener<List<Post>>() { new Response.Listener<List<Post>>() {
@Override @Override
public void onResponse(List<Post> list) { public void onResponse(List<Post> list) {
Loader.this.request = null; ChanLoader.this.request = null;
onData(list); onData(list);
} }
}, new Response.ErrorListener() { }, new Response.ErrorListener() {
@Override @Override
public void onErrorResponse(VolleyError error) { public void onErrorResponse(VolleyError error) {
Loader.this.request = null; ChanLoader.this.request = null;
onError(error); onError(error);
} }
} }
@ -329,8 +329,8 @@ public class Loader {
post.title = loadable.title; post.title = loadable.title;
} }
for (LoaderListener l : listeners) { for (ChanLoaderCallback l : listeners) {
l.onData(thread); l.onChanLoaderData(thread);
} }
lastLoadTime = Time.get(); lastLoadTime = Time.get();
@ -353,16 +353,16 @@ public class Loader {
error = new EndOfLineException(); error = new EndOfLineException();
} }
for (LoaderListener l : listeners) { for (ChanLoaderCallback l : listeners) {
l.onError(error); l.onChanLoaderError(error);
} }
clearTimer(); clearTimer();
} }
public static interface LoaderListener { public static interface ChanLoaderCallback {
public void onData(ChanThread result); public void onChanLoaderData(ChanThread result);
public void onError(VolleyError error); public void onChanLoaderError(VolleyError error);
} }
} }

@ -27,7 +27,7 @@ public class LoaderPool {
private static LoaderPool instance; private static LoaderPool instance;
private static Map<Loadable, Loader> loaders = new HashMap<>(); private static Map<Loadable, ChanLoader> loaders = new HashMap<>();
public static LoaderPool getInstance() { public static LoaderPool getInstance() {
if (instance == null) { if (instance == null) {
@ -37,33 +37,33 @@ public class LoaderPool {
return instance; return instance;
} }
public Loader obtain(Loadable loadable, Loader.LoaderListener listener) { public ChanLoader obtain(Loadable loadable, ChanLoader.ChanLoaderCallback listener) {
Loader loader = loaders.get(loadable); ChanLoader chanLoader = loaders.get(loadable);
if (loader == null) { if (chanLoader == null) {
loader = new Loader(loadable); chanLoader = new ChanLoader(loadable);
loaders.put(loadable, loader); loaders.put(loadable, chanLoader);
} }
loader.addListener(listener); chanLoader.addListener(listener);
return loader; return chanLoader;
} }
public void release(Loader loader, Loader.LoaderListener listener) { public void release(ChanLoader chanLoader, ChanLoader.ChanLoaderCallback listener) {
Loader foundLoader = null; ChanLoader foundChanLoader = null;
for (Loadable l : loaders.keySet()) { for (Loadable l : loaders.keySet()) {
if (loader.getLoadable().equals(l)) { if (chanLoader.getLoadable().equals(l)) {
foundLoader = loaders.get(l); foundChanLoader = loaders.get(l);
break; break;
} }
} }
if (foundLoader == null) { if (foundChanLoader == null) {
throw new RuntimeException("The released loader does not exist"); throw new RuntimeException("The released loader does not exist");
} }
if (loader.removeListener(listener)) { if (chanLoader.removeListener(listener)) {
loaders.remove(loader.getLoadable()); loaders.remove(chanLoader.getLoadable());
} }
} }
} }

@ -27,8 +27,8 @@ import org.floens.chan.core.model.Pass;
import org.floens.chan.core.model.Reply; import org.floens.chan.core.model.Reply;
import org.floens.chan.core.model.SavedReply; import org.floens.chan.core.model.SavedReply;
import org.floens.chan.ui.activity.ImagePickActivity; import org.floens.chan.ui.activity.ImagePickActivity;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Utils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
@ -559,7 +559,7 @@ public class ReplyManager {
try { try {
final CloseableHttpResponse response = client.execute(post); final CloseableHttpResponse response = client.execute(post);
final String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); final String responseString = EntityUtils.toString(response.getEntity(), "UTF-8");
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
listener.onResponse(responseString, client, response); listener.onResponse(responseString, client, response);
@ -567,7 +567,7 @@ public class ReplyManager {
}); });
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
listener.onResponse(null, client, null); listener.onResponse(null, client, null);

@ -41,7 +41,7 @@ import org.floens.chan.ChanApplication;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.ChanPreferences; 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.loader.LoaderPool;
import org.floens.chan.core.manager.ReplyManager.DeleteListener; import org.floens.chan.core.manager.ReplyManager.DeleteListener;
import org.floens.chan.core.manager.ReplyManager.DeleteResponse; 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.activity.ReplyActivity;
import org.floens.chan.ui.fragment.PostRepliesFragment; import org.floens.chan.ui.fragment.PostRepliesFragment;
import org.floens.chan.ui.fragment.ReplyFragment; import org.floens.chan.ui.fragment.ReplyFragment;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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 * All PostView's need to have this referenced. This manages some things like
* pages, starting and stopping of loading, handling linkables, replies popups * pages, starting and stopping of loading, handling linkables, replies popups
* etc. onDestroy, onStart and onStop must be called from the activity/fragment * 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 { public static enum ViewMode {
LIST, GRID LIST, GRID
} }
@ -80,7 +82,7 @@ public class ThreadManager implements Loader.LoaderListener {
private int lastPost = -1; private int lastPost = -1;
private String highlightedId = null; private String highlightedId = null;
private Loader loader; private ChanLoader chanLoader;
public ThreadManager(Activity activity, final ThreadManagerListener listener) { public ThreadManager(Activity activity, final ThreadManagerListener listener) {
this.activity = activity; this.activity = activity;
@ -92,36 +94,36 @@ public class ThreadManager implements Loader.LoaderListener {
} }
public void onStart() { public void onStart() {
if (loader != null) { if (chanLoader != null) {
if (isWatching()) { if (isWatching()) {
loader.setAutoLoadMore(true); chanLoader.setAutoLoadMore(true);
loader.requestMoreDataAndResetTimer(); chanLoader.requestMoreDataAndResetTimer();
} }
} }
} }
public void onStop() { public void onStop() {
if (loader != null) { if (chanLoader != null) {
loader.setAutoLoadMore(false); chanLoader.setAutoLoadMore(false);
} }
} }
public void bindLoader(Loadable loadable) { public void bindLoader(Loadable loadable) {
if (loader != null) { if (chanLoader != null) {
unbindLoader(); unbindLoader();
} }
loader = LoaderPool.getInstance().obtain(loadable, this); chanLoader = LoaderPool.getInstance().obtain(loadable, this);
if (isWatching()) { if (isWatching()) {
loader.setAutoLoadMore(true); chanLoader.setAutoLoadMore(true);
} }
} }
public void unbindLoader() { public void unbindLoader() {
if (loader != null) { if (chanLoader != null) {
loader.setAutoLoadMore(false); chanLoader.setAutoLoadMore(false);
LoaderPool.getInstance().release(loader, this); LoaderPool.getInstance().release(chanLoader, this);
loader = null; chanLoader = null;
} else { } else {
Logger.e(TAG, "Loader already unbinded"); Logger.e(TAG, "Loader already unbinded");
} }
@ -132,11 +134,11 @@ public class ThreadManager implements Loader.LoaderListener {
} }
public void bottomPostViewed() { public void bottomPostViewed() {
if (loader.getLoadable().isThreadMode() && loader.getThread() != null && loader.getThread().posts.size() > 0) { if (chanLoader.getLoadable().isThreadMode() && chanLoader.getThread() != null && chanLoader.getThread().posts.size() > 0) {
loader.getLoadable().lastViewed = loader.getThread().posts.get(loader.getThread().posts.size() - 1).no; 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) { if (pin != null) {
pin.onBottomPostViewed(); pin.onBottomPostViewed();
ChanApplication.getWatchManager().onPinsChanged(); ChanApplication.getWatchManager().onPinsChanged();
@ -144,11 +146,11 @@ public class ThreadManager implements Loader.LoaderListener {
} }
public boolean isWatching() { public boolean isWatching() {
if (!loader.getLoadable().isThreadMode()) { if (!chanLoader.getLoadable().isThreadMode()) {
return false; return false;
} else if (!ChanPreferences.getThreadAutoRefresh()) { } else if (!ChanPreferences.getThreadAutoRefresh()) {
return false; return false;
} else if (loader.getThread() != null && loader.getThread().closed) { } else if (chanLoader.getThread() != null && chanLoader.getThread().closed) {
return false; return false;
} else { } else {
return true; return true;
@ -156,8 +158,8 @@ public class ThreadManager implements Loader.LoaderListener {
} }
public void requestData() { public void requestData() {
if (loader != null) { if (chanLoader != null) {
loader.requestData(); chanLoader.requestData();
} else { } else {
Logger.e(TAG, "Loader null in requestData"); Logger.e(TAG, "Loader null in requestData");
} }
@ -167,22 +169,22 @@ public class ThreadManager implements Loader.LoaderListener {
* Called by postadapter and threadwatchcounterview.onclick * Called by postadapter and threadwatchcounterview.onclick
*/ */
public void requestNextData() { public void requestNextData() {
if (loader != null) { if (chanLoader != null) {
loader.requestMoreData(); chanLoader.requestMoreData();
} else { } else {
Logger.e(TAG, "Loader null in requestData"); Logger.e(TAG, "Loader null in requestData");
} }
} }
@Override @Override
public void onError(VolleyError error) { public void onChanLoaderError(VolleyError error) {
threadManagerListener.onThreadLoadError(error); threadManagerListener.onThreadLoadError(error);
} }
@Override @Override
public void onData(ChanThread thread) { public void onChanLoaderData(ChanThread thread) {
if (!isWatching()) { if (!isWatching()) {
loader.setAutoLoadMore(false); chanLoader.setAutoLoadMore(false);
} }
if (thread.posts.size() > 0) { if (thread.posts.size() > 0) {
@ -193,23 +195,23 @@ public class ThreadManager implements Loader.LoaderListener {
} }
public boolean hasLoader() { public boolean hasLoader() {
return loader != null; return chanLoader != null;
} }
public Post findPostById(int id) { public Post findPostById(int id) {
if (loader == null) if (chanLoader == null)
return null; return null;
return loader.findPostById(id); return chanLoader.findPostById(id);
} }
public Loadable getLoadable() { public Loadable getLoadable() {
if (loader == null) if (chanLoader == null)
return null; return null;
return loader.getLoadable(); return chanLoader.getLoadable();
} }
public Loader getLoader() { public ChanLoader getChanLoader() {
return loader; return chanLoader;
} }
public void onThumbnailClicked(Post post) { public void onThumbnailClicked(Post post) {
@ -217,7 +219,7 @@ public class ThreadManager implements Loader.LoaderListener {
} }
public void onPostClicked(Post post) { public void onPostClicked(Post post) {
if (loader != null) { if (chanLoader != null) {
threadManagerListener.onPostClicked(post); threadManagerListener.onPostClicked(post);
} }
} }
@ -225,11 +227,11 @@ public class ThreadManager implements Loader.LoaderListener {
public void showPostOptions(final Post post, PopupMenu popupMenu) { public void showPostOptions(final Post post, PopupMenu popupMenu) {
Menu menu = popupMenu.getMenu(); 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)); 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)); 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()); copyToClipboard(post.comment.toString());
break; break;
case 5: // Report case 5: // Report
Utils.openLink(activity, ChanUrls.getReportUrl(post.board, post.no)); AndroidUtils.openLink(ChanUrls.getReportUrl(post.board, post.no));
break; break;
case 6: // Id case 6: // Id
highlightedId = post.id; highlightedId = post.id;
@ -296,15 +298,15 @@ public class ThreadManager implements Loader.LoaderListener {
} }
public void openReply(boolean startInActivity) { public void openReply(boolean startInActivity) {
if (loader == null) if (chanLoader == null)
return; return;
if (startInActivity) { if (startInActivity) {
ReplyActivity.setLoadable(loader.getLoadable()); ReplyActivity.setLoadable(chanLoader.getLoadable());
Intent i = new Intent(activity, ReplyActivity.class); Intent i = new Intent(activity, ReplyActivity.class);
activity.startActivity(i); activity.startActivity(i);
} else { } else {
ReplyFragment reply = ReplyFragment.newInstance(loader.getLoadable(), true); ReplyFragment reply = ReplyFragment.newInstance(chanLoader.getLoadable(), true);
reply.show(activity.getFragmentManager(), "replyDialog"); reply.show(activity.getFragmentManager(), "replyDialog");
} }
} }
@ -326,7 +328,7 @@ public class ThreadManager implements Loader.LoaderListener {
} }
public boolean isPostLastSeen(Post post) { 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) { private void copyToClipboard(String comment) {
@ -341,7 +343,7 @@ public class ThreadManager implements Loader.LoaderListener {
if (post.hasImage) { if (post.hasImage) {
text += "File: " + post.filename + "." + post.ext + " \nDimensions: " + post.imageWidth + "x" 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; text += "Time: " + post.date;
@ -440,14 +442,14 @@ public class ThreadManager implements Loader.LoaderListener {
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
Utils.openLink(activity, (String) linkable.value); AndroidUtils.openLink((String) linkable.value);
} }
}) })
.setTitle(R.string.open_link_confirmation) .setTitle(R.string.open_link_confirmation)
.setMessage((String) linkable.value) .setMessage((String) linkable.value)
.show(); .show();
} else { } else {
Utils.openLink(activity, (String) linkable.value); AndroidUtils.openLink((String) linkable.value);
} }
} else if (linkable.type == PostLinkable.Type.THREAD) { } else if (linkable.type == PostLinkable.Type.THREAD) {
final PostLinkable.ThreadLink link = (PostLinkable.ThreadLink) linkable.value; final PostLinkable.ThreadLink link = (PostLinkable.ThreadLink) linkable.value;
@ -476,8 +478,8 @@ public class ThreadManager implements Loader.LoaderListener {
currentPopupFragment.dismissNoCallback(); currentPopupFragment.dismissNoCallback();
} }
PostRepliesFragment popup = PostRepliesFragment.newInstance(repliesPopup, this); // PostRepliesFragment popup = PostRepliesFragment.newInstance(repliesPopup, this);
PostRepliesFragment popup = null;
FragmentTransaction ft = activity.getFragmentManager().beginTransaction(); FragmentTransaction ft = activity.getFragmentManager().beginTransaction();
ft.add(popup, "postPopup"); ft.add(popup, "postPopup");
ft.commitAllowingStateLoss(); ft.commitAllowingStateLoss();
@ -492,8 +494,8 @@ public class ThreadManager implements Loader.LoaderListener {
popupQueue.remove(popupQueue.size() - 1); popupQueue.remove(popupQueue.size() - 1);
if (popupQueue.size() > 0) { 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(); FragmentTransaction ft = activity.getFragmentManager().beginTransaction();
ft.add(popup, "postPopup"); ft.add(popup, "postPopup");
ft.commit(); ft.commit();
@ -519,7 +521,7 @@ public class ThreadManager implements Loader.LoaderListener {
LinearLayout wrapper = new LinearLayout(activity); LinearLayout wrapper = new LinearLayout(activity);
wrapper.addView(checkBox); wrapper.addView(checkBox);
int padding = Utils.dp(8f); int padding = dp(8f);
wrapper.setPadding(padding, padding, padding, padding); wrapper.setPadding(padding, padding, padding, padding);
new AlertDialog.Builder(activity).setTitle(R.string.delete_confirm).setView(wrapper) new AlertDialog.Builder(activity).setTitle(R.string.delete_confirm).setView(wrapper)

@ -26,8 +26,8 @@ import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.ui.service.WatchNotifier; import org.floens.chan.ui.service.WatchNotifier;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -314,7 +314,7 @@ public class WatchManager implements ChanApplication.ForegroundChangedListener {
ScheduledFuture scheduledFuture = executor.schedule(new Runnable() { ScheduledFuture scheduledFuture = executor.schedule(new Runnable() {
@Override @Override
public void run() { public void run() {
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
timerFired(); timerFired();

@ -176,6 +176,14 @@ public class Loadable {
title = Post.generateTitle(post); title = Post.generateTitle(post);
} }
public void generateTitle() {
if (mode == Mode.CATALOG) {
title = "/" + board + "/";
} else {
title = "/" + board + "/" + no;
}
}
public static class Mode { public static class Mode {
public static final int INVALID = -1; public static final int INVALID = -1;
public static final int THREAD = 0; public static final int THREAD = 0;

@ -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<Post> 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<Post> 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<PostLinkable> linkables);
public void clipboardPost(Post post);
public void showThread(Loadable threadLoadable);
public void openLink(String link);
public void showPostsPopup(Post forPost, List<Post> posts);
}
}

@ -20,22 +20,22 @@ package org.floens.chan.core.watch;
import com.android.volley.VolleyError; import com.android.volley.VolleyError;
import org.floens.chan.ChanApplication; 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.loader.LoaderPool;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class PinWatcher implements Loader.LoaderListener { public class PinWatcher implements ChanLoader.ChanLoaderCallback {
private static final String TAG = "PinWatcher"; private static final String TAG = "PinWatcher";
private final Pin pin; private final Pin pin;
private Loader loader; private ChanLoader chanLoader;
private final List<Post> posts = new ArrayList<>(); private final List<Post> posts = new ArrayList<>();
private final List<Post> quotes = new ArrayList<>(); private final List<Post> quotes = new ArrayList<>();
@ -45,19 +45,19 @@ public class PinWatcher implements Loader.LoaderListener {
public PinWatcher(Pin pin) { public PinWatcher(Pin pin) {
this.pin = pin; this.pin = pin;
loader = LoaderPool.getInstance().obtain(pin.loadable, this); chanLoader = LoaderPool.getInstance().obtain(pin.loadable, this);
} }
public void destroy() { public void destroy() {
if (loader != null) { if (chanLoader != null) {
LoaderPool.getInstance().release(loader, this); LoaderPool.getInstance().release(chanLoader, this);
loader = null; chanLoader = null;
} }
} }
public void update() { public void update() {
if (!pin.isError) { if (!pin.isError) {
loader.loadMoreIfTime(); chanLoader.loadMoreIfTime();
} }
} }
@ -104,19 +104,19 @@ public class PinWatcher implements Loader.LoaderListener {
} }
public long getTimeUntilNextLoad() { public long getTimeUntilNextLoad() {
return loader.getTimeUntilLoadMore(); return chanLoader.getTimeUntilLoadMore();
} }
public boolean isLoading() { public boolean isLoading() {
return loader.isLoading(); return chanLoader.isLoading();
} }
@Override @Override
public void onError(VolleyError error) { public void onChanLoaderError(VolleyError error) {
Logger.e(TAG, "PinWatcher onError: ", error); Logger.e(TAG, "PinWatcher onError: ", error);
pin.isError = true; pin.isError = true;
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
ChanApplication.getWatchManager().onPinsChanged(); ChanApplication.getWatchManager().onPinsChanged();
@ -125,7 +125,7 @@ public class PinWatcher implements Loader.LoaderListener {
} }
@Override @Override
public void onData(ChanThread thread) { public void onChanLoaderData(ChanThread thread) {
pin.isError = false; pin.isError = false;
if (pin.thumbnailUrl == null && thread.op != null && thread.op.hasImage) { 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)); pin.watchLastCount, pin.watchNewCount, wereNewPosts, pin.quoteLastCount, pin.quoteNewCount, wereNewQuotes));
} }
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
ChanApplication.getWatchManager().onPinsChanged(); ChanApplication.getWatchManager().onPinsChanged();

@ -38,7 +38,7 @@ import java.util.List;
* because by default it handles touches for its list items... i.e. it's in * 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 * charge of drawing the pressed state (the list selector), handling list item
* clicks, etc. * clicks, etc.
* * <p/>
* <p> * <p>
* After creating the listener, the caller should also call * After creating the listener, the caller should also call
* {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)} * {@link ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}
@ -48,11 +48,11 @@ import java.util.List;
* {@link SwipeDismissListViewTouchListener} is paused during list view * {@link SwipeDismissListViewTouchListener} is paused during list view
* scrolling. * scrolling.
* </p> * </p>
* * <p/>
* <p> * <p>
* Example usage: * Example usage:
* </p> * </p>
* * <p/>
* <pre> * <pre>
* SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(listView, * SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(listView,
* new SwipeDismissListViewTouchListener.OnDismissCallback() { * new SwipeDismissListViewTouchListener.OnDismissCallback() {
@ -66,7 +66,7 @@ import java.util.List;
* listView.setOnTouchListener(touchListener); * listView.setOnTouchListener(touchListener);
* listView.setOnScrollListener(touchListener.makeScrollListener()); * listView.setOnScrollListener(touchListener.makeScrollListener());
* </pre> * </pre>
* * <p/>
* <p> * <p>
* This class Requires API level 12 or later due to use of * This class Requires API level 12 or later due to use of
* {@link android.view.ViewPropertyAnimator}. * {@link android.view.ViewPropertyAnimator}.

@ -54,8 +54,10 @@ import org.floens.chan.ui.SwipeDismissListViewTouchListener;
import org.floens.chan.ui.SwipeDismissListViewTouchListener.DismissCallbacks; import org.floens.chan.ui.SwipeDismissListViewTouchListener.DismissCallbacks;
import org.floens.chan.ui.ThemeActivity; import org.floens.chan.ui.ThemeActivity;
import org.floens.chan.ui.adapter.PinnedAdapter; import org.floens.chan.ui.adapter.PinnedAdapter;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.ThemeHelper; 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 abstract class BaseActivity extends ThemeActivity implements PanelSlideListener, WatchManager.PinListener {
public static boolean doRestartOnResume = false; public static boolean doRestartOnResume = false;
@ -141,7 +143,7 @@ public abstract class BaseActivity extends ThemeActivity implements PanelSlideLi
private void initPane() { private void initPane() {
threadPane.setPanelSlideListener(this); threadPane.setPanelSlideListener(this);
threadPane.setParallaxDistance(Utils.dp(100)); threadPane.setParallaxDistance(dp(100));
threadPane.setShadowResource(R.drawable.panel_shadow); threadPane.setShadowResource(R.drawable.panel_shadow);
TypedArray ta = obtainStyledAttributes(null, R.styleable.BoardPane, R.attr.board_pane_style, 0); 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(); }).setTitle(R.string.drawer_pinned_change_title).setView(text).create();
Utils.requestKeyboardFocus(titleDialog, text); AndroidUtils.requestKeyboardFocus(titleDialog, text);
titleDialog.show(); titleDialog.show();
} }

@ -18,10 +18,13 @@
package org.floens.chan.ui.activity; package org.floens.chan.ui.activity;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.res.Configuration;
import android.os.Bundle; 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. * 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 { public class BoardActivity extends Activity {
private static final String TAG = "StartActivity"; 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 @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -51,5 +85,5 @@ public class BoardActivity extends Activity {
Intent intent = new Intent(this, ChanActivity.class); Intent intent = new Intent(this, ChanActivity.class);
startActivity(intent); startActivity(intent);
finish(); finish();
} }*/
} }

@ -52,7 +52,7 @@ import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.ui.SwipeDismissListViewTouchListener; import org.floens.chan.ui.SwipeDismissListViewTouchListener;
import org.floens.chan.ui.ThemeActivity; 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.ArrayList;
import java.util.List; import java.util.List;
@ -261,7 +261,7 @@ public class BoardEditor extends ThemeActivity {
} }
}).setTitle(R.string.board_add).setView(text).create(); }).setTitle(R.string.board_add).setView(text).create();
Utils.requestKeyboardFocus(dialog, text); AndroidUtils.requestKeyboardFocus(dialog, text);
dialog.show(); dialog.show();
} }

@ -44,7 +44,7 @@ import org.floens.chan.ChanApplication;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.ChanPreferences; 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.BoardManager;
import org.floens.chan.core.manager.ThreadManager; import org.floens.chan.core.manager.ThreadManager;
import org.floens.chan.core.model.Board; 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.core.model.Post;
import org.floens.chan.ui.fragment.ThreadFragment; import org.floens.chan.ui.fragment.ThreadFragment;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Utils;
import java.util.List; import java.util.List;
import static org.floens.chan.utils.AndroidUtils.dp;
public class ChanActivity extends BaseActivity implements AdapterView.OnItemSelectedListener, BoardManager.BoardChangeListener { public class ChanActivity extends BaseActivity implements AdapterView.OnItemSelectedListener, BoardManager.BoardChangeListener {
private static final String TAG = "ChanActivity"; 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 // Nexus 10 is 800 x 1280 dp
if (ChanPreferences.getForcePhoneLayout()) { if (ChanPreferences.getForcePhoneLayout()) {
leftParams.width = width - Utils.dp(30); leftParams.width = width - dp(30);
rightParams.width = width; rightParams.width = width;
isSlidable = true; isSlidable = true;
} else { } else {
if (width < Utils.dp(400)) { if (width < dp(400)) {
leftParams.width = width - Utils.dp(30); leftParams.width = width - dp(30);
rightParams.width = width; rightParams.width = width;
isSlidable = true; isSlidable = true;
} else if (width < Utils.dp(800)) { } else if (width < dp(800)) {
leftParams.width = width - Utils.dp(60); leftParams.width = width - dp(60);
rightParams.width = width; rightParams.width = width;
isSlidable = true; isSlidable = true;
} else if (width < Utils.dp(1000)) { } else if (width < dp(1000)) {
leftParams.width = Utils.dp(300); leftParams.width = dp(300);
rightParams.width = width - Utils.dp(300); rightParams.width = width - dp(300);
isSlidable = false; isSlidable = false;
} else { } else {
leftParams.width = Utils.dp(400); leftParams.width = dp(400);
rightParams.width = width - Utils.dp(400); rightParams.width = width - dp(400);
isSlidable = false; isSlidable = false;
} }
} }
@ -335,10 +336,10 @@ public class ChanActivity extends BaseActivity implements AdapterView.OnItemSele
LayoutParams drawerParams = pinDrawerView.getLayoutParams(); LayoutParams drawerParams = pinDrawerView.getLayoutParams();
if (width < Utils.dp(340)) { if (width < dp(340)) {
drawerParams.width = Utils.dp(280); drawerParams.width = dp(280);
} else { } else {
drawerParams.width = Utils.dp(320); drawerParams.width = dp(320);
} }
pinDrawerView.setLayoutParams(drawerParams); pinDrawerView.setLayoutParams(drawerParams);
@ -485,13 +486,13 @@ public class ChanActivity extends BaseActivity implements AdapterView.OnItemSele
return true; return true;
case R.id.action_pin: case R.id.action_pin:
if (threadFragment.hasLoader()) { if (threadFragment.hasLoader()) {
Loader loader = threadFragment.getLoader(); ChanLoader chanLoader = threadFragment.getLoader();
if (loader != null && loader.getLoadable().isThreadMode() && loader.getThread() != null) { if (chanLoader != null && chanLoader.getLoadable().isThreadMode() && chanLoader.getThread() != null) {
Pin pin = ChanApplication.getWatchManager().findPinByLoadable(threadLoadable); Pin pin = ChanApplication.getWatchManager().findPinByLoadable(threadLoadable);
if (pin != null) { if (pin != null) {
ChanApplication.getWatchManager().removePin(pin); ChanApplication.getWatchManager().removePin(pin);
} else { } else {
ChanApplication.getWatchManager().addPin(loader.getLoadable(), loader.getThread().op); ChanApplication.getWatchManager().addPin(chanLoader.getLoadable(), chanLoader.getThread().op);
} }
updateActionBarState(); updateActionBarState();
} }

@ -29,8 +29,8 @@ import android.widget.Toast;
import org.floens.chan.ChanApplication; import org.floens.chan.ChanApplication;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils; import org.floens.chan.utils.IOUtils;
import org.floens.chan.utils.Utils;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -96,6 +96,8 @@ public class ImagePickActivity extends Activity {
FileInputStream is = new FileInputStream(fileDescriptor.getFileDescriptor()); FileInputStream is = new FileInputStream(fileDescriptor.getFileDescriptor());
FileOutputStream os = new FileOutputStream(cacheFile); FileOutputStream os = new FileOutputStream(cacheFile);
IOUtils.copy(is, os); IOUtils.copy(is, os);
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
@ -107,7 +109,7 @@ public class ImagePickActivity extends Activity {
} catch (IOException | SecurityException e) { } catch (IOException | SecurityException e) {
e.printStackTrace(); e.printStackTrace();
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
ChanApplication.getReplyManager()._onPickedFile("", null); ChanApplication.getReplyManager()._onPickedFile("", null);

@ -175,7 +175,7 @@ public class ImageViewActivity extends ThemeActivity implements ViewPager.OnPage
Post post = adapter.getPost(position); Post post = adapter.getPost(position);
if (!threadManager.arePostRepliesOpen()) { if (!threadManager.arePostRepliesOpen()) {
postAdapter.scrollToPost(post.no); // postAdapter.scrollToPost(post.no); //TODO
} }
} }

@ -42,7 +42,7 @@ import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.manager.ReplyManager.PassResponse; import org.floens.chan.core.manager.ReplyManager.PassResponse;
import org.floens.chan.core.model.Pass; import org.floens.chan.core.model.Pass;
import org.floens.chan.ui.ThemeActivity; 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 { public class PassSettingsActivity extends ThemeActivity implements OnCheckedChangeListener {
private SwitchCompat onSwitch; private SwitchCompat onSwitch;
@ -118,7 +118,7 @@ public class PassSettingsActivity extends ThemeActivity implements OnCheckedChan
link.setOnClickListener(new View.OnClickListener() { link.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { 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));
} }
}); });

@ -35,11 +35,12 @@ import org.floens.chan.R;
import org.floens.chan.core.ChanPreferences; import org.floens.chan.core.ChanPreferences;
import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Pin;
import org.floens.chan.ui.view.CustomNetworkImageView; import org.floens.chan.ui.view.CustomNetworkImageView;
import org.floens.chan.utils.Utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.floens.chan.utils.AndroidUtils.dp;
public class PinnedAdapter extends BaseAdapter { public class PinnedAdapter extends BaseAdapter {
private final static int VIEW_TYPE_ITEM = 0; private final static int VIEW_TYPE_ITEM = 0;
private final static int VIEW_TYPE_HEADER = 1; private final static int VIEW_TYPE_HEADER = 1;
@ -122,7 +123,7 @@ public class PinnedAdapter extends BaseAdapter {
if (pin.thumbnailUrl != null) { if (pin.thumbnailUrl != null) {
imageView.setVisibility(View.VISIBLE); imageView.setVisibility(View.VISIBLE);
imageView.setFadeIn(0); imageView.setFadeIn(0);
imageView.forceImageDimensions(Utils.dp(48), Utils.dp(48)); imageView.forceImageDimensions(dp(48), dp(48));
imageView.setImageUrl(pin.thumbnailUrl, ChanApplication.getVolleyImageLoader()); imageView.setImageUrl(pin.thumbnailUrl, ChanApplication.getVolleyImageLoader());
} else { } else {
imageView.setVisibility(View.GONE); imageView.setVisibility(View.GONE);

@ -18,13 +18,11 @@
package org.floens.chan.ui.adapter; package org.floens.chan.ui.adapter;
import android.content.Context; import android.content.Context;
import android.os.Handler;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter; import android.widget.BaseAdapter;
import android.widget.Filter; import android.widget.Filter;
import android.widget.Filterable; import android.widget.Filterable;
@ -33,19 +31,17 @@ import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.R; 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.ChanThread;
import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.ui.ScrollerRunnable;
import org.floens.chan.ui.view.PostView; import org.floens.chan.ui.view.PostView;
import org.floens.chan.utils.Utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import static org.floens.chan.utils.AndroidUtils.dp;
public class PostAdapter extends BaseAdapter implements Filterable { public class PostAdapter extends BaseAdapter implements Filterable {
private static final int VIEW_TYPE_ITEM = 0; private static final int VIEW_TYPE_ITEM = 0;
private static final int VIEW_TYPE_STATUS = 1; 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 Object lock = new Object();
private final Context context; private final Context context;
private final AbsListView listView;
private final ThreadManager threadManager; private final PostAdapterCallback postAdapterCallback;
private final PostAdapterListener listener; private final PostView.PostViewCallback postViewCallback;
/** /**
* The list with the original data * The list with the original data
@ -75,11 +70,10 @@ public class PostAdapter extends BaseAdapter implements Filterable {
private int pendingScrollToPost = -1; private int pendingScrollToPost = -1;
private String statusPrefix = ""; private String statusPrefix = "";
public PostAdapter(Context activity, ThreadManager threadManager, AbsListView listView, PostAdapterListener listener) { public PostAdapter(Context context, PostAdapterCallback postAdapterCallback, PostView.PostViewCallback postViewCallback) {
context = activity; this.postAdapterCallback = postAdapterCallback;
this.threadManager = threadManager; this.context = context;
this.listView = listView; this.postViewCallback = postViewCallback;
this.listener = listener;
} }
@Override @Override
@ -130,7 +124,7 @@ public class PostAdapter extends BaseAdapter implements Filterable {
} }
PostView postView = (PostView) convertView; PostView postView = (PostView) convertView;
postView.setPost(getItem(position), threadManager); postView.setPost(getItem(position), postViewCallback);
return postView; return postView;
} }
@ -185,16 +179,11 @@ public class PostAdapter extends BaseAdapter implements Filterable {
displayList.addAll((List<Post>) results.values); displayList.addAll((List<Post>) results.values);
} }
notifyDataSetChanged(); notifyDataSetChanged();
listener.onFilterResults(filter, ((List<Post>) results.values).size(), TextUtils.isEmpty(filter)); postAdapterCallback.onFilteredResults(filter, ((List<Post>) results.values).size(), TextUtils.isEmpty(filter));
if (pendingScrollToPost >= 0) { if (pendingScrollToPost >= 0) {
final int to = pendingScrollToPost; final int to = pendingScrollToPost;
pendingScrollToPost = -1; pendingScrollToPost = -1;
listView.post(new Runnable() { postAdapterCallback.scrollTo(to);
@Override
public void run() {
scrollToPost(to);
}
});
} }
} }
}; };
@ -239,6 +228,7 @@ public class PostAdapter extends BaseAdapter implements Filterable {
notifyDataSetChanged(); notifyDataSetChanged();
} }
/* TODO
public void scrollToPost(int no) { public void scrollToPost(int no) {
if (isFiltering()) { if (isFiltering()) {
pendingScrollToPost = no; pendingScrollToPost = no;
@ -260,7 +250,7 @@ public class PostAdapter extends BaseAdapter implements Filterable {
} }
} }
} }
} }*/
public void setStatusMessage(String loadMessage) { public void setStatusMessage(String loadMessage) {
this.statusMessage = loadMessage; this.statusMessage = loadMessage;
@ -271,20 +261,20 @@ public class PostAdapter extends BaseAdapter implements Filterable {
} }
private void onGetBottomView() { private void onGetBottomView() {
if (threadManager.getLoadable().isBoardMode() && !endOfLine) { /*if (postAdapterCallback.getLoadable().isBoardMode() && !endOfLine) {
// Try to load more posts // Try to load more posts
threadManager.requestNextData(); threadManager.requestNextData();
} }*/
if (lastPostCount != sourceList.size()) { if (lastPostCount != sourceList.size()) {
lastPostCount = sourceList.size(); lastPostCount = sourceList.size();
threadManager.bottomPostViewed(); postAdapterCallback.onListScrolledToBottom();
notifyDataSetChanged(); notifyDataSetChanged();
} }
} }
private boolean showStatusView() { private boolean showStatusView() {
Loadable l = threadManager.getLoadable(); Loadable l = postAdapterCallback.getLoadable();
if (l != null) { if (l != null) {
return l.isBoardMode() || l.isThreadMode(); return l.isBoardMode() || l.isThreadMode();
} else { } else {
@ -296,8 +286,16 @@ public class PostAdapter extends BaseAdapter implements Filterable {
return !TextUtils.isEmpty(filter); return !TextUtils.isEmpty(filter);
} }
public interface PostAdapterListener { public interface PostAdapterCallback {
public void onFilterResults(String filter, int count, boolean all); 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 { public class StatusView extends LinearLayout {
@ -319,20 +317,22 @@ public class PostAdapter extends BaseAdapter implements Filterable {
} }
public void init() { public void init() {
Loader loader = threadManager.getLoader(); // TODO
if (loader == null) /*
ChanLoader chanLoader = threadManager.getChanLoader();
if (chanLoader == null)
return; return;
setGravity(Gravity.CENTER); setGravity(Gravity.CENTER);
Loadable loadable = loader.getLoadable(); Loadable loadable = chanLoader.getLoadable();
if (loadable.isThreadMode()) { if (loadable.isThreadMode()) {
String error = getStatusMessage(); String error = getStatusMessage();
if (error != null) { if (error != null) {
setText(error); setText(error);
} else { } else {
if (threadManager.isWatching()) { if (threadManager.isWatching()) {
long time = loader.getTimeUntilLoadMore() / 1000L; long time = chanLoader.getTimeUntilLoadMore() / 1000L;
if (time == 0) { if (time == 0) {
setText(statusPrefix + context.getString(R.string.thread_refresh_now)); setText(statusPrefix + context.getString(R.string.thread_refresh_now));
} else { } else {
@ -348,7 +348,7 @@ public class PostAdapter extends BaseAdapter implements Filterable {
} }
}, 1000); }, 1000);
} else { } else {
if (loader.getTimeUntilLoadMore() == 0) { if (chanLoader.getTimeUntilLoadMore() == 0) {
setText(statusPrefix + context.getString(R.string.thread_refresh_now)); setText(statusPrefix + context.getString(R.string.thread_refresh_now));
} else { } else {
setText(statusPrefix + context.getString(R.string.thread_refresh_bar_inactive)); setText(statusPrefix + context.getString(R.string.thread_refresh_bar_inactive));
@ -358,9 +358,9 @@ public class PostAdapter extends BaseAdapter implements Filterable {
setOnClickListener(new OnClickListener() { setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
Loader loader = threadManager.getLoader(); ChanLoader chanLoader = threadManager.getChanLoader();
if (loader != null) { if (chanLoader != null) {
loader.requestMoreDataAndResetTimer(); chanLoader.requestMoreDataAndResetTimer();
setText(context.getString(R.string.thread_refresh_now)); setText(context.getString(R.string.thread_refresh_now));
} }
@ -376,7 +376,7 @@ public class PostAdapter extends BaseAdapter implements Filterable {
} else { } else {
setProgressBar(); setProgressBar();
} }
} }*/
} }
@Override @Override
@ -389,7 +389,7 @@ public class PostAdapter extends BaseAdapter implements Filterable {
TextView text = new TextView(context); TextView text = new TextView(context);
text.setText(string); text.setText(string);
text.setGravity(Gravity.CENTER); 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() { private void setProgressBar() {

@ -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<ToolbarMenuSubItem> 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);
}
}

@ -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);
}
}
}

@ -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);
}
}

@ -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();
}
}

@ -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;
}
}

@ -42,11 +42,13 @@ import org.floens.chan.ui.activity.ImageViewActivity;
import org.floens.chan.ui.adapter.ImageViewAdapter; import org.floens.chan.ui.adapter.ImageViewAdapter;
import org.floens.chan.ui.view.ThumbnailImageView; import org.floens.chan.ui.view.ThumbnailImageView;
import org.floens.chan.ui.view.ThumbnailImageView.ThumbnailImageViewCallback; 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.ImageSaver;
import org.floens.chan.utils.Utils;
import java.io.File; import java.io.File;
import static org.floens.chan.utils.AndroidUtils.dp;
public class ImageViewFragment extends Fragment implements ThumbnailImageViewCallback { public class ImageViewFragment extends Fragment implements ThumbnailImageViewCallback {
private Context context; private Context context;
private ImageViewActivity activity; private ImageViewActivity activity;
@ -238,7 +240,7 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal
activity.invalidateActionBar(); activity.invalidateActionBar();
break; break;
case R.id.action_open_browser: case R.id.action_open_browser:
Utils.openLink(context, post.imageUrl); AndroidUtils.openLink(post.imageUrl);
break; break;
case R.id.action_image_save: case R.id.action_image_save:
case R.id.action_share: case R.id.action_share:
@ -254,7 +256,7 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal
// Search if it was an ImageSearch item // Search if it was an ImageSearch item
for (ImageSearch engine : ImageSearch.engines) { for (ImageSearch engine : ImageSearch.engines) {
if (item.getItemId() == engine.getId()) { if (item.getItemId() == engine.getId()) {
Utils.openLink(context, engine.getUrl(post.imageUrl)); AndroidUtils.openLink(engine.getUrl(post.imageUrl));
break; break;
} }
} }
@ -285,13 +287,13 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal
TextView noticeText = new TextView(context); TextView noticeText = new TextView(context);
noticeText.setText(R.string.video_playback_warning); noticeText.setText(R.string.video_playback_warning);
noticeText.setTextSize(16f); noticeText.setTextSize(16f);
notice.addView(noticeText, Utils.MATCH_WRAP_PARAMS); notice.addView(noticeText, AndroidUtils.MATCH_WRAP_PARAMS);
final CheckBox dontShowAgain = new CheckBox(context); final CheckBox dontShowAgain = new CheckBox(context);
dontShowAgain.setText(R.string.video_playback_ignore); 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); notice.setPadding(padding, padding, padding, padding);
new AlertDialog.Builder(context) new AlertDialog.Builder(context)

@ -31,8 +31,9 @@ import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.ChanPreferences; 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.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.ui.view.PostView;
import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.ThemeHelper;
@ -44,20 +45,21 @@ public class PostRepliesFragment extends DialogFragment {
private ListView listView; private ListView listView;
private Activity activity; private Activity activity;
private ThreadManager.RepliesPopup repliesPopup; private PostPopupHelper.RepliesData repliesData;
private ThreadManager manager; private PostPopupHelper postPopupHelper;
private boolean callback = true; 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(); PostRepliesFragment fragment = new PostRepliesFragment();
fragment.repliesPopup = repliesPopup; fragment.repliesData = repliesData;
fragment.manager = manager; fragment.postPopupHelper = postPopupHelper;
fragment.presenter = presenter;
return fragment; return fragment;
} }
public void dismissNoCallback() { public void dismissNoCallback() {
callback = false; postPopupHelper = null;
dismiss(); dismiss();
} }
@ -72,8 +74,8 @@ public class PostRepliesFragment extends DialogFragment {
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog); super.onDismiss(dialog);
if (callback && manager != null) { if (postPopupHelper != null) {
manager.onPostRepliesPop(); postPopupHelper.onPostRepliesPop();
} }
} }
@ -98,8 +100,9 @@ public class PostRepliesFragment extends DialogFragment {
container.findViewById(R.id.replies_close).setOnClickListener(new View.OnClickListener() { container.findViewById(R.id.replies_close).setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
manager.closeAllPostFragments(); if (postPopupHelper != null) {
dismiss(); postPopupHelper.closeAllPostFragments();
}
} }
}); });
@ -117,7 +120,7 @@ public class PostRepliesFragment extends DialogFragment {
activity = getActivity(); activity = getActivity();
if (repliesPopup == null) { if (repliesData == null) {
// Restoring from background. // Restoring from background.
dismiss(); dismiss();
} else { } else {
@ -133,15 +136,15 @@ public class PostRepliesFragment extends DialogFragment {
final Post p = getItem(position); final Post p = getItem(position);
postView.setPost(p, manager); postView.setPost(p, presenter);
postView.setHighlightQuotesWithNo(repliesPopup.forNo); postView.setHighlightQuotesWithNo(repliesData.forPost.no);
postView.setOnClickListeners(new View.OnClickListener() { postView.setOnClickListeners(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
manager.closeAllPostFragments(); if (postPopupHelper != null) {
postPopupHelper.postClicked(p);
}
dismiss(); 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.setAdapter(adapter);
listView.setSelectionFromTop(repliesPopup.listViewIndex, repliesPopup.listViewTop); listView.setSelectionFromTop(repliesData.listViewIndex, repliesData.listViewTop);
listView.setOnScrollListener(new AbsListView.OnScrollListener() { listView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override @Override
public void onScrollStateChanged(AbsListView view, int scrollState) { public void onScrollStateChanged(AbsListView view, int scrollState) {
@ -160,10 +163,10 @@ public class PostRepliesFragment extends DialogFragment {
@Override @Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (repliesPopup != null) { if (repliesData != null) {
repliesPopup.listViewIndex = view.getFirstVisiblePosition(); repliesData.listViewIndex = view.getFirstVisiblePosition();
View v = view.getChildAt(0); View v = view.getChildAt(0);
repliesPopup.listViewTop = (v == null) ? 0 : v.getTop(); repliesData.listViewTop = (v == null) ? 0 : v.getTop();
} }
} }
}); });

@ -61,13 +61,15 @@ import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Reply; import org.floens.chan.core.model.Reply;
import org.floens.chan.ui.ViewFlipperAnimations; import org.floens.chan.ui.ViewFlipperAnimations;
import org.floens.chan.ui.view.LoadView; import org.floens.chan.ui.view.LoadView;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.ImageDecoder; import org.floens.chan.utils.ImageDecoder;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.ThemeHelper;
import org.floens.chan.utils.Utils;
import java.io.File; import java.io.File;
import static org.floens.chan.utils.AndroidUtils.dp;
public class ReplyFragment extends DialogFragment { public class ReplyFragment extends DialogFragment {
private static final String TAG = "ReplyFragment"; private static final String TAG = "ReplyFragment";
@ -458,8 +460,8 @@ public class ReplyFragment extends DialogFragment {
boolean probablyWebm = name.endsWith(".webm"); boolean probablyWebm = name.endsWith(".webm");
int maxSize = probablyWebm ? b.maxWebmSize : b.maxFileSize; int maxSize = probablyWebm ? b.maxWebmSize : b.maxFileSize;
if (file.length() > maxSize) { if (file.length() > maxSize) {
String fileSize = Utils.getReadableFileSize((int) file.length(), false); String fileSize = AndroidUtils.getReadableFileSize((int) file.length(), false);
String maxSizeString = Utils.getReadableFileSize(maxSize, 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); String text = getString(probablyWebm ? R.string.reply_webm_too_big : R.string.reply_file_too_big, fileSize, maxSizeString);
fileStatusView.setVisibility(View.VISIBLE); fileStatusView.setVisibility(View.VISIBLE);
fileStatusView.setText(text); fileStatusView.setText(text);
@ -510,11 +512,11 @@ public class ReplyFragment extends DialogFragment {
private void noPreview(LoadView loadView) { private void noPreview(LoadView loadView) {
TextView text = new TextView(context); TextView text = new TextView(context);
text.setLayoutParams(Utils.MATCH_WRAP_PARAMS); text.setLayoutParams(AndroidUtils.MATCH_WRAP_PARAMS);
text.setGravity(Gravity.CENTER); text.setGravity(Gravity.CENTER);
text.setText(R.string.reply_no_preview); text.setText(R.string.reply_no_preview);
text.setTextSize(16f); text.setTextSize(16f);
int padding = Utils.dp(16); int padding = dp(16);
text.setPadding(padding, padding, padding, padding); text.setPadding(padding, padding, padding, padding);
loadView.setView(text); loadView.setView(text);
} }

@ -45,8 +45,8 @@ import com.android.volley.VolleyError;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.ChanPreferences; 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.EndOfLineException;
import org.floens.chan.core.loader.Loader;
import org.floens.chan.core.manager.ThreadManager; import org.floens.chan.core.manager.ThreadManager;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Loadable; 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.activity.ImageViewActivity;
import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostAdapter;
import org.floens.chan.ui.view.LoadView; import org.floens.chan.ui.view.LoadView;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.ImageSaver; import org.floens.chan.utils.ImageSaver;
import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.ThemeHelper;
import org.floens.chan.utils.Utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.net.ssl.SSLException; 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 ThreadManager threadManager;
private Loadable loadable; private Loadable loadable;
@ -125,8 +128,8 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
this.viewMode = viewMode; this.viewMode = viewMode;
} }
public Loader getLoader() { public ChanLoader getLoader() {
return threadManager.getLoader(); return threadManager.getChanLoader();
} }
public void startFiltering() { public void startFiltering() {
@ -193,7 +196,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
((BaseActivity) getActivity()).onOPClicked(post); ((BaseActivity) getActivity()).onOPClicked(post);
} else if (loadable.isThreadMode() && isFiltering) { } else if (loadable.isThreadMode() && isFiltering) {
filterView.clearSearch(); filterView.clearSearch();
postAdapter.scrollToPost(post.no); // postAdapter.scrollToPost(post.no);
} }
} }
@ -240,7 +243,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
@Override @Override
public void onScrollTo(int post) { public void onScrollTo(int post) {
if (postAdapter != null) { if (postAdapter != null) {
postAdapter.scrollToPost(post); // postAdapter.scrollToPost(post);
} }
} }
@ -275,7 +278,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
if (highlightedPost >= 0) { if (highlightedPost >= 0) {
threadManager.highlightPost(highlightedPost); threadManager.highlightPost(highlightedPost);
postAdapter.scrollToPost(highlightedPost); // postAdapter.scrollToPost(highlightedPost);
highlightedPost = -1; highlightedPost = -1;
} }
@ -299,7 +302,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
highlightedPost = -1; highlightedPost = -1;
} }
public void onFilterResults(String filter, int count, boolean all) { public void onFilteredResults(String filter, int count, boolean all) {
isFiltering = !all; isFiltering = !all;
if (filterView != null) { 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() { private RelativeLayout createView() {
RelativeLayout compound = new RelativeLayout(getActivity()); RelativeLayout compound = new RelativeLayout(getActivity());
@ -315,12 +338,12 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
filterView = new FilterView(getActivity()); filterView = new FilterView(getActivity());
filterView.setVisibility(View.GONE); filterView.setVisibility(View.GONE);
listViewContainer.addView(filterView, Utils.MATCH_WRAP_PARAMS); listViewContainer.addView(filterView, AndroidUtils.MATCH_WRAP_PARAMS);
if (viewMode == ThreadManager.ViewMode.LIST) { if (viewMode == ThreadManager.ViewMode.LIST) {
ListView list = new ListView(getActivity()); ListView list = new ListView(getActivity());
listView = list; listView = list;
postAdapter = new PostAdapter(getActivity(), threadManager, listView, this); // postAdapter = new PostAdapter(getActivity(), threadManager, listView, this);
listView.setAdapter(postAdapter); listView.setAdapter(postAdapter);
list.setSelectionFromTop(loadable.listViewIndex, loadable.listViewTop); list.setSelectionFromTop(loadable.listViewIndex, loadable.listViewTop);
} else if (viewMode == ThreadManager.ViewMode.GRID) { } 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); int postGridWidth = getActivity().getResources().getDimensionPixelSize(R.dimen.post_grid_width);
grid.setColumnWidth(postGridWidth); grid.setColumnWidth(postGridWidth);
listView = grid; listView = grid;
postAdapter = new PostAdapter(getActivity(), threadManager, listView, this); // postAdapter = new PostAdapter(getActivity(), threadManager, listView, this);
listView.setAdapter(postAdapter); listView.setAdapter(postAdapter);
listView.setSelection(loadable.listViewIndex); 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()) { if (loadable.isThreadMode()) {
skip = new ImageView(getActivity()); skip = new ImageView(getActivity());
skip.setImageResource(R.drawable.skip_arrow_down); skip.setImageResource(R.drawable.skip_arrow_down);
skip.setVisibility(View.GONE); skip.setVisibility(View.GONE);
compound.addView(skip, Utils.WRAP_PARAMS); compound.addView(skip, AndroidUtils.WRAP_PARAMS);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) skip.getLayoutParams(); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) skip.getLayoutParams();
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); 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); skip.setLayoutParams(params);
skipLogic = new SkipLogic(skip, listView); skipLogic = new SkipLogic(skip, listView);
@ -418,19 +441,19 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
String errorMessage = getLoadErrorText(error); String errorMessage = getLoadErrorText(error);
LinearLayout wrapper = new LinearLayout(getActivity()); LinearLayout wrapper = new LinearLayout(getActivity());
wrapper.setLayoutParams(Utils.MATCH_PARAMS); wrapper.setLayoutParams(AndroidUtils.MATCH_PARAMS);
wrapper.setGravity(Gravity.CENTER); wrapper.setGravity(Gravity.CENTER);
wrapper.setOrientation(LinearLayout.VERTICAL); wrapper.setOrientation(LinearLayout.VERTICAL);
TextView text = new TextView(getActivity()); TextView text = new TextView(getActivity());
text.setLayoutParams(Utils.WRAP_PARAMS); text.setLayoutParams(AndroidUtils.WRAP_PARAMS);
text.setText(errorMessage); text.setText(errorMessage);
text.setTextSize(24f); text.setTextSize(24f);
wrapper.addView(text); wrapper.addView(text);
Button retry = new Button(getActivity()); Button retry = new Button(getActivity());
retry.setText(R.string.thread_load_failed_retry); retry.setText(R.string.thread_load_failed_retry);
retry.setLayoutParams(Utils.WRAP_PARAMS); retry.setLayoutParams(AndroidUtils.WRAP_PARAMS);
retry.setGravity(Gravity.CENTER); retry.setGravity(Gravity.CENTER);
retry.setOnClickListener(new View.OnClickListener() { retry.setOnClickListener(new View.OnClickListener() {
@Override @Override
@ -444,7 +467,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
wrapper.addView(retry); wrapper.addView(retry);
LinearLayout.LayoutParams retryParams = (LinearLayout.LayoutParams) retry.getLayoutParams(); LinearLayout.LayoutParams retryParams = (LinearLayout.LayoutParams) retry.getLayoutParams();
retryParams.topMargin = Utils.dp(12); retryParams.topMargin = dp(12);
retry.setLayoutParams(retryParams); retry.setLayoutParams(retryParams);
return wrapper; return wrapper;
@ -586,11 +609,11 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
searchViewContainer.addView(closeButton); searchViewContainer.addView(closeButton);
closeButton.setImageResource(ThemeHelper.getInstance().getTheme().isLightTheme ? R.drawable.ic_action_cancel : R.drawable.ic_action_cancel_dark); closeButton.setImageResource(ThemeHelper.getInstance().getTheme().isLightTheme ? R.drawable.ic_action_cancel : R.drawable.ic_action_cancel_dark);
LinearLayout.LayoutParams closeButtonParams = (LinearLayout.LayoutParams) closeButton.getLayoutParams(); LinearLayout.LayoutParams closeButtonParams = (LinearLayout.LayoutParams) closeButton.getLayoutParams();
searchViewParams.width = Utils.dp(48); searchViewParams.width = dp(48);
searchViewParams.height = LayoutParams.MATCH_PARENT; searchViewParams.height = LayoutParams.MATCH_PARENT;
closeButton.setLayoutParams(closeButtonParams); closeButton.setLayoutParams(closeButtonParams);
Utils.setPressedDrawable(closeButton); setPressedDrawable(closeButton);
int padding = Utils.dp(8); int padding = dp(8);
closeButton.setPadding(padding, padding, padding, padding); closeButton.setPadding(padding, padding, padding, padding);
closeButton.setOnClickListener(new OnClickListener() { 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.setQueryHint(getString(R.string.search_hint));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@ -619,7 +642,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
textView = new TextView(getContext()); textView = new TextView(getContext());
textView.setGravity(Gravity.CENTER); 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) { private void setText(String filter, int count, boolean all) {

@ -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<RepliesData> dataQueue = new ArrayList<>();
private PostRepliesFragment currentPopupFragment;
public PostPopupHelper(Context context, ThreadPresenter presenter) {
this.context = context;
this.presenter = presenter;
}
public void showPosts(Post forPost, List<Post> 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<Post> posts;
public Post forPost;
public int listViewIndex;
public int listViewTop;
public RepliesData(Post forPost, List<Post> posts) {
this.forPost = forPost;
this.posts = posts;
}
}
}

@ -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();
}
}

@ -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<PostLinkable> 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<Post> 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);
}
}

@ -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) {
}
}

@ -34,7 +34,7 @@ import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.watch.PinWatcher; import org.floens.chan.core.watch.PinWatcher;
import org.floens.chan.ui.activity.ChanActivity; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -171,7 +171,7 @@ public class WatchNotifier extends Service {
Collections.sort(notificationList, POST_AGE_COMPARER); Collections.sort(notificationList, POST_AGE_COMPARER);
List<CharSequence> lines = new ArrayList<>(); List<CharSequence> lines = new ArrayList<>();
for (Post post : notificationList) { for (Post post : notificationList) {
CharSequence prefix = Utils.ellipsize(post.title, 18); CharSequence prefix = AndroidUtils.ellipsize(post.title, 18);
CharSequence comment; CharSequence comment;
if (post.comment.length() == 0) { if (post.comment.length() == 0) {

@ -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;
}

@ -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<Animator> 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);
}
}

@ -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<ToolbarMenuItem> 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<ToolbarMenuItemSubMenu.SubItem> 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;
}
}

@ -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);
}
}

@ -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;
}
}

@ -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<ToolbarMenuSubItem> items;
private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;
private ToolbarMenuItemSubMenuCallback callback;
public ToolbarMenuSubMenu(Context context, View anchor, List<ToolbarMenuSubItem> 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<String> 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<String> {
public SubMenuArrayAdapter(Context context, int resource, List<String> 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;
}
}
}

@ -20,7 +20,7 @@ import android.util.AttributeSet;
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
import org.floens.chan.utils.Utils; import org.floens.chan.utils.AndroidUtils;
public class CustomScaleImageView extends SubsamplingScaleImageView { public class CustomScaleImageView extends SubsamplingScaleImageView {
private InitedCallback initCallback; private InitedCallback initCallback;
@ -41,7 +41,7 @@ public class CustomScaleImageView extends SubsamplingScaleImageView {
protected void onImageReady() { protected void onImageReady() {
super.onImageReady(); super.onImageReady();
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (initCallback != null) { if (initCallback != null) {
@ -55,7 +55,7 @@ public class CustomScaleImageView extends SubsamplingScaleImageView {
protected void onOutOfMemory() { protected void onOutOfMemory() {
super.onOutOfMemory(); super.onOutOfMemory();
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (initCallback != null) { if (initCallback != null) {

@ -25,11 +25,11 @@ import android.view.MotionEvent;
/** /**
* Hacky fix for Issue #4 and * Hacky fix for Issue #4 and
* http://code.google.com/p/android/issues/detail?id=18990 * http://code.google.com/p/android/issues/detail?id=18990
* * <p/>
* ScaleGestureDetector seems to mess up the touch events, which means that * ScaleGestureDetector seems to mess up the touch events, which means that
* ViewGroups which make use of onInterceptTouchEvent throw a lot of * ViewGroups which make use of onInterceptTouchEvent throw a lot of
* IllegalArgumentException: pointerIndex out of range. * IllegalArgumentException: pointerIndex out of range.
* * <p/>
* There's not much I can do in my code for now, but we can mask the result by * 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. * just catching the problem and ignoring it.
* *

@ -18,22 +18,27 @@
package org.floens.chan.ui.view; package org.floens.chan.ui.view;
import android.animation.Animator; import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar; 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 * Container for a view with an ProgressBar. Toggles between the view and a
* ProgressBar. * ProgressBar.
*/ */
public class LoadView extends FrameLayout { public class LoadView extends FrameLayout {
public int fadeDuration = 100; private int fadeDuration = 200;
private Map<View, AnimatorSet> animatorsIn = new HashMap<>();
private Map<View, AnimatorSet> animatorsOut = new HashMap<>();
public LoadView(Context context) { public LoadView(Context context) {
super(context); super(context);
@ -54,6 +59,10 @@ public class LoadView extends FrameLayout {
setView(null, false); 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 * 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. * new one. Set view to null to show the progressbar.
@ -64,40 +73,86 @@ public class LoadView extends FrameLayout {
setView(view, true); setView(view, true);
} }
public void setView(View view, boolean animation) { public void setView(View newView, boolean animate) {
if (view == null) { // Passing null means showing a progressbar
LinearLayout layout = new LinearLayout(getContext()); if (newView == null) {
layout.setGravity(Gravity.CENTER); 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()); // Readded while still running a add/remove animation for the new view
layout.addView(pb); // This also removes the new view from this view
view = layout; AnimatorSet out = animatorsOut.remove(newView);
if (out != null) {
out.cancel();
} }
while (getChildCount() > 1) { AnimatorSet in = animatorsIn.remove(newView);
removeViewAt(0); if (in != null) {
in.cancel();
} }
View currentView = getChildAt(0); // Add fade out animations for all remaining view
if (currentView != null) { for (int i = 0; i < getChildCount(); i++) {
if (animation) { View child = getChildAt(i);
final View tempView = currentView; if (child != null) {
currentView.animate().setDuration(fadeDuration).alpha(0).setListener(new SimpleAnimatorListener() { AnimatorSet inSet = animatorsIn.remove(child);
@Override if (inSet != null) {
public void onAnimationEnd(Animator animation) { inSet.cancel();
removeView(tempView); }
}
}); if (!animatorsOut.containsKey(child)) {
} else { animateViewOut(child);
removeView(currentView); }
} }
} }
addView(view); addView(newView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
if (animation) { if (animate) {
view.setAlpha(0f); // Fade view in
view.animate().setDuration(fadeDuration).alpha(1f); 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();
}
} }

@ -31,6 +31,8 @@ import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
@ -43,13 +45,14 @@ import com.android.volley.toolbox.NetworkImageView;
import org.floens.chan.ChanApplication; import org.floens.chan.ChanApplication;
import org.floens.chan.R; 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.Post;
import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.utils.IconCache; import org.floens.chan.utils.IconCache;
import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.ThemeHelper;
import org.floens.chan.utils.Time; 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 { public class PostView extends LinearLayout implements View.OnClickListener {
private final static LinearLayout.LayoutParams matchParams = new LinearLayout.LayoutParams( 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 final Activity context;
private ThreadManager manager;
private Post post; private Post post;
private PostViewCallback callback;
private Loadable loadable;
private int highlightQuotesNo = -1; private int highlightQuotesNo = -1;
private boolean isBuild = false; 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.post = post;
this.manager = manager; this.callback = callback;
this.loadable = callback.getLoadable();
highlightQuotesNo = -1; 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); 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); commentView.setText(post.comment);
if (manager.getLoadable().isThreadMode()) { if (loadable.isThreadMode()) {
post.setLinkableListener(this); post.setLinkableListener(this);
commentView.setMovementMethod(new PostViewMovementMethod()); commentView.setMovementMethod(new PostViewMovementMethod());
commentView.setOnClickListener(this); 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() { repliesCountView.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { 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) { if (post.isSavedReply) {
full.setBackgroundColor(savedReplyColor); full.setBackgroundColor(savedReplyColor);
} else if (manager.isPostHightlighted(post)) { } else if (callback.isPostHightlighted(post)) {
full.setBackgroundColor(highlightedColor); full.setBackgroundColor(highlightedColor);
} else { } else {
full.setBackgroundColor(0x00000000); full.setBackgroundColor(0x00000000);
} }
if (manager.isPostLastSeen(post)) { if (callback.isPostLastSeen(post)) {
lastSeen.setVisibility(View.VISIBLE); lastSeen.setVisibility(View.VISIBLE);
} else { } else {
lastSeen.setVisibility(View.GONE); lastSeen.setVisibility(View.GONE);
@ -318,7 +323,7 @@ public class PostView extends LinearLayout implements View.OnClickListener {
imageView.setOnClickListener(new View.OnClickListener() { imageView.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { 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()) { if (isList()) {
commentView.setPadding(postPadding, commentPadding, postPadding, commentPadding); commentView.setPadding(postPadding, commentPadding, postPadding, commentPadding);
if (manager.getLoadable().isBoardMode() || manager.getLoadable().isCatalogMode()) { if (loadable.isBoardMode() || loadable.isCatalogMode()) {
commentView.setMaxHeight(postListMaxHeight); commentView.setMaxHeight(postListMaxHeight);
} }
} else if (isGrid()) { } else if (isGrid()) {
@ -398,7 +403,7 @@ public class PostView extends LinearLayout implements View.OnClickListener {
} }
repliesCountView = new TextView(context); repliesCountView = new TextView(context);
Utils.setPressedDrawable(repliesCountView); setPressedDrawable(repliesCountView);
repliesCountView.setTextColor(replyCountColor); repliesCountView.setTextColor(replyCountColor);
repliesCountView.setPadding(postPadding, postPadding, postPadding, postPadding); repliesCountView.setPadding(postPadding, postPadding, postPadding, postPadding);
repliesCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, repliesCountSize); repliesCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, repliesCountSize);
@ -410,21 +415,28 @@ public class PostView extends LinearLayout implements View.OnClickListener {
lastSeen.setBackgroundColor(0xffff0000); lastSeen.setBackgroundColor(0xffff0000);
contentContainer.addView(lastSeen, new LayoutParams(LayoutParams.MATCH_PARENT, lastSeenHeight)); contentContainer.addView(lastSeen, new LayoutParams(LayoutParams.MATCH_PARENT, lastSeenHeight));
if (!manager.getLoadable().isThreadMode()) { if (!loadable.isThreadMode()) {
Utils.setPressedDrawable(contentContainer); setPressedDrawable(contentContainer);
} }
full.addView(contentContainer, matchWrapParams); full.addView(contentContainer, matchWrapParams);
optionsView = new ImageView(context); optionsView = new ImageView(context);
optionsView.setImageResource(R.drawable.ic_overflow); optionsView.setImageResource(R.drawable.ic_overflow);
Utils.setPressedDrawable(optionsView); setPressedDrawable(optionsView);
optionsView.setPadding(optionsLeftPadding, optionsTopPadding, optionsRightPadding, optionsBottomPadding); optionsView.setPadding(optionsLeftPadding, optionsTopPadding, optionsRightPadding, optionsBottomPadding);
optionsView.setOnClickListener(new OnClickListener() { optionsView.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(final View v) { public void onClick(final View v) {
PopupMenu popupMenu = new PopupMenu(context, 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(); popupMenu.show();
if (ThemeHelper.getInstance().getTheme().isLightTheme) { if (ThemeHelper.getInstance().getTheme().isLightTheme) {
optionsView.setImageResource(R.drawable.ic_overflow_black); optionsView.setImageResource(R.drawable.ic_overflow_black);
@ -454,20 +466,44 @@ public class PostView extends LinearLayout implements View.OnClickListener {
} }
public void onLinkableClick(PostLinkable linkable) { public void onLinkableClick(PostLinkable linkable) {
manager.onPostLinkableClicked(linkable); callback.onPostLinkableClicked(linkable);
} }
@Override @Override
public void onClick(View v) { public void onClick(View v) {
manager.onPostClicked(post); callback.onPostClicked(post);
} }
private boolean isList() { private boolean isList() {
return manager.getViewMode() == ThreadManager.ViewMode.LIST; return true;
// TODO
// return callback.getViewMode() == ThreadManager.ViewMode.LIST;
} }
private boolean isGrid() { 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 { private class PostViewMovementMethod extends LinkMovementMethod {

@ -38,9 +38,9 @@ import com.koushikdutta.async.future.Future;
import org.floens.chan.ChanApplication; import org.floens.chan.ChanApplication;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.ChanPreferences; import org.floens.chan.core.ChanPreferences;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.FileCache; import org.floens.chan.utils.FileCache;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Utils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -107,7 +107,7 @@ public class ThumbnailImageView extends LoadView implements View.OnClickListener
if (response.getBitmap() != null && thumbnailNeeded) { if (response.getBitmap() != null && thumbnailNeeded) {
ImageView thumbnail = new ImageView(getContext()); ImageView thumbnail = new ImageView(getContext());
thumbnail.setImageBitmap(response.getBitmap()); thumbnail.setImageBitmap(response.getBitmap());
thumbnail.setLayoutParams(Utils.MATCH_PARAMS); thumbnail.setLayoutParams(AndroidUtils.MATCH_PARAMS);
setView(thumbnail, false); setView(thumbnail, false);
} }
} }
@ -222,7 +222,7 @@ public class ThumbnailImageView extends LoadView implements View.OnClickListener
GifImageView view = new GifImageView(getContext()); GifImageView view = new GifImageView(getContext());
view.setImageDrawable(drawable); view.setImageDrawable(drawable);
view.setLayoutParams(Utils.MATCH_PARAMS); view.setLayoutParams(AndroidUtils.MATCH_PARAMS);
setView(view, false); setView(view, false);
} }
@ -273,7 +273,7 @@ public class ThumbnailImageView extends LoadView implements View.OnClickListener
videoView.setZOrderOnTop(true); videoView.setZOrderOnTop(true);
videoView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, videoView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
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); LayoutParams par = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
par.gravity = Gravity.CENTER; par.gravity = Gravity.CENTER;
videoView.setLayoutParams(par); videoView.setLayoutParams(par);

@ -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<String, Typeface> 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);
}
}

@ -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);<br>
* 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]));
}
}

@ -60,7 +60,7 @@ public class FileCache {
.progress(new ProgressCallback() { .progress(new ProgressCallback() {
@Override @Override
public void onProgress(final long downloaded, final long total) { public void onProgress(final long downloaded, final long total) {
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
callback.onProgress(downloaded, total, false); callback.onProgress(downloaded, total, false);

@ -17,6 +17,7 @@
*/ */
package org.floens.chan.utils; package org.floens.chan.utils;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -26,43 +27,47 @@ import java.io.StringWriter;
import java.io.Writer; import java.io.Writer;
public class IOUtils { public class IOUtils {
private static final int DEFAULT_BUFFER_SIZE = 4096;
public static String readString(InputStream is) { public static String readString(InputStream is) {
StringWriter sw = new StringWriter(); InputStreamReader reader = new InputStreamReader(is);
StringWriter writer = new StringWriter();
try { try {
copy(new InputStreamReader(is), sw); copy(reader, writer);
is.close();
sw.close();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); 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 { public static void copy(InputStream is, OutputStream os) throws IOException {
int read; int read;
byte[] buffer = new byte[4096]; byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
while ((read = is.read(buffer)) != -1) { while ((read = is.read(buffer)) != -1) {
os.write(buffer, 0, read); os.write(buffer, 0, read);
} }
is.close();
os.close();
} }
public static void copy(Reader input, Writer output) throws IOException { public static void copy(Reader input, Writer output) throws IOException {
char[] buffer = new char[4096]; char[] buffer = new char[DEFAULT_BUFFER_SIZE];
int read; int read;
while ((read = input.read(buffer)) != -1) { while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read); output.write(buffer, 0, read);
} }
} }
public static void closeQuietly(Closeable stream) {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// ignore
}
}
}
} }

@ -49,17 +49,12 @@ public class ImageDecoder {
try { try {
IOUtils.copy(fis, baos); IOUtils.copy(fis, baos);
bitmap = decode(baos.toByteArray(), maxWidth, maxHeight); bitmap = decode(baos.toByteArray(), maxWidth, maxHeight);
} catch (IOException | OutOfMemoryError e) { } catch (IOException | OutOfMemoryError e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
try { IOUtils.closeQuietly(fis);
fis.close(); IOUtils.closeQuietly(baos);
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
} }
return bitmap; return bitmap;

@ -38,6 +38,8 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -120,7 +122,7 @@ public class ImageSaver {
} }
private void showToast(final Context context, final String message) { private void showToast(final Context context, final String message) {
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
Toast.makeText(context, message, Toast.LENGTH_LONG).show(); Toast.makeText(context, message, Toast.LENGTH_LONG).show();
@ -250,11 +252,16 @@ public class ImageSaver {
private boolean storeImage(final File source, final File destination) { private boolean storeImage(final File source, final File destination) {
boolean res = true; boolean res = true;
InputStream is = null;
OutputStream os = null;
try { try {
is = new FileInputStream(source);
os = new FileOutputStream(destination);
IOUtils.copy(new FileInputStream(source), new FileOutputStream(destination)); IOUtils.copy(new FileInputStream(source), new FileOutputStream(destination));
} catch (IOException e) { } catch (IOException e) {
res = false; res = false;
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
} }
return res; return res;
@ -265,7 +272,7 @@ public class ImageSaver {
new MediaScannerConnection.OnScanCompletedListener() { new MediaScannerConnection.OnScanCompletedListener() {
@Override @Override
public void onScanCompleted(String unused, final Uri uri) { public void onScanCompleted(String unused, final Uri uri) {
Utils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
Logger.i(TAG, "Media scan succeeded: " + uri); Logger.i(TAG, "Media scan succeeded: " + uri);

@ -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 <http://www.gnu.org/licenses/>.
*/
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) {
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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";
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<solid android:color="#20000000" />
</shape>
</item>
<item android:state_focused="true">
<shape>
<solid android:color="#20000000" />
</shape>
</item>
<item android:drawable="@android:color/transparent" />
</selector>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<org.floens.multipanetest.layout.ImageViewLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff000000">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</org.floens.multipanetest.layout.ImageViewLayout>

@ -0,0 +1,42 @@
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/root_layout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffffff">
<org.floens.chan.ui.toolbar.Toolbar
android:elevation="8dp"
android:id="@+id/toolbar"
android:background="@color/primary"
android:layout_width="match_parent"
android:layout_height="56dp" />
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<FrameLayout
android:background="#ffffff00"
android:id="@+id/drawer"
android:layout_gravity="left"
android:layout_width="200dp"
android:layout_height="match_parent">
<TextView
android:text="Hello, world!"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
</android.support.v4.widget.DrawerLayout>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffffff"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Settings here" />
</LinearLayout>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:textSize="16sp"
android:textColor="#ff000000"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp" />

@ -103,7 +103,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="multiple_images">images</string> <string name="multiple_images">images</string>
<string name="one_post">post</string> <string name="one_post">post</string>
<string name="multiple_posts">posts</string> <string name="multiple_posts">posts</string>
<string name="post_info">Info</string> <string name="post_info">Post info</string>
<string-array name="post_options"> <string-array name="post_options">
<item>Quote</item> <item>Quote</item>
<item>Quote text</item> <item>Quote text</item>

@ -22,9 +22,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<item name="colorPrimaryDark">@color/primary_dark</item> <item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent_material_light</item> <item name="colorAccent">@color/accent_material_light</item>
<item name="android:windowBackground">@android:color/white</item>
<item name="dropDownListViewStyle">@style/ToolbarDropDownListViewStyle</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
</style> </style>
<!-- For the toolbar dropdown list -->
<style name="ToolbarDropDownListViewStyle" parent="Widget.AppCompat.ListView.DropDown">
<item name="android:background">#ffffffff</item>
</style>
<style name="Chan.ImageView" parent="Theme.AppCompat.NoActionBar"> <style name="Chan.ImageView" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@color/image_view_background</item> <item name="android:windowBackground">@color/image_view_background</item>
<item name="android:windowIsTranslucent">true</item> <item name="android:windowIsTranslucent">true</item>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.
Loading…
Cancel
Save