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

*

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

- * + *

*

* Example usage: *

- * + *

*

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

*

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

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

* There's not much I can do in my code for now, but we can mask the result by * just catching the problem and ignoring it. * diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java index 5065c562..ac49fb5c 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java @@ -18,22 +18,27 @@ package org.floens.chan.ui.view; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.widget.FrameLayout; -import android.widget.LinearLayout; import android.widget.ProgressBar; -import org.floens.chan.utils.SimpleAnimatorListener; +import java.util.HashMap; +import java.util.Map; /** * Container for a view with an ProgressBar. Toggles between the view and a * ProgressBar. */ public class LoadView extends FrameLayout { - public int fadeDuration = 100; + private int fadeDuration = 200; + private Map animatorsIn = new HashMap<>(); + private Map animatorsOut = new HashMap<>(); public LoadView(Context context) { super(context); @@ -54,6 +59,10 @@ public class LoadView extends FrameLayout { setView(null, false); } + public void setFadeDuration(int fadeDuration) { + this.fadeDuration = fadeDuration; + } + /** * Set the content of this container. It will fade the old one out with the * new one. Set view to null to show the progressbar. @@ -64,40 +73,86 @@ public class LoadView extends FrameLayout { setView(view, true); } - public void setView(View view, boolean animation) { - if (view == null) { - LinearLayout layout = new LinearLayout(getContext()); - layout.setGravity(Gravity.CENTER); + public void setView(View newView, boolean animate) { + // Passing null means showing a progressbar + if (newView == null) { + FrameLayout progressBar = new FrameLayout(getContext()); + progressBar.addView(new ProgressBar(getContext()), new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER)); + newView = progressBar; + } - ProgressBar pb = new ProgressBar(getContext()); - layout.addView(pb); - view = layout; + // Readded while still running a add/remove animation for the new view + // This also removes the new view from this view + AnimatorSet out = animatorsOut.remove(newView); + if (out != null) { + out.cancel(); } - while (getChildCount() > 1) { - removeViewAt(0); + AnimatorSet in = animatorsIn.remove(newView); + if (in != null) { + in.cancel(); } - View currentView = getChildAt(0); - if (currentView != null) { - if (animation) { - final View tempView = currentView; - currentView.animate().setDuration(fadeDuration).alpha(0).setListener(new SimpleAnimatorListener() { - @Override - public void onAnimationEnd(Animator animation) { - removeView(tempView); - } - }); - } else { - removeView(currentView); + // Add fade out animations for all remaining view + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child != null) { + AnimatorSet inSet = animatorsIn.remove(child); + if (inSet != null) { + inSet.cancel(); + } + + if (!animatorsOut.containsKey(child)) { + animateViewOut(child); + } } } - addView(view); + addView(newView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - if (animation) { - view.setAlpha(0f); - view.animate().setDuration(fadeDuration).alpha(1f); + if (animate) { + // Fade view in + if (newView.getAlpha() == 1f) { + newView.setAlpha(0f); + } + animateViewIn(newView); + } else { + newView.setAlpha(1f); } } + + private void animateViewOut(final View view) { + // Cancel any fade in animations + AnimatorSet fadeIn = animatorsIn.remove(view); + if (fadeIn != null) { + fadeIn.cancel(); + } + + final AnimatorSet set = new AnimatorSet(); + set.setDuration(fadeDuration); + set.play(ObjectAnimator.ofFloat(view, View.ALPHA, 0f)); + animatorsOut.put(view, set); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + removeView(view); + animatorsOut.remove(set); + } + }); + set.start(); + } + + private void animateViewIn(View view) { + final AnimatorSet set = new AnimatorSet(); + set.setDuration(fadeDuration); + set.play(ObjectAnimator.ofFloat(view, View.ALPHA, 1f)); + animatorsIn.put(view, set); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animatorsIn.remove(set); + } + }); + set.start(); + } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java index 5473f4c7..b4569c67 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java @@ -31,6 +31,8 @@ import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.util.AttributeSet; import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; @@ -43,13 +45,14 @@ import com.android.volley.toolbox.NetworkImageView; import org.floens.chan.ChanApplication; import org.floens.chan.R; -import org.floens.chan.core.manager.ThreadManager; +import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostLinkable; import org.floens.chan.utils.IconCache; import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.Time; -import org.floens.chan.utils.Utils; + +import static org.floens.chan.utils.AndroidUtils.setPressedDrawable; public class PostView extends LinearLayout implements View.OnClickListener { private final static LinearLayout.LayoutParams matchParams = new LinearLayout.LayoutParams( @@ -63,8 +66,9 @@ public class PostView extends LinearLayout implements View.OnClickListener { private final Activity context; - private ThreadManager manager; private Post post; + private PostViewCallback callback; + private Loadable loadable; private int highlightQuotesNo = -1; private boolean isBuild = false; @@ -113,13 +117,14 @@ public class PostView extends LinearLayout implements View.OnClickListener { } } - public void setPost(final Post post, final ThreadManager manager) { + public void setPost(final Post post, final PostViewCallback callback) { this.post = post; - this.manager = manager; + this.callback = callback; + this.loadable = callback.getLoadable(); highlightQuotesNo = -1; - boolean boardCatalogMode = manager.getLoadable().isBoardMode() || manager.getLoadable().isCatalogMode(); + boolean boardCatalogMode = loadable.isBoardMode() || loadable.isCatalogMode(); TypedArray ta = context.obtainStyledAttributes(null, R.styleable.PostView, R.attr.post_style, 0); @@ -167,7 +172,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { commentView.setText(post.comment); - if (manager.getLoadable().isThreadMode()) { + if (loadable.isThreadMode()) { post.setLinkableListener(this); commentView.setMovementMethod(new PostViewMovementMethod()); commentView.setOnClickListener(this); @@ -199,11 +204,11 @@ public class PostView extends LinearLayout implements View.OnClickListener { } } - if (manager.getLoadable().isThreadMode()) { + if (loadable.isThreadMode()) { repliesCountView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - manager.showPostReplies(post); + callback.onShowPostReplies(post); } }); } @@ -236,13 +241,13 @@ public class PostView extends LinearLayout implements View.OnClickListener { if (post.isSavedReply) { full.setBackgroundColor(savedReplyColor); - } else if (manager.isPostHightlighted(post)) { + } else if (callback.isPostHightlighted(post)) { full.setBackgroundColor(highlightedColor); } else { full.setBackgroundColor(0x00000000); } - if (manager.isPostLastSeen(post)) { + if (callback.isPostLastSeen(post)) { lastSeen.setVisibility(View.VISIBLE); } else { lastSeen.setVisibility(View.GONE); @@ -318,7 +323,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { imageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - manager.onThumbnailClicked(post); + callback.onThumbnailClicked(post); } }); @@ -382,7 +387,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { if (isList()) { commentView.setPadding(postPadding, commentPadding, postPadding, commentPadding); - if (manager.getLoadable().isBoardMode() || manager.getLoadable().isCatalogMode()) { + if (loadable.isBoardMode() || loadable.isCatalogMode()) { commentView.setMaxHeight(postListMaxHeight); } } else if (isGrid()) { @@ -398,7 +403,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { } repliesCountView = new TextView(context); - Utils.setPressedDrawable(repliesCountView); + setPressedDrawable(repliesCountView); repliesCountView.setTextColor(replyCountColor); repliesCountView.setPadding(postPadding, postPadding, postPadding, postPadding); repliesCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, repliesCountSize); @@ -410,21 +415,28 @@ public class PostView extends LinearLayout implements View.OnClickListener { lastSeen.setBackgroundColor(0xffff0000); contentContainer.addView(lastSeen, new LayoutParams(LayoutParams.MATCH_PARENT, lastSeenHeight)); - if (!manager.getLoadable().isThreadMode()) { - Utils.setPressedDrawable(contentContainer); + if (!loadable.isThreadMode()) { + setPressedDrawable(contentContainer); } full.addView(contentContainer, matchWrapParams); optionsView = new ImageView(context); optionsView.setImageResource(R.drawable.ic_overflow); - Utils.setPressedDrawable(optionsView); + setPressedDrawable(optionsView); optionsView.setPadding(optionsLeftPadding, optionsTopPadding, optionsRightPadding, optionsBottomPadding); optionsView.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { PopupMenu popupMenu = new PopupMenu(context, v); - manager.showPostOptions(post, popupMenu); + callback.onPopulatePostOptions(post, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + callback.onPostOptionClicked(post, item.getItemId()); + return true; + } + }); popupMenu.show(); if (ThemeHelper.getInstance().getTheme().isLightTheme) { optionsView.setImageResource(R.drawable.ic_overflow_black); @@ -454,20 +466,44 @@ public class PostView extends LinearLayout implements View.OnClickListener { } public void onLinkableClick(PostLinkable linkable) { - manager.onPostLinkableClicked(linkable); + callback.onPostLinkableClicked(linkable); } @Override public void onClick(View v) { - manager.onPostClicked(post); + callback.onPostClicked(post); } private boolean isList() { - return manager.getViewMode() == ThreadManager.ViewMode.LIST; + return true; + // TODO +// return callback.getViewMode() == ThreadManager.ViewMode.LIST; } private boolean isGrid() { - return manager.getViewMode() == ThreadManager.ViewMode.GRID; + return false; + // TODO +// return callback.getViewMode() == ThreadManager.ViewMode.GRID; + } + + public interface PostViewCallback { + public Loadable getLoadable(); + + public void onPostClicked(Post post); + + public void onThumbnailClicked(Post post); + + public void onShowPostReplies(Post post); + + public void onPopulatePostOptions(Post post, Menu menu); + + public void onPostOptionClicked(Post post, int id); + + public void onPostLinkableClicked(PostLinkable linkable); + + public boolean isPostHightlighted(Post post); + + public boolean isPostLastSeen(Post post); } private class PostViewMovementMethod extends LinkMovementMethod { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java index 773b1141..ad6bb938 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java @@ -38,9 +38,9 @@ import com.koushikdutta.async.future.Future; import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.core.ChanPreferences; +import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.FileCache; import org.floens.chan.utils.Logger; -import org.floens.chan.utils.Utils; import java.io.File; import java.io.IOException; @@ -107,7 +107,7 @@ public class ThumbnailImageView extends LoadView implements View.OnClickListener if (response.getBitmap() != null && thumbnailNeeded) { ImageView thumbnail = new ImageView(getContext()); thumbnail.setImageBitmap(response.getBitmap()); - thumbnail.setLayoutParams(Utils.MATCH_PARAMS); + thumbnail.setLayoutParams(AndroidUtils.MATCH_PARAMS); setView(thumbnail, false); } } @@ -222,7 +222,7 @@ public class ThumbnailImageView extends LoadView implements View.OnClickListener GifImageView view = new GifImageView(getContext()); view.setImageDrawable(drawable); - view.setLayoutParams(Utils.MATCH_PARAMS); + view.setLayoutParams(AndroidUtils.MATCH_PARAMS); setView(view, false); } @@ -273,7 +273,7 @@ public class ThumbnailImageView extends LoadView implements View.OnClickListener videoView.setZOrderOnTop(true); videoView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - videoView.setLayoutParams(Utils.MATCH_PARAMS); + videoView.setLayoutParams(AndroidUtils.MATCH_PARAMS); LayoutParams par = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); par.gravity = Gravity.CENTER; videoView.setLayoutParams(par); diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java new file mode 100644 index 00000000..4e37e25c --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java @@ -0,0 +1,193 @@ +package org.floens.chan.utils; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.inputmethod.InputMethodManager; +import android.widget.Toast; + +import org.floens.chan.ChanApplication; +import org.floens.chan.R; + +import java.util.HashMap; + +public class AndroidUtils { + public final static ViewGroup.LayoutParams MATCH_PARAMS = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + public final static ViewGroup.LayoutParams WRAP_PARAMS = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + public final static ViewGroup.LayoutParams MATCH_WRAP_PARAMS = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + public final static ViewGroup.LayoutParams WRAP_MATCH_PARAMS = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); + private static HashMap typefaceCache = new HashMap<>(); + + public static Typeface ROBOTO_MEDIUM; + + public static void init() { + ROBOTO_MEDIUM = getTypeface("Roboto-Medium.ttf"); + } + + public static Resources getRes() { + return ChanApplication.con.getResources(); + } + + public static Context getAppRes() { + return ChanApplication.con; + } + + public static void openLink(String link) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (intent.resolveActivity(getAppRes().getPackageManager()) != null) { + getAppRes().startActivity(intent); + } else { + Toast.makeText(getAppRes(), R.string.open_link_failed, Toast.LENGTH_LONG).show(); + } + } + + public static void shareLink(String link) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, link); + Intent chooser = Intent.createChooser(intent, getRes().getString(R.string.action_share)); + chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (chooser.resolveActivity(getAppRes().getPackageManager()) != null) { + getAppRes().startActivity(chooser); + } else { + Toast.makeText(getAppRes(), R.string.open_link_failed, Toast.LENGTH_LONG).show(); + } + } + + public static int getAttrPixel(int attr) { + TypedArray typedArray = ChanApplication.con.getTheme().obtainStyledAttributes(new int[]{attr}); + int pixels = typedArray.getDimensionPixelSize(0, 0); + typedArray.recycle(); + return pixels; + } + + public static Drawable getAttrDrawable(int attr) { + TypedArray typedArray = ChanApplication.con.getTheme().obtainStyledAttributes(new int[]{attr}); + Drawable drawable = typedArray.getDrawable(0); + typedArray.recycle(); + return drawable; + } + + public static int dp(float dp) { + return (int) (dp * getRes().getDisplayMetrics().density); + } + + public static Typeface getTypeface(String name) { + if (!typefaceCache.containsKey(name)) { + Typeface typeface = Typeface.createFromAsset(getRes().getAssets(), "font/" + name); + typefaceCache.put(name, typeface); + } + return typefaceCache.get(name); + } + + /** + * Causes the runnable to be added to the message queue. The runnable will + * be run on the ui thread. + * + * @param runnable + */ + public static void runOnUiThread(Runnable runnable) { + new Handler(Looper.getMainLooper()).post(runnable); + } + + public static void requestKeyboardFocus(Dialog dialog, final View view) { + view.requestFocus(); + dialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(view, 0); + } + }); + } + + public static String getReadableFileSize(int bytes, boolean si) { + int unit = si ? 1000 : 1024; + if (bytes < unit) + return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); + return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); + } + + public static CharSequence ellipsize(CharSequence text, int max) { + if (text.length() <= max) { + return text; + } else { + return text.subSequence(0, max) + "\u2026"; + } + } + + public interface OnMeasuredCallback { + void onMeasured(View view, int width, int height); + } + + /** + * Waits for a measure. Calls callback immediately if the view width and height are more than 0. + * Otherwise it registers an onpredrawlistener and rechedules a layout. + * Warning: the view you give must be attached to the view root!!! + */ + public static void waitForMeasure(final View view, final OnMeasuredCallback callback) { + waitForMeasure(true, view, callback); + } + + public static void waitForLayout(final View view, final OnMeasuredCallback callback) { + waitForMeasure(false, view, callback); + } + + private static void waitForMeasure(boolean returnIfZero, final View view, final OnMeasuredCallback callback) { + int width = view.getWidth(); + int height = view.getHeight(); + + if (returnIfZero && width > 0 && height > 0) { + callback.onMeasured(view, width, height); + } else { + view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + final ViewTreeObserver observer = view.getViewTreeObserver(); + if (observer.isAlive()) { + observer.removeOnPreDrawListener(this); + } + + try { + callback.onMeasured(view, view.getWidth(), view.getHeight()); + } catch (Exception e) { + Log.i("AndroidUtils", "Exception in onMeasured", e); + } + + return true; + } + }); + } + } + + public static void setPressedDrawable(View view) { + TypedArray arr = view.getContext().obtainStyledAttributes(new int[]{android.R.attr.selectableItemBackground}); + + Drawable drawable = arr.getDrawable(0); + + arr.recycle(); + view.setBackgroundDrawable(drawable); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java new file mode 100644 index 00000000..c938476e --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java @@ -0,0 +1,51 @@ +package org.floens.chan.utils; + +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; +import android.widget.ImageView; + +public class AnimationUtils { + /** + * On your start view call startView.getGlobalVisibleRect(startBounds) + * and on your end container call endContainer.getGlobalVisibleRect(endBounds, globalOffset);
+ * startBounds and endBounds will be adjusted appropriately and the starting scale will be returned. + * + * @param startBounds your startBounds + * @param endBounds your endBounds + * @param globalOffset your globalOffset + * @return the starting scale + */ + public static float calculateBoundsAnimation(Rect startBounds, Rect endBounds, Point globalOffset) { + startBounds.offset(-globalOffset.x, -globalOffset.y); + endBounds.offset(-globalOffset.x, -globalOffset.y); + + float startScale; + if ((float) endBounds.width() / endBounds.height() > (float) startBounds.width() / startBounds.height()) { + // Extend start bounds horizontally + startScale = (float) startBounds.height() / endBounds.height(); + float startWidth = startScale * endBounds.width(); + float deltaWidth = (startWidth - startBounds.width()) / 2; + startBounds.left -= deltaWidth; + startBounds.right += deltaWidth; + } else { + // Extend start bounds vertically + startScale = (float) startBounds.width() / endBounds.width(); + float startHeight = startScale * endBounds.height(); + float deltaHeight = (startHeight - startBounds.height()) / 2; + startBounds.top -= deltaHeight; + startBounds.bottom += deltaHeight; + } + + return startScale; + } + + public static void adjustImageViewBoundsToDrawableBounds(ImageView imageView, Rect bounds) { + float[] f = new float[9]; + imageView.getImageMatrix().getValues(f); + bounds.left += f[Matrix.MTRANS_X]; + bounds.top += f[Matrix.MTRANS_Y]; + bounds.right = (bounds.left + (int) (imageView.getDrawable().getIntrinsicWidth() * f[Matrix.MSCALE_X])); + bounds.bottom = (bounds.top + (int) (imageView.getDrawable().getIntrinsicHeight() * f[Matrix.MSCALE_Y])); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java b/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java index 3ea6ee97..233b1bc6 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java @@ -60,7 +60,7 @@ public class FileCache { .progress(new ProgressCallback() { @Override public void onProgress(final long downloaded, final long total) { - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { callback.onProgress(downloaded, total, false); diff --git a/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java index 1e7ff38d..9e051692 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java @@ -17,6 +17,7 @@ */ package org.floens.chan.utils; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -26,43 +27,47 @@ import java.io.StringWriter; import java.io.Writer; public class IOUtils { + private static final int DEFAULT_BUFFER_SIZE = 4096; + public static String readString(InputStream is) { - StringWriter sw = new StringWriter(); + InputStreamReader reader = new InputStreamReader(is); + StringWriter writer = new StringWriter(); try { - copy(new InputStreamReader(is), sw); - is.close(); - sw.close(); + copy(reader, writer); } catch (IOException e) { e.printStackTrace(); + } finally { + closeQuietly(writer); + closeQuietly(reader); } - return sw.toString(); + return writer.toString(); } - /** - * Copies the inputstream to the outputstream and closes both streams. - * - * @param is - * @param os - * @throws IOException - */ public static void copy(InputStream is, OutputStream os) throws IOException { int read; - byte[] buffer = new byte[4096]; + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; while ((read = is.read(buffer)) != -1) { os.write(buffer, 0, read); } - - is.close(); - os.close(); } public static void copy(Reader input, Writer output) throws IOException { - char[] buffer = new char[4096]; + char[] buffer = new char[DEFAULT_BUFFER_SIZE]; int read; while ((read = input.read(buffer)) != -1) { output.write(buffer, 0, read); } } + + public static void closeQuietly(Closeable stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + // ignore + } + } + } } diff --git a/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java b/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java index d0631708..0639dad6 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java @@ -49,17 +49,12 @@ public class ImageDecoder { try { IOUtils.copy(fis, baos); - bitmap = decode(baos.toByteArray(), maxWidth, maxHeight); } catch (IOException | OutOfMemoryError e) { e.printStackTrace(); } finally { - try { - fis.close(); - baos.close(); - } catch (IOException e) { - e.printStackTrace(); - } + IOUtils.closeQuietly(fis); + IOUtils.closeQuietly(baos); } return bitmap; diff --git a/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java b/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java index b9e9c526..db95a2b8 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java @@ -38,6 +38,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -120,7 +122,7 @@ public class ImageSaver { } private void showToast(final Context context, final String message) { - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(context, message, Toast.LENGTH_LONG).show(); @@ -250,11 +252,16 @@ public class ImageSaver { private boolean storeImage(final File source, final File destination) { boolean res = true; - + InputStream is = null; + OutputStream os = null; try { + is = new FileInputStream(source); + os = new FileOutputStream(destination); IOUtils.copy(new FileInputStream(source), new FileOutputStream(destination)); } catch (IOException e) { res = false; + IOUtils.closeQuietly(is); + IOUtils.closeQuietly(os); } return res; @@ -265,7 +272,7 @@ public class ImageSaver { new MediaScannerConnection.OnScanCompletedListener() { @Override public void onScanCompleted(String unused, final Uri uri) { - Utils.runOnUiThread(new Runnable() { + AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { Logger.i(TAG, "Media scan succeeded: " + uri); diff --git a/Clover/app/src/main/java/org/floens/chan/utils/SimpleAnimatorListener.java b/Clover/app/src/main/java/org/floens/chan/utils/SimpleAnimatorListener.java deleted file mode 100644 index ecafd1fd..00000000 --- a/Clover/app/src/main/java/org/floens/chan/utils/SimpleAnimatorListener.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Clover - 4chan browser https://github.com/Floens/Clover/ - * Copyright (C) 2014 Floens - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.floens.chan.utils; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; - -/** - * Extends AnimatorListener with no-op methods. - */ -public class SimpleAnimatorListener implements AnimatorListener { - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - - @Override - public void onAnimationStart(Animator animation) { - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/utils/Utils.java b/Clover/app/src/main/java/org/floens/chan/utils/Utils.java deleted file mode 100644 index 6bbef463..00000000 --- a/Clover/app/src/main/java/org/floens/chan/utils/Utils.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Clover - 4chan browser https://github.com/Floens/Clover/ - * Copyright (C) 2014 Floens - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.floens.chan.utils; - -import android.app.Dialog; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnShowListener; -import android.content.Intent; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.util.DisplayMetrics; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.Toast; - -import org.floens.chan.ChanApplication; -import org.floens.chan.R; - -public class Utils { - private static DisplayMetrics displayMetrics; - - public final static ViewGroup.LayoutParams MATCH_PARAMS = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - public final static ViewGroup.LayoutParams WRAP_PARAMS = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - public final static ViewGroup.LayoutParams MATCH_WRAP_PARAMS = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - public final static ViewGroup.LayoutParams WRAP_MATCH_PARAMS = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); - - public static int dp(float dp) { - // return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); - if (displayMetrics == null) { - displayMetrics = ChanApplication.getInstance().getResources().getDisplayMetrics(); - } - - return (int) (dp * displayMetrics.density); - } - - /** - * Sets the android.R.attr.selectableItemBackground as background drawable - * on the view. - * - * @param view - */ - @SuppressWarnings("deprecation") - public static void setPressedDrawable(View view) { - Drawable drawable = Utils.getSelectableBackgroundDrawable(view.getContext()); - view.setBackgroundDrawable(drawable); - } - - public static Drawable getSelectableBackgroundDrawable(Context context) { - TypedArray arr = context.obtainStyledAttributes(new int[]{android.R.attr.selectableItemBackground}); - - Drawable drawable = arr.getDrawable(0); - - arr.recycle(); - - return drawable; - } - - /** - * Causes the runnable to be added to the message queue. The runnable will - * be run on the ui thread. - * - * @param runnable - */ - public static void runOnUiThread(Runnable runnable) { - new Handler(Looper.getMainLooper()).post(runnable); - } - - public static void requestKeyboardFocus(Dialog dialog, final View view) { - view.requestFocus(); - dialog.setOnShowListener(new OnShowListener() { - @Override - public void onShow(DialogInterface dialog) { - InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService( - Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(view, 0); - } - }); - } - - public static void openLink(Context context, String link) { - try { - context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(link))); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - Toast.makeText(context, R.string.open_link_failed, Toast.LENGTH_LONG).show(); - } - } - - public static String getReadableFileSize(int bytes, boolean si) { - int unit = si ? 1000 : 1024; - if (bytes < unit) - return bytes + " B"; - int exp = (int) (Math.log(bytes) / Math.log(unit)); - String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); - return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); - } - - public static CharSequence ellipsize(CharSequence text, int max) { - if (text.length() <= max) { - return text; - } else { - return text.subSequence(0, max) + "\u2026"; - } - } -} diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_more.png b/Clover/app/src/main/res/drawable-hdpi/ic_more.png new file mode 100644 index 00000000..fdc4a5ad Binary files /dev/null and b/Clover/app/src/main/res/drawable-hdpi/ic_more.png differ diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_more.png b/Clover/app/src/main/res/drawable-mdpi/ic_more.png new file mode 100644 index 00000000..1d8ad18a Binary files /dev/null and b/Clover/app/src/main/res/drawable-mdpi/ic_more.png differ diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_more.png b/Clover/app/src/main/res/drawable-xhdpi/ic_more.png new file mode 100644 index 00000000..1b04eda0 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xhdpi/ic_more.png differ diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_more.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_more.png new file mode 100644 index 00000000..2955c02e Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxhdpi/ic_more.png differ diff --git a/Clover/app/src/main/res/drawable-xxxhdpi/ic_more.png b/Clover/app/src/main/res/drawable-xxxhdpi/ic_more.png new file mode 100644 index 00000000..25b6f9f1 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxxhdpi/ic_more.png differ diff --git a/Clover/app/src/main/res/drawable/gray_background_selector.xml b/Clover/app/src/main/res/drawable/gray_background_selector.xml new file mode 100644 index 00000000..9693dac2 --- /dev/null +++ b/Clover/app/src/main/res/drawable/gray_background_selector.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Clover/app/src/main/res/layout/image_view_layout.xml b/Clover/app/src/main/res/layout/image_view_layout.xml new file mode 100644 index 00000000..7a14f1e6 --- /dev/null +++ b/Clover/app/src/main/res/layout/image_view_layout.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/Clover/app/src/main/res/layout/root_layout.xml b/Clover/app/src/main/res/layout/root_layout.xml new file mode 100644 index 00000000..a9224edb --- /dev/null +++ b/Clover/app/src/main/res/layout/root_layout.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/Clover/app/src/main/res/layout/settings_layout.xml b/Clover/app/src/main/res/layout/settings_layout.xml new file mode 100644 index 00000000..e53a7844 --- /dev/null +++ b/Clover/app/src/main/res/layout/settings_layout.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/Clover/app/src/main/res/layout/toolbar_menu_item.xml b/Clover/app/src/main/res/layout/toolbar_menu_item.xml new file mode 100644 index 00000000..b0bb324a --- /dev/null +++ b/Clover/app/src/main/res/layout/toolbar_menu_item.xml @@ -0,0 +1,9 @@ + + diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index c0968006..dc0816aa 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -103,7 +103,7 @@ along with this program. If not, see . images post posts - Info + Post info Quote Quote text diff --git a/Clover/app/src/main/res/values/styles.xml b/Clover/app/src/main/res/values/styles.xml index 627003da..531aaf6d 100644 --- a/Clover/app/src/main/res/values/styles.xml +++ b/Clover/app/src/main/res/values/styles.xml @@ -22,9 +22,17 @@ along with this program. If not, see . @color/primary_dark @color/accent_material_light + @android:color/white + + @style/ToolbarDropDownListViewStyle true + + +