Merge branch 'dev' into dev

crowdin
Florens 7 years ago committed by GitHub
commit 0be0e8a68a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      Clover/app/build.gradle
  2. 12
      Clover/app/src/main/AndroidManifest.xml
  3. 5
      Clover/app/src/main/java/org/floens/chan/controller/Controller.java
  4. 36
      Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheProvider.java
  5. 7
      Clover/app/src/main/java/org/floens/chan/core/di/NetModule.java
  6. 11
      Clover/app/src/main/java/org/floens/chan/core/repository/SiteRepository.java
  7. 44
      Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumDownloadController.java
  8. 42
      Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumViewController.java
  9. 17
      Clover/app/src/main/java/org/floens/chan/ui/controller/ArchiveController.java
  10. 186
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  11. 26
      Clover/app/src/main/java/org/floens/chan/ui/controller/FiltersController.java
  12. 81
      Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java
  13. 194
      Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java
  14. 34
      Clover/app/src/main/java/org/floens/chan/ui/controller/LogsController.java
  15. 30
      Clover/app/src/main/java/org/floens/chan/ui/controller/SitesSetupController.java
  16. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java
  17. 5
      Clover/app/src/main/java/org/floens/chan/ui/controller/ToolbarNavigationController.java
  18. 187
      Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java
  19. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java
  20. 7
      Clover/app/src/main/java/org/floens/chan/ui/layout/SearchLayout.java
  21. 120
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java
  22. 402
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
  23. 530
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarContainer.java
  24. 56
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenu.java
  25. 170
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuItem.java
  26. 64
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuSubItem.java
  27. 100
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenuView.java
  28. 159
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarPresenter.java
  29. 4
      Clover/app/src/main/java/org/floens/chan/ui/view/CustomScaleImageView.java
  30. 16
      Clover/app/src/main/java/org/floens/chan/ui/view/FloatingMenu.java
  31. 17
      Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java
  32. 12
      Clover/app/src/main/res/xml/file_paths.xml
  33. 2
      Clover/build.gradle

@ -124,28 +124,32 @@ android {
} }
dependencies { dependencies {
def supportVersion = '27.1.0' def supportVersion = '27.1.1'
implementation "com.android.support:support-v13:${supportVersion}" implementation "com.android.support:support-v13:${supportVersion}"
implementation "com.android.support:appcompat-v7:${supportVersion}" implementation "com.android.support:appcompat-v7:${supportVersion}"
implementation "com.android.support:recyclerview-v7:${supportVersion}" implementation "com.android.support:recyclerview-v7:${supportVersion}"
implementation "com.android.support:cardview-v7:${supportVersion}" implementation "com.android.support:cardview-v7:${supportVersion}"
implementation "com.android.support:support-annotations:${supportVersion}" implementation "com.android.support:support-annotations:${supportVersion}"
implementation "com.android.support:exifinterface:${supportVersion}"
implementation "com.android.support:design:${supportVersion}" implementation "com.android.support:design:${supportVersion}"
implementation "com.android.support:customtabs:${supportVersion}" implementation "com.android.support:customtabs:${supportVersion}"
implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation 'com.squareup.okhttp3:okhttp:3.8.0' implementation 'com.squareup.okhttp3:okhttp:3.10.0'
//noinspection GradleDependency
implementation 'com.j256.ormlite:ormlite-core:4.48' implementation 'com.j256.ormlite:ormlite-core:4.48'
//noinspection GradleDependency
implementation 'com.j256.ormlite:ormlite-android:4.48' implementation 'com.j256.ormlite:ormlite-android:4.48'
implementation 'org.jsoup:jsoup:1.9.2' implementation 'org.jsoup:jsoup:1.11.3'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.1.16' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.12'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.5.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
//noinspection GradleDependency
implementation 'de.greenrobot:eventbus:2.4.0' implementation 'de.greenrobot:eventbus:2.4.0'
implementation 'org.nibor.autolink:autolink:0.6.0' implementation 'org.nibor.autolink:autolink:0.9.0'
implementation 'com.google.code.gson:gson:2.8.1' implementation 'com.google.code.gson:gson:2.8.5'
implementation 'me.xdrop:fuzzywuzzy:1.1.9' implementation 'me.xdrop:fuzzywuzzy:1.1.10'
implementation 'org.codejargon.feather:feather:1.0' implementation 'org.codejargon.feather:feather:1.0'
releaseImplementation 'ch.acra:acra-http:5.0.1' releaseImplementation 'ch.acra:acra-http:5.1.3'
} }

@ -95,6 +95,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</receiver> </receiver>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="org.floens.chan.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

@ -29,6 +29,7 @@ import org.floens.chan.controller.transition.FadeOutTransition;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.controller.DoubleNavigationController; import org.floens.chan.ui.controller.DoubleNavigationController;
import org.floens.chan.ui.toolbar.NavigationItem; import org.floens.chan.ui.toolbar.NavigationItem;
import org.floens.chan.ui.toolbar.Toolbar;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
@ -247,6 +248,10 @@ public abstract class Controller {
return (ViewGroup) LayoutInflater.from(context).inflate(resId, null); return (ViewGroup) LayoutInflater.from(context).inflate(resId, null);
} }
public Toolbar getToolbar() {
return null;
}
private void attachToView(ViewGroup parentView, boolean over) { private void attachToView(ViewGroup parentView, boolean over) {
ViewGroup.LayoutParams params = view.getLayoutParams(); ViewGroup.LayoutParams params = view.getLayoutParams();

@ -0,0 +1,36 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.cache;
import android.content.Context;
import android.net.Uri;
import android.support.v4.content.FileProvider;
import org.floens.chan.utils.AndroidUtils;
import java.io.File;
public class FileCacheProvider {
// Same as the one defined in the manifest.
private static final String AUTHORITY = "org.floens.chan.fileprovider";
public static Uri getUriForFile(File file) {
Context applicationContext = AndroidUtils.getAppContext();
return FileProvider.getUriForFile(applicationContext, AUTHORITY, file);
}
}

@ -37,6 +37,11 @@ public class NetModule {
} }
private File getCacheDir(Context applicationContext) { private File getCacheDir(Context applicationContext) {
return applicationContext.getExternalCacheDir() != null ? applicationContext.getExternalCacheDir() : applicationContext.getCacheDir(); // See also res/xml/filepaths.xml for the fileprovider.
if (applicationContext.getExternalCacheDir() != null) {
return applicationContext.getExternalCacheDir();
} else {
return applicationContext.getCacheDir();
}
} }
} }

@ -10,6 +10,7 @@ import org.floens.chan.core.settings.json.JsonSettings;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteRegistry; import org.floens.chan.core.site.SiteRegistry;
import org.floens.chan.core.site.sites.chan4.Chan4; import org.floens.chan.core.site.sites.chan4.Chan4;
import org.floens.chan.utils.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -22,6 +23,8 @@ import javax.inject.Singleton;
@Singleton @Singleton
public class SiteRepository { public class SiteRepository {
private static final String TAG = "SiteRepository";
private DatabaseManager databaseManager; private DatabaseManager databaseManager;
private Sites sitesObservable = new Sites(); private Sites sitesObservable = new Sites();
@ -84,7 +87,13 @@ public class SiteRepository {
databaseManager.getDatabaseSiteManager().getAll()); databaseManager.getDatabaseSiteManager().getAll());
for (SiteModel siteModel : models) { for (SiteModel siteModel : models) {
SiteConfigSettingsHolder holder = instantiateSiteFromModel(siteModel); SiteConfigSettingsHolder holder;
try {
holder = instantiateSiteFromModel(siteModel);
} catch (IllegalArgumentException e) {
Logger.e(TAG, "instantiateSiteFromModel", e);
break;
}
Site site = holder.site; Site site = holder.site;
SiteConfig config = holder.config; SiteConfig config = holder.config;

@ -33,14 +33,12 @@ import android.widget.ImageView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.saver.ImageSaveTask; import org.floens.chan.core.saver.ImageSaveTask;
import org.floens.chan.core.saver.ImageSaver; import org.floens.chan.core.saver.ImageSaver;
import org.floens.chan.ui.theme.ThemeHelper; import org.floens.chan.ui.theme.ThemeHelper;
import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem; import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.view.FloatingMenuItem;
import org.floens.chan.ui.view.GridRecyclerView; import org.floens.chan.ui.view.GridRecyclerView;
import org.floens.chan.ui.view.PostImageThumbnailView; import org.floens.chan.ui.view.PostImageThumbnailView;
import org.floens.chan.utils.RecyclerUtils; import org.floens.chan.utils.RecyclerUtils;
@ -51,9 +49,7 @@ import java.util.List;
import static org.floens.chan.ui.theme.ThemeHelper.theme; import static org.floens.chan.ui.theme.ThemeHelper.theme;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
public class AlbumDownloadController extends Controller implements ToolbarMenuItem.ToolbarMenuItemCallback, View.OnClickListener { public class AlbumDownloadController extends Controller implements View.OnClickListener {
private static final int CHECK_ALL = 1;
private GridRecyclerView recyclerView; private GridRecyclerView recyclerView;
private GridLayoutManager gridLayoutManager; private GridLayoutManager gridLayoutManager;
private FloatingActionButton download; private FloatingActionButton download;
@ -79,8 +75,9 @@ public class AlbumDownloadController extends Controller implements ToolbarMenuIt
updateTitle(); updateTitle();
navigation.menu = new ToolbarMenu(context); navigation.buildMenu()
navigation.menu.addItem(new ToolbarMenuItem(context, this, CHECK_ALL, R.drawable.ic_select_all_white_24dp)); .withItem(R.drawable.ic_select_all_white_24dp, this::onCheckAllClicked)
.build();
download = view.findViewById(R.id.download); download = view.findViewById(R.id.download);
download.setOnClickListener(this); download.setOnClickListener(this);
@ -134,28 +131,21 @@ public class AlbumDownloadController extends Controller implements ToolbarMenuIt
} }
} }
@Override private void onCheckAllClicked(ToolbarMenuItem menuItem) {
public void onMenuItemClicked(ToolbarMenuItem menuItem) { RecyclerUtils.clearRecyclerCache(recyclerView);
if ((Integer) menuItem.getId() == CHECK_ALL) {
RecyclerUtils.clearRecyclerCache(recyclerView); for (int i = 0, itemsSize = items.size(); i < itemsSize; i++) {
AlbumDownloadItem item = items.get(i);
for (int i = 0, itemsSize = items.size(); i < itemsSize; i++) { if (item.checked == allChecked) {
AlbumDownloadItem item = items.get(i); item.checked = !allChecked;
if (item.checked == allChecked) { AlbumDownloadCell cell = (AlbumDownloadCell) recyclerView.findViewHolderForAdapterPosition(i);
item.checked = !allChecked; if (cell != null) {
AlbumDownloadCell cell = (AlbumDownloadCell) recyclerView.findViewHolderForAdapterPosition(i); setItemChecked(cell, item.checked, true);
if (cell != null) {
setItemChecked(cell, item.checked, true);
}
} }
} }
updateAllChecked();
updateTitle();
} }
} updateAllChecked();
updateTitle();
@Override
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
} }
public void setPostImages(Loadable loadable, List<PostImage> postImages) { public void setPostImages(Loadable loadable, List<PostImage> postImages) {

@ -26,24 +26,21 @@ import android.view.ViewGroup;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.ui.cell.AlbumViewCell; import org.floens.chan.ui.cell.AlbumViewCell;
import org.floens.chan.ui.toolbar.ToolbarMenu; import org.floens.chan.ui.toolbar.ToolbarMenuSubItem;
import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.view.FloatingMenuItem;
import org.floens.chan.ui.view.GridRecyclerView; import org.floens.chan.ui.view.GridRecyclerView;
import org.floens.chan.ui.view.PostImageThumbnailView; import org.floens.chan.ui.view.PostImageThumbnailView;
import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.ui.view.ThumbnailView;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
public class AlbumViewController extends Controller implements ImageViewerController.ImageViewerCallback, ImageViewerController.GoPostCallback, ToolbarMenuItem.ToolbarMenuItemCallback { public class AlbumViewController extends Controller implements
private static final int SAVE_ALBUM_ID = 101; ImageViewerController.ImageViewerCallback,
ImageViewerController.GoPostCallback {
private GridRecyclerView recyclerView; private GridRecyclerView recyclerView;
private GridLayoutManager gridLayoutManager; private GridLayoutManager gridLayoutManager;
@ -61,13 +58,13 @@ public class AlbumViewController extends Controller implements ImageViewerContro
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
view = inflateRes(R.layout.controller_album_view); // Navigation
navigation.buildMenu().withOverflow()
navigation.menu = new ToolbarMenu(context); .withSubItem(R.string.action_download_album, this::downloadAlbumClicked)
List<FloatingMenuItem> items = new ArrayList<>(); .build().build();
items.add(new FloatingMenuItem(SAVE_ALBUM_ID, R.string.action_download_album));
navigation.createOverflow(context, this, items);
// View setup
view = inflateRes(R.layout.controller_album_view);
recyclerView = view.findViewById(R.id.recycler_view); recyclerView = view.findViewById(R.id.recycler_view);
recyclerView.setHasFixedSize(true); recyclerView.setHasFixedSize(true);
gridLayoutManager = new GridLayoutManager(context, 3); gridLayoutManager = new GridLayoutManager(context, 3);
@ -88,19 +85,10 @@ public class AlbumViewController extends Controller implements ImageViewerContro
targetIndex = index; targetIndex = index;
} }
@Override private void downloadAlbumClicked(ToolbarMenuSubItem item) {
public void onMenuItemClicked(ToolbarMenuItem item) { AlbumDownloadController albumDownloadController = new AlbumDownloadController(context);
} albumDownloadController.setPostImages(loadable, postImages);
navigationController.pushController(albumDownloadController);
@Override
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
switch ((Integer) item.getId()) {
case SAVE_ALBUM_ID:
AlbumDownloadController albumDownloadController = new AlbumDownloadController(context);
albumDownloadController.setPostImages(loadable, postImages);
navigationController.pushController(albumDownloadController);
break;
}
} }
@Override @Override

@ -33,12 +33,10 @@ import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.presenter.ArchivePresenter; import org.floens.chan.core.presenter.ArchivePresenter;
import org.floens.chan.ui.helper.BoardHelper; import org.floens.chan.ui.helper.BoardHelper;
import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem; import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.view.CrossfadeView; import org.floens.chan.ui.view.CrossfadeView;
import org.floens.chan.ui.view.DividerItemDecoration; import org.floens.chan.ui.view.DividerItemDecoration;
import org.floens.chan.ui.view.FastScrollerHelper; import org.floens.chan.ui.view.FastScrollerHelper;
import org.floens.chan.ui.view.FloatingMenuItem;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -48,7 +46,6 @@ import javax.inject.Inject;
import static org.floens.chan.Chan.inject; import static org.floens.chan.Chan.inject;
public class ArchiveController extends Controller implements ArchivePresenter.Callback, public class ArchiveController extends Controller implements ArchivePresenter.Callback,
ToolbarMenuItem.ToolbarMenuItemCallback,
ToolbarNavigationController.ToolbarSearchCallback, ToolbarNavigationController.ToolbarSearchCallback,
SwipeRefreshLayout.OnRefreshListener { SwipeRefreshLayout.OnRefreshListener {
private static final int SEARCH_ID = 1; private static final int SEARCH_ID = 1;
@ -84,9 +81,9 @@ public class ArchiveController extends Controller implements ArchivePresenter.Ca
// Navigation // Navigation
navigation.title = context.getString(R.string.archive_title, BoardHelper.getName(board)); navigation.title = context.getString(R.string.archive_title, BoardHelper.getName(board));
ToolbarMenu menu = new ToolbarMenu(context); navigation.buildMenu()
navigation.menu = menu; .withItem(R.drawable.ic_search_white_24dp, this::searchClicked)
menu.addItem(new ToolbarMenuItem(context, this, SEARCH_ID, R.drawable.ic_search_white_24dp)); .build();
// View binding // View binding
crossfadeView = view.findViewById(R.id.crossfade); crossfadeView = view.findViewById(R.id.crossfade);
@ -112,18 +109,12 @@ public class ArchiveController extends Controller implements ArchivePresenter.Ca
presenter.create(this, board); presenter.create(this, board);
} }
@Override private void searchClicked(ToolbarMenuItem item) {
public void onMenuItemClicked(ToolbarMenuItem item) {
((ToolbarNavigationController) navigationController).showSearch(); ((ToolbarNavigationController) navigationController).showSearch();
} }
@Override
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
}
@Override @Override
public void onSearchEntered(String entered) { public void onSearchEntered(String entered) {
presenter.onSearchEntered(entered);
} }
@Override @Override

@ -17,12 +17,13 @@
*/ */
package org.floens.chan.ui.controller; package org.floens.chan.ui.controller;
import android.annotation.SuppressLint; import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.view.View; import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator; import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Board;
@ -36,9 +37,10 @@ import org.floens.chan.ui.adapter.PostsFilter;
import org.floens.chan.ui.helper.BoardHelper; import org.floens.chan.ui.helper.BoardHelper;
import org.floens.chan.ui.layout.BrowseBoardsFloatingMenu; import org.floens.chan.ui.layout.BrowseBoardsFloatingMenu;
import org.floens.chan.ui.layout.ThreadLayout; import org.floens.chan.ui.layout.ThreadLayout;
import org.floens.chan.ui.toolbar.NavigationItem;
import org.floens.chan.ui.toolbar.ToolbarMenu; import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem; import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.toolbar.ToolbarMiddleMenu; import org.floens.chan.ui.toolbar.ToolbarMenuSubItem;
import org.floens.chan.ui.view.FloatingMenu; import org.floens.chan.ui.view.FloatingMenu;
import org.floens.chan.ui.view.FloatingMenuItem; import org.floens.chan.ui.view.FloatingMenuItem;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
@ -52,18 +54,11 @@ import static org.floens.chan.Chan.inject;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class BrowseController extends ThreadController implements public class BrowseController extends ThreadController implements
ToolbarMenuItem.ToolbarMenuItemCallback,
ThreadLayout.ThreadLayoutCallback, ThreadLayout.ThreadLayoutCallback,
BrowsePresenter.Callback, BrowsePresenter.Callback,
BrowseBoardsFloatingMenu.ClickCallback { BrowseBoardsFloatingMenu.ClickCallback {
private static final int SEARCH_ID = 1; private static final int VIEW_MODE_ID = 1;
private static final int REFRESH_ID = 2; private static final int ARCHIVE_ID = 2;
private static final int REPLY_ID = 101;
private static final int SHARE_ID = 103;
private static final int VIEW_MODE_ID = 104;
private static final int ORDER_ID = 105;
private static final int OPEN_BROWSER_ID = 106;
private static final int ARCHIVE_ID = 107;
@Inject @Inject
BrowsePresenter presenter; BrowsePresenter presenter;
@ -71,12 +66,6 @@ public class BrowseController extends ThreadController implements
private ChanSettings.PostViewMode postViewMode; private ChanSettings.PostViewMode postViewMode;
private PostsFilter.Order order; private PostsFilter.Order order;
private FloatingMenuItem viewModeMenuItem;
private ToolbarMenuItem search;
private ToolbarMenuItem refresh;
private ToolbarMenuItem overflow;
private FloatingMenuItem archive;
public BrowseController(Context context) { public BrowseController(Context context) {
super(context); super(context);
} }
@ -118,11 +107,9 @@ public class BrowseController extends ThreadController implements
setupMiddleNavigation(); setupMiddleNavigation();
// Toolbar menu // Toolbar menu
ToolbarMenu menu = new ToolbarMenu(context);
navigation.menu = menu;
navigation.hasBack = false; navigation.hasBack = false;
search = menu.addItem(new ToolbarMenuItem(context, this, SEARCH_ID, R.drawable.ic_search_white_24dp)); /*search = menu.addItem(new ToolbarMenuItem(context, this, SEARCH_ID, R.drawable.ic_search_white_24dp));
refresh = menu.addItem(new ToolbarMenuItem(context, this, REFRESH_ID, R.drawable.ic_refresh_white_24dp)); refresh = menu.addItem(new ToolbarMenuItem(context, this, REFRESH_ID, R.drawable.ic_refresh_white_24dp));
// Toolbar overflow // Toolbar overflow
@ -144,73 +131,102 @@ public class BrowseController extends ThreadController implements
items.add(new FloatingMenuItem(OPEN_BROWSER_ID, R.string.action_open_browser)); items.add(new FloatingMenuItem(OPEN_BROWSER_ID, R.string.action_open_browser));
items.add(new FloatingMenuItem(SHARE_ID, R.string.action_share)); items.add(new FloatingMenuItem(SHARE_ID, R.string.action_share));
overflow.setSubMenu(new FloatingMenu(context, overflow.getView(), items)); overflow.setSubMenu(new FloatingMenu(context, overflow.getView(), items));*/
NavigationItem.MenuOverflowBuilder overflowBuilder = navigation.buildMenu()
.withItem(R.drawable.ic_search_white_24dp, this::searchClicked)
.withItem(R.drawable.ic_refresh_white_24dp, this::reloadClicked)
.withOverflow();
if (!ChanSettings.enableReplyFab.get()) {
overflowBuilder.withSubItem(R.string.action_reply, this::replyClicked);
}
overflowBuilder.withSubItem(VIEW_MODE_ID,
postViewMode == ChanSettings.PostViewMode.LIST ?
R.string.action_switch_catalog : R.string.action_switch_board,
this::viewModeClicked);
overflowBuilder
.withSubItem(ARCHIVE_ID, R.string.thread_view_archive, this::archiveClicked)
.withSubItem(R.string.action_order, this::orderClicked)
.withSubItem(R.string.action_open_browser, this::openBrowserClicked)
.withSubItem(R.string.action_share, this::shareClicked)
.build()
.build();
// Presenter // Presenter
presenter.create(this); presenter.create(this);
} }
private void setupMiddleNavigation() { private void searchClicked(ToolbarMenuItem item) {
navigation.middleMenu = new ToolbarMiddleMenu() { ThreadPresenter presenter = threadLayout.getPresenter();
@SuppressLint("InflateParams") if (presenter.isBound()) {
@Override View refreshView = item.getView();
public void show(View anchor) { refreshView.setScaleX(1f);
BrowseBoardsFloatingMenu boardsFloatingMenu = new BrowseBoardsFloatingMenu(context); refreshView.setScaleY(1f);
boardsFloatingMenu.show(view, anchor, BrowseController.this, refreshView.animate()
presenter.currentBoard()); .scaleX(10f)
} .scaleY(10f)
}; .setDuration(500)
.setInterpolator(new AccelerateInterpolator(2f))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
refreshView.setScaleX(1f);
refreshView.setScaleY(1f);
}
});
((ToolbarNavigationController) navigationController).showSearch();
}
} }
@Override private void reloadClicked(ToolbarMenuItem item) {
public void onMenuItemClicked(ToolbarMenuItem item) {
ThreadPresenter presenter = threadLayout.getPresenter(); ThreadPresenter presenter = threadLayout.getPresenter();
if (presenter.isBound()) {
switch ((Integer) item.getId()) { presenter.requestData();
case SEARCH_ID:
if (presenter.isBound()) { // Give the rotation menu item view a spin.
((ToolbarNavigationController) navigationController).showSearch(); View refreshView = item.getView();
} refreshView.setRotation(0f);
break; refreshView.animate()
case REFRESH_ID: .rotation(360f)
if (presenter.isBound()) { .setDuration(500)
presenter.requestData(); .setInterpolator(new DecelerateInterpolator(2f));
ImageView refreshView = refresh.getView();
refreshView.setRotation(0f);
refreshView.animate()
.rotation(360f)
.setDuration(500)
.setInterpolator(new DecelerateInterpolator(2f));
}
break;
} }
} }
@Override private void replyClicked(ToolbarMenuSubItem item) {
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) { threadLayout.openReply(true);
final ThreadPresenter presenter = threadLayout.getPresenter(); }
Integer id = (Integer) item.getId(); private void viewModeClicked(ToolbarMenuSubItem item) {
switch (id) { handleViewMode(item);
case REPLY_ID: }
handleReply();
break; private void archiveClicked(ToolbarMenuSubItem item) {
case SHARE_ID: openArchive();
case OPEN_BROWSER_ID: }
handleShareAndOpenInBrowser(presenter, id);
break; private void orderClicked(ToolbarMenuSubItem item) {
case VIEW_MODE_ID: handleOrder(threadLayout.getPresenter());
handleViewMode(); }
break; private void openBrowserClicked(ToolbarMenuSubItem item) {
case ORDER_ID: handleShareAndOpenInBrowser(threadLayout.getPresenter(), false);
handleOrder(presenter); }
break; private void shareClicked(ToolbarMenuSubItem item) {
case ARCHIVE_ID: handleShareAndOpenInBrowser(threadLayout.getPresenter(), true);
openArchive(); }
break;
} private void setupMiddleNavigation() {
navigation.setMiddleMenu(anchor -> {
BrowseBoardsFloatingMenu boardsFloatingMenu = new BrowseBoardsFloatingMenu(context);
boardsFloatingMenu.show(view, anchor, BrowseController.this,
presenter.currentBoard());
});
} }
@Override @Override
@ -239,16 +255,12 @@ public class BrowseController extends ThreadController implements
} }
} }
private void handleReply() { private void handleShareAndOpenInBrowser(ThreadPresenter presenter, boolean share) {
threadLayout.openReply(true);
}
private void handleShareAndOpenInBrowser(ThreadPresenter presenter, Integer id) {
if (presenter.isBound()) { if (presenter.isBound()) {
Loadable loadable = presenter.getLoadable(); Loadable loadable = presenter.getLoadable();
String link = loadable.site.resolvable().desktopUrl(loadable, null); String link = loadable.site.resolvable().desktopUrl(loadable, null);
if (id == SHARE_ID) { if (share) {
AndroidUtils.shareLink(link); AndroidUtils.shareLink(link);
} else { } else {
AndroidUtils.openLinkInBrowser((Activity) context, link); AndroidUtils.openLinkInBrowser((Activity) context, link);
@ -256,7 +268,7 @@ public class BrowseController extends ThreadController implements
} }
} }
private void handleViewMode() { private void handleViewMode(ToolbarMenuSubItem item) {
if (postViewMode == ChanSettings.PostViewMode.LIST) { if (postViewMode == ChanSettings.PostViewMode.LIST) {
postViewMode = ChanSettings.PostViewMode.CARD; postViewMode = ChanSettings.PostViewMode.CARD;
} else { } else {
@ -267,7 +279,7 @@ public class BrowseController extends ThreadController implements
int viewModeText = postViewMode == ChanSettings.PostViewMode.LIST ? int viewModeText = postViewMode == ChanSettings.PostViewMode.LIST ?
R.string.action_switch_catalog : R.string.action_switch_board; R.string.action_switch_catalog : R.string.action_switch_board;
viewModeMenuItem.setText(context.getString(viewModeText)); item.text = context.getString(viewModeText);
threadLayout.setPostViewMode(postViewMode); threadLayout.setPostViewMode(postViewMode);
} }
@ -302,6 +314,7 @@ public class BrowseController extends ThreadController implements
items.add(new FloatingMenuItem(order, name)); items.add(new FloatingMenuItem(order, name));
} }
ToolbarMenuItem overflow = navigation.findItem(ToolbarMenu.OVERFLOW_ID);
FloatingMenu menu = new FloatingMenu(context, overflow.getView(), items); FloatingMenu menu = new FloatingMenu(context, overflow.getView(), items);
menu.setCallback(new FloatingMenu.FloatingMenuCallback() { menu.setCallback(new FloatingMenu.FloatingMenuCallback() {
@Override @Override
@ -347,7 +360,8 @@ public class BrowseController extends ThreadController implements
@Override @Override
public void showArchiveOption(boolean show) { public void showArchiveOption(boolean show) {
archive.setEnabled(show); ToolbarMenuSubItem archive = navigation.findSubItem(ARCHIVE_ID);
archive.enabled = show;
} }
@Override @Override

@ -38,9 +38,7 @@ import org.floens.chan.core.manager.FilterType;
import org.floens.chan.core.model.orm.Filter; import org.floens.chan.core.model.orm.Filter;
import org.floens.chan.ui.helper.RefreshUIMessage; import org.floens.chan.ui.helper.RefreshUIMessage;
import org.floens.chan.ui.layout.FilterLayout; import org.floens.chan.ui.layout.FilterLayout;
import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem; import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.view.FloatingMenuItem;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -55,10 +53,9 @@ import static org.floens.chan.ui.theme.ThemeHelper.theme;
import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.getAttrColor;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class FiltersController extends Controller implements ToolbarMenuItem.ToolbarMenuItemCallback, ToolbarNavigationController.ToolbarSearchCallback, View.OnClickListener { public class FiltersController extends Controller implements
private static final int SEARCH_ID = 1; ToolbarNavigationController.ToolbarSearchCallback,
private static final int CLEAR_ID = 101; View.OnClickListener {
@Inject @Inject
DatabaseManager databaseManager; DatabaseManager databaseManager;
@ -109,8 +106,10 @@ public class FiltersController extends Controller implements ToolbarMenuItem.Too
inject(this); inject(this);
navigation.setTitle(R.string.filters_screen); navigation.setTitle(R.string.filters_screen);
navigation.menu = new ToolbarMenu(context);
navigation.menu.addItem(new ToolbarMenuItem(context, this, SEARCH_ID, R.drawable.ic_search_white_24dp)); navigation.buildMenu()
.withItem(R.drawable.ic_search_white_24dp, this::searchClicked)
.build();
view = inflateRes(R.layout.controller_filters); view = inflateRes(R.layout.controller_filters);
@ -134,15 +133,8 @@ public class FiltersController extends Controller implements ToolbarMenuItem.Too
} }
} }
@Override private void searchClicked(ToolbarMenuItem item) {
public void onMenuItemClicked(ToolbarMenuItem item) { ((ToolbarNavigationController) navigationController).showSearch();
if ((Integer) item.getId() == SEARCH_ID) {
((ToolbarNavigationController) navigationController).showSearch();
}
}
@Override
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
} }
public void showFilterDialog(final Filter filter) { public void showFilterDialog(final Filter filter) {

@ -18,7 +18,6 @@
package org.floens.chan.ui.controller; package org.floens.chan.ui.controller;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@ -41,10 +40,9 @@ import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.History; import org.floens.chan.core.model.orm.History;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.helper.HintPopup; import org.floens.chan.ui.helper.HintPopup;
import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem; import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.toolbar.ToolbarMenuSubItem;
import org.floens.chan.ui.view.CrossfadeView; import org.floens.chan.ui.view.CrossfadeView;
import org.floens.chan.ui.view.FloatingMenuItem;
import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.ui.view.ThumbnailView;
import java.util.ArrayList; import java.util.ArrayList;
@ -57,10 +55,10 @@ import static org.floens.chan.Chan.inject;
import static org.floens.chan.ui.theme.ThemeHelper.theme; import static org.floens.chan.ui.theme.ThemeHelper.theme;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
public class HistoryController extends Controller implements CompoundButton.OnCheckedChangeListener, ToolbarMenuItem.ToolbarMenuItemCallback, ToolbarNavigationController.ToolbarSearchCallback { public class HistoryController extends Controller implements
CompoundButton.OnCheckedChangeListener,
ToolbarNavigationController.ToolbarSearchCallback {
private static final int SEARCH_ID = 1; private static final int SEARCH_ID = 1;
private static final int CLEAR_ID = 101;
private static final int SAVED_REPLY_CLEAR_ID = 102;
@Inject @Inject
DatabaseManager databaseManager; DatabaseManager databaseManager;
@ -87,19 +85,20 @@ public class HistoryController extends Controller implements CompoundButton.OnCh
databaseHistoryManager = databaseManager.getDatabaseHistoryManager(); databaseHistoryManager = databaseManager.getDatabaseHistoryManager();
databaseSavedReplyManager = databaseManager.getDatabaseSavedReplyManager(); databaseSavedReplyManager = databaseManager.getDatabaseSavedReplyManager();
// Navigation
navigation.setTitle(R.string.history_screen); navigation.setTitle(R.string.history_screen);
List<FloatingMenuItem> items = new ArrayList<>();
items.add(new FloatingMenuItem(CLEAR_ID, R.string.history_clear)); navigation.buildMenu()
items.add(new FloatingMenuItem(SAVED_REPLY_CLEAR_ID, R.string.saved_reply_clear)); .withItem(R.drawable.ic_search_white_24dp, this::searchClicked)
navigation.menu = new ToolbarMenu(context); .withOverflow()
navigation.menu.addItem(new ToolbarMenuItem(context, this, SEARCH_ID, R.drawable.ic_search_white_24dp)); .withSubItem(R.string.history_clear, this::clearHistoryClicked)
ToolbarMenuItem overflow = navigation.createOverflow(context, this, items); .withSubItem(R.string.saved_reply_clear, this::clearSavedReplyClicked)
overflow.getSubMenu().setPopupWidth(dp(4 * 56)); .build().build();
SwitchCompat historyEnabledSwitch = new SwitchCompat(context); SwitchCompat historyEnabledSwitch = new SwitchCompat(context);
historyEnabledSwitch.setChecked(ChanSettings.historyEnabled.get()); historyEnabledSwitch.setChecked(ChanSettings.historyEnabled.get());
historyEnabledSwitch.setOnCheckedChangeListener(this); historyEnabledSwitch.setOnCheckedChangeListener(this);
navigation.rightView = historyEnabledSwitch; navigation.setRightView(historyEnabledSwitch);
view = inflateRes(R.layout.controller_history); view = inflateRes(R.layout.controller_history);
crossfade = view.findViewById(R.id.crossfade); crossfade = view.findViewById(R.id.crossfade);
@ -116,42 +115,28 @@ public class HistoryController extends Controller implements CompoundButton.OnCh
} }
} }
@Override private void searchClicked(ToolbarMenuItem item) {
public void onMenuItemClicked(ToolbarMenuItem item) { ((ToolbarNavigationController) navigationController).showSearch();
if ((Integer) item.getId() == SEARCH_ID) {
((ToolbarNavigationController) navigationController).showSearch();
}
} }
@Override private void clearHistoryClicked(ToolbarMenuSubItem item) {
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) { new AlertDialog.Builder(context)
switch ((Integer) item.getId()) { .setTitle(R.string.history_clear_confirm)
case CLEAR_ID: .setNegativeButton(R.string.cancel, null)
new AlertDialog.Builder(context) .setPositiveButton(R.string.history_clear_confirm_button, (dialog, which) -> {
.setTitle(R.string.history_clear_confirm) databaseManager.runTaskAsync(databaseHistoryManager.clearHistory());
.setNegativeButton(R.string.cancel, null) adapter.load();
.setPositiveButton(R.string.history_clear_confirm_button, new DialogInterface.OnClickListener() { })
@Override .show();
public void onClick(DialogInterface dialog, int which) { }
databaseManager.runTaskAsync(databaseHistoryManager.clearHistory());
adapter.load(); private void clearSavedReplyClicked(ToolbarMenuSubItem item) {
} new AlertDialog.Builder(context)
}) .setTitle(R.string.saved_reply_clear_confirm)
.show(); .setNegativeButton(R.string.cancel, null)
break; .setPositiveButton(R.string.saved_reply_clear_confirm_button, (dialog, which) ->
case SAVED_REPLY_CLEAR_ID: databaseManager.runTaskAsync(databaseSavedReplyManager.clearSavedReplies()))
new AlertDialog.Builder(context) .show();
.setTitle(R.string.saved_reply_clear_confirm)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.saved_reply_clear_confirm_button, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
databaseManager.runTaskAsync(databaseSavedReplyManager.clearSavedReplies());
}
})
.show();
break;
}
} }
@Override @Override

@ -39,7 +39,6 @@ import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator; import android.view.animation.DecelerateInterpolator;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.Toast; import android.widget.Toast;
import com.android.volley.VolleyError; import com.android.volley.VolleyError;
@ -55,9 +54,11 @@ import org.floens.chan.core.saver.ImageSaver;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.ImageSearch; import org.floens.chan.core.site.ImageSearch;
import org.floens.chan.ui.adapter.ImageViewerAdapter; import org.floens.chan.ui.adapter.ImageViewerAdapter;
import org.floens.chan.ui.toolbar.NavigationItem;
import org.floens.chan.ui.toolbar.Toolbar; import org.floens.chan.ui.toolbar.Toolbar;
import org.floens.chan.ui.toolbar.ToolbarMenu; import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem; import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.toolbar.ToolbarMenuSubItem;
import org.floens.chan.ui.view.CustomScaleImageView; import org.floens.chan.ui.view.CustomScaleImageView;
import org.floens.chan.ui.view.FloatingMenu; import org.floens.chan.ui.view.FloatingMenu;
import org.floens.chan.ui.view.FloatingMenuItem; import org.floens.chan.ui.view.FloatingMenuItem;
@ -81,18 +82,12 @@ import static org.floens.chan.Chan.inject;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class ImageViewerController extends Controller implements ImageViewerPresenter.Callback, ToolbarMenuItem.ToolbarMenuItemCallback { public class ImageViewerController extends Controller implements ImageViewerPresenter.Callback {
private static final String TAG = "ImageViewerController"; private static final String TAG = "ImageViewerController";
private static final int TRANSITION_DURATION = 300; private static final int TRANSITION_DURATION = 300;
private static final float TRANSITION_FINAL_ALPHA = 0.85f; private static final float TRANSITION_FINAL_ALPHA = 0.85f;
private static final int GO_POST_ID = 1; private static final int VOLUME_ID = 1;
private static final int VOLUME_ID = 3;
private static final int SAVE_ID = 2;
private static final int OPEN_BROWSER_ID = 103;
private static final int SHARE_ID = 104;
private static final int SEARCH_ID = 105;
private static final int SAVE_ALBUM = 106;
@Inject @Inject
ImageLoader imageLoader; ImageLoader imageLoader;
@ -110,9 +105,6 @@ public class ImageViewerController extends Controller implements ImageViewerPres
private OptionalSwipeViewPager pager; private OptionalSwipeViewPager pager;
private LoadingBar loadingBar; private LoadingBar loadingBar;
private ToolbarMenuItem overflowMenuItem;
private ToolbarMenuItem volumeMenuItem;
public ImageViewerController(Context context, Toolbar toolbar) { public ImageViewerController(Context context, Toolbar toolbar) {
super(context); super(context);
inject(this); inject(this);
@ -126,24 +118,27 @@ public class ImageViewerController extends Controller implements ImageViewerPres
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // Navigation
navigation.subtitle = "0"; navigation.subtitle = "0";
navigation.menu = new ToolbarMenu(context);
NavigationItem.MenuBuilder menuBuilder = navigation.buildMenu();
if (goPostCallback != null) { if (goPostCallback != null) {
navigation.menu.addItem(new ToolbarMenuItem(context, this, GO_POST_ID, R.drawable.ic_subdirectory_arrow_left_white_24dp)); menuBuilder.withItem(R.drawable.ic_subdirectory_arrow_left_white_24dp, this::goPostClicked);
} }
volumeMenuItem = navigation.menu.addItem(new ToolbarMenuItem(context, menuBuilder.withItem(VOLUME_ID, R.drawable.ic_volume_off_white_24dp, this::volumeClicked);
this, VOLUME_ID, R.drawable.ic_volume_off_white_24dp)); menuBuilder.withItem(R.drawable.ic_file_download_white_24dp, this::saveClicked);
navigation.menu.addItem(new ToolbarMenuItem(context, this, SAVE_ID, R.drawable.ic_file_download_white_24dp));
List<FloatingMenuItem> items = new ArrayList<>(); NavigationItem.MenuOverflowBuilder overflowBuilder = menuBuilder.withOverflow();
items.add(new FloatingMenuItem(OPEN_BROWSER_ID, R.string.action_open_browser)); overflowBuilder.withSubItem(R.string.action_open_browser, this::openBrowserClicked);
items.add(new FloatingMenuItem(SHARE_ID, R.string.action_share)); overflowBuilder.withSubItem(R.string.action_share, this::shareClicked);
items.add(new FloatingMenuItem(SEARCH_ID, R.string.action_search_image)); overflowBuilder.withSubItem(R.string.action_search_image, this::searchClicked);
items.add(new FloatingMenuItem(SAVE_ALBUM, R.string.action_download_album)); overflowBuilder.withSubItem(R.string.action_download_album, this::downloadAlbumClicked);
overflowMenuItem = navigation.createOverflow(context, this, items);
overflowBuilder.build().build();
// View setup
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
view = inflateRes(R.layout.controller_image_viewer); view = inflateRes(R.layout.controller_image_viewer);
previewImage = view.findViewById(R.id.preview_image); previewImage = view.findViewById(R.id.preview_image);
@ -164,84 +159,56 @@ public class ImageViewerController extends Controller implements ImageViewerPres
}); });
} }
@Override private void goPostClicked(ToolbarMenuItem item) {
public void onDestroy() { PostImage postImage = presenter.getCurrentPostImage();
super.onDestroy(); ImageViewerCallback imageViewerCallback = goPostCallback.goToPost(postImage);
if (imageViewerCallback != null) {
// hax: we need to wait for the recyclerview to do a layout before we know
// where the new thumbnails are to get the bounds from to animate to
this.imageViewerCallback = imageViewerCallback;
AndroidUtils.waitForLayout(view, view -> {
presenter.onExit();
return false;
});
} else {
presenter.onExit();
}
}
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); private void volumeClicked(ToolbarMenuItem item) {
presenter.onVolumeClicked();
} }
@Override private void saveClicked(ToolbarMenuItem item) {
public void onMenuItemClicked(ToolbarMenuItem item) { saveShare(false, presenter.getCurrentPostImage());
switch ((Integer) item.getId()) {
case GO_POST_ID:
PostImage postImage = presenter.getCurrentPostImage();
ImageViewerCallback imageViewerCallback = goPostCallback.goToPost(postImage);
if (imageViewerCallback != null) {
// hax: we need to wait for the recyclerview to do a layout before we know
// where the new thumbnails are to get the bounds from to animate to
this.imageViewerCallback = imageViewerCallback;
AndroidUtils.waitForLayout(view, new AndroidUtils.OnMeasuredCallback() {
@Override
public boolean onMeasured(View view) {
presenter.onExit();
return false;
}
});
} else {
presenter.onExit();
}
break;
case SAVE_ID:
saveShare(false, presenter.getCurrentPostImage());
break;
case VOLUME_ID:
presenter.onVolumeClicked();
break;
}
} }
@Override private void openBrowserClicked(ToolbarMenuSubItem item) {
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
PostImage postImage = presenter.getCurrentPostImage(); PostImage postImage = presenter.getCurrentPostImage();
switch ((Integer) item.getId()) { AndroidUtils.openLinkInBrowser((Activity) context, postImage.imageUrl.toString());
case SHARE_ID: }
saveShare(true, postImage);
break;
case OPEN_BROWSER_ID:
AndroidUtils.openLinkInBrowser((Activity) context, postImage.imageUrl.toString());
break;
case SEARCH_ID:
List<FloatingMenuItem> items = new ArrayList<>();
for (ImageSearch imageSearch : ImageSearch.engines) {
items.add(new FloatingMenuItem(imageSearch.getId(), imageSearch.getName()));
}
FloatingMenu menu = new FloatingMenu(context, overflowMenuItem.getView(), items);
menu.setCallback(new FloatingMenu.FloatingMenuCallback() {
@Override
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
for (ImageSearch imageSearch : ImageSearch.engines) {
if (((Integer) item.getId()) == imageSearch.getId()) {
final HttpUrl searchImageUrl = getSearchImageUrl(presenter.getCurrentPostImage());
AndroidUtils.openLinkInBrowser((Activity) context, imageSearch.getUrl(searchImageUrl.toString()));
break;
}
}
}
@Override private void shareClicked(ToolbarMenuSubItem item) {
public void onFloatingMenuDismissed(FloatingMenu menu) { PostImage postImage = presenter.getCurrentPostImage();
} saveShare(true, postImage);
}); }
menu.show();
break; private void searchClicked(ToolbarMenuSubItem item) {
case SAVE_ALBUM: showImageSearchOptions();
List<PostImage> all = presenter.getAllPostImages(); }
AlbumDownloadController albumDownloadController = new AlbumDownloadController(context);
albumDownloadController.setPostImages(presenter.getLoadable(), all); private void downloadAlbumClicked(ToolbarMenuSubItem item) {
navigationController.pushController(albumDownloadController); List<PostImage> all = presenter.getAllPostImages();
break; AlbumDownloadController albumDownloadController = new AlbumDownloadController(context);
} albumDownloadController.setPostImages(presenter.getLoadable(), all);
navigationController.pushController(albumDownloadController);
}
@Override
public void onDestroy() {
super.onDestroy();
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} }
private void saveShare(boolean share, PostImage postImage) { private void saveShare(boolean share, PostImage postImage) {
@ -353,10 +320,37 @@ public class ImageViewerController extends Controller implements ImageViewerPres
@Override @Override
public void showVolumeMenuItem(boolean show, boolean muted) { public void showVolumeMenuItem(boolean show, boolean muted) {
ImageView view = volumeMenuItem.getView(); ToolbarMenuItem volumeMenuItem = navigation.findItem(VOLUME_ID);
view.setVisibility(show ? View.VISIBLE : View.GONE); volumeMenuItem.setVisible(show);
view.setImageResource(muted ? volumeMenuItem.setImage(
R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp); muted ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp);
}
private void showImageSearchOptions() {
// TODO: move to presenter
List<FloatingMenuItem> items = new ArrayList<>();
for (ImageSearch imageSearch : ImageSearch.engines) {
items.add(new FloatingMenuItem(imageSearch.getId(), imageSearch.getName()));
}
ToolbarMenuItem overflowMenuItem = navigation.findItem(ToolbarMenu.OVERFLOW_ID);
FloatingMenu menu = new FloatingMenu(context, overflowMenuItem.getView(), items);
menu.setCallback(new FloatingMenu.FloatingMenuCallback() {
@Override
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
for (ImageSearch imageSearch : ImageSearch.engines) {
if (((Integer) item.getId()) == imageSearch.getId()) {
final HttpUrl searchImageUrl = getSearchImageUrl(presenter.getCurrentPostImage());
AndroidUtils.openLinkInBrowser((Activity) context, imageSearch.getUrl(searchImageUrl.toString()));
break;
}
}
}
@Override
public void onFloatingMenuDismissed(FloatingMenu menu) {
}
});
menu.show();
} }
public void startPreviewInTransition(PostImage postImage) { public void startPreviewInTransition(PostImage postImage) {

@ -27,25 +27,19 @@ import android.widget.Toast;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.ui.toolbar.ToolbarMenu; import org.floens.chan.ui.toolbar.ToolbarMenuSubItem;
import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.view.FloatingMenuItem;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils; import org.floens.chan.utils.IOUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.getAttrColor;
public class LogsController extends Controller implements ToolbarMenuItem.ToolbarMenuItemCallback { public class LogsController extends Controller {
private static final String TAG = "LogsController"; private static final String TAG = "LogsController";
private static final int COPY_ID = 101;
private TextView logTextView; private TextView logTextView;
private String logText; private String logText;
@ -60,10 +54,9 @@ public class LogsController extends Controller implements ToolbarMenuItem.Toolba
navigation.setTitle(org.floens.chan.R.string.settings_logs_screen); navigation.setTitle(org.floens.chan.R.string.settings_logs_screen);
navigation.menu = new ToolbarMenu(context); navigation.buildMenu().withOverflow()
List<FloatingMenuItem> items = new ArrayList<>(); .withSubItem(R.string.settings_logs_copy, this::copyLogsClicked)
items.add(new FloatingMenuItem(COPY_ID, R.string.settings_logs_copy)); .build().build();
navigation.createOverflow(context, this, items);
ScrollView container = new ScrollView(context); ScrollView container = new ScrollView(context);
container.setBackgroundColor(getAttrColor(context, org.floens.chan.R.attr.backcolor)); container.setBackgroundColor(getAttrColor(context, org.floens.chan.R.attr.backcolor));
@ -75,18 +68,11 @@ public class LogsController extends Controller implements ToolbarMenuItem.Toolba
loadLogs(); loadLogs();
} }
@Override private void copyLogsClicked(ToolbarMenuSubItem item) {
public void onMenuItemClicked(ToolbarMenuItem item) { ClipboardManager clipboard = (ClipboardManager) AndroidUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE);
} ClipData clip = ClipData.newPlainText("Logs", logText);
clipboard.setPrimaryClip(clip);
@Override Toast.makeText(context, R.string.settings_logs_copied_to_clipboard, Toast.LENGTH_SHORT).show();
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
if ((int) item.getId() == COPY_ID) {
ClipboardManager clipboard = (ClipboardManager) AndroidUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("Logs", logText);
clipboard.setPrimaryClip(clip);
Toast.makeText(context, R.string.settings_logs_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
} }
private void loadLogs() { private void loadLogs() {

@ -44,11 +44,8 @@ import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteIcon; import org.floens.chan.core.site.SiteIcon;
import org.floens.chan.ui.helper.HintPopup; import org.floens.chan.ui.helper.HintPopup;
import org.floens.chan.ui.layout.SiteAddLayout; import org.floens.chan.ui.layout.SiteAddLayout;
import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.view.CrossfadeView; import org.floens.chan.ui.view.CrossfadeView;
import org.floens.chan.ui.view.DividerItemDecoration; import org.floens.chan.ui.view.DividerItemDecoration;
import org.floens.chan.ui.view.FloatingMenuItem;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -62,15 +59,11 @@ import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground;
public class SitesSetupController extends StyledToolbarNavigationController implements public class SitesSetupController extends StyledToolbarNavigationController implements
SitesSetupPresenter.Callback, SitesSetupPresenter.Callback,
ToolbarMenuItem.ToolbarMenuItemCallback,
View.OnClickListener { View.OnClickListener {
private static final int DONE_ID = 1;
@Inject @Inject
SitesSetupPresenter presenter; SitesSetupPresenter presenter;
private ToolbarMenuItem doneMenuItem;
private CrossfadeView crossfadeView; private CrossfadeView crossfadeView;
private RecyclerView sitesRecyclerview; private RecyclerView sitesRecyclerview;
private FloatingActionButton addButton; private FloatingActionButton addButton;
@ -152,21 +145,12 @@ public class SitesSetupController extends StyledToolbarNavigationController impl
public void showDoneCheckmark() { public void showDoneCheckmark() {
navigation.swipeable = false; navigation.swipeable = false;
navigation.menu = new ToolbarMenu(context);
doneMenuItem = navigation.menu.addItem(
new ToolbarMenuItem(context, this, DONE_ID, 0, R.drawable.ic_done_white_24dp));
doneMenuItem.getView().setAlpha(0f);
}
@Override navigation.buildMenu()
public void onMenuItemClicked(ToolbarMenuItem item) { .withItem(R.drawable.ic_done_white_24dp, (item) -> presenter.onDoneClicked())
if ((Integer) item.getId() == DONE_ID) { .build();
presenter.onDoneClicked();
}
}
@Override // doneMenuItem.getView().setAlpha(0f);
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
} }
@Override @Override
@ -244,9 +228,9 @@ public class SitesSetupController extends StyledToolbarNavigationController impl
@Override @Override
public void setNextAllowed(boolean nextAllowed) { public void setNextAllowed(boolean nextAllowed) {
if (doneMenuItem != null) { // if (doneMenuItem != null) {
doneMenuItem.getView().animate().alpha(nextAllowed ? 1f : 0f).start(); // doneMenuItem.getView().animate().alpha(nextAllowed ? 1f : 0f).start();
} // }
if (!nextAllowed) { if (!nextAllowed) {
navigation.swipeable = false; navigation.swipeable = false;
} }

@ -231,7 +231,7 @@ public abstract class ThreadController extends Controller implements
@Override @Override
public Toolbar getToolbar() { public Toolbar getToolbar() {
if (navigationController instanceof ToolbarNavigationController) { if (navigationController instanceof ToolbarNavigationController) {
return ((ToolbarNavigationController) navigationController).getToolbar(); return navigationController.getToolbar();
} else { } else {
return null; return null;
} }

@ -35,6 +35,7 @@ public abstract class ToolbarNavigationController extends NavigationController i
super(context); super(context);
} }
@Override
public Toolbar getToolbar() { public Toolbar getToolbar() {
return toolbar; return toolbar;
} }
@ -61,7 +62,7 @@ public abstract class ToolbarNavigationController extends NavigationController i
toolbar.processScrollCollapse(Toolbar.TOOLBAR_COLLAPSE_SHOW, true); toolbar.processScrollCollapse(Toolbar.TOOLBAR_COLLAPSE_SHOW, true);
toolbar.beginTransition(to.navigation); toolbar.beginTransition(to.navigation);
toolbar.transitionProgress(0f, false); toolbar.transitionProgress(0f);
return true; return true;
} }
@ -70,7 +71,7 @@ public abstract class ToolbarNavigationController extends NavigationController i
public void swipeTransitionProgress(float progress) { public void swipeTransitionProgress(float progress) {
super.swipeTransitionProgress(progress); super.swipeTransitionProgress(progress);
toolbar.transitionProgress(progress, false); toolbar.transitionProgress(progress);
} }
@Override @Override

@ -20,7 +20,9 @@ package org.floens.chan.ui.controller;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.view.View;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
@ -32,36 +34,25 @@ import org.floens.chan.core.presenter.ThreadPresenter;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.helper.HintPopup; import org.floens.chan.ui.helper.HintPopup;
import org.floens.chan.ui.layout.ThreadLayout; import org.floens.chan.ui.layout.ThreadLayout;
import org.floens.chan.ui.toolbar.NavigationItem;
import org.floens.chan.ui.toolbar.ToolbarMenu; import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem; import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.view.FloatingMenuItem; import org.floens.chan.ui.toolbar.ToolbarMenuSubItem;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import static org.floens.chan.Chan.inject; import static org.floens.chan.Chan.inject;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.getAttrColor;
public class ViewThreadController extends ThreadController implements ThreadLayout.ThreadLayoutCallback, ToolbarMenuItem.ToolbarMenuItemCallback { public class ViewThreadController extends ThreadController implements ThreadLayout.ThreadLayoutCallback {
private static final int ALBUM_ID = 1; private static final int PIN_ID = 1;
private static final int PIN_ID = 2;
private static final int REPLY_ID = 101;
private static final int REFRESH_ID = 103;
private static final int SEARCH_ID = 104;
private static final int SHARE_ID = 105;
private static final int UP_ID = 106;
private static final int DOWN_ID = 107;
private static final int OPEN_BROWSER_ID = 108;
@Inject @Inject
WatchManager watchManager; WatchManager watchManager;
private ToolbarMenuItem pinItem; private boolean pinItemPinned = false;
private ToolbarMenuItem overflowItem;
private Loadable loadable; private Loadable loadable;
public ViewThreadController(Context context) { public ViewThreadController(Context context) {
@ -82,25 +73,81 @@ public class ViewThreadController extends ThreadController implements ThreadLayo
view.setBackgroundColor(getAttrColor(context, R.attr.backcolor)); view.setBackgroundColor(getAttrColor(context, R.attr.backcolor));
navigation.hasDrawer = true; navigation.hasDrawer = true;
navigation.menu = new ToolbarMenu(context);
navigation.menu.addItem(new ToolbarMenuItem(context, this, ALBUM_ID, R.drawable.ic_image_white_24dp)); NavigationItem.MenuOverflowBuilder menuOverflowBuilder = navigation.buildMenu()
pinItem = navigation.menu.addItem(new ToolbarMenuItem(context, this, PIN_ID, R.drawable.ic_bookmark_outline_white_24dp)); .withItem(R.drawable.ic_image_white_24dp, this::albumClicked)
List<FloatingMenuItem> items = new ArrayList<>(); .withItem(PIN_ID, R.drawable.ic_bookmark_outline_white_24dp, this::pinClicked)
.withOverflow();
if (!ChanSettings.enableReplyFab.get()) { if (!ChanSettings.enableReplyFab.get()) {
items.add(new FloatingMenuItem(REPLY_ID, context.getString(R.string.action_reply))); menuOverflowBuilder.withSubItem(R.string.action_reply, this::replyClicked);
} }
items.add(new FloatingMenuItem(SEARCH_ID, R.string.action_search));
items.add(new FloatingMenuItem(REFRESH_ID, R.string.action_reload)); menuOverflowBuilder
items.add(new FloatingMenuItem(OPEN_BROWSER_ID, R.string.action_open_browser)); .withSubItem(R.string.action_search, this::searchClicked)
items.add(new FloatingMenuItem(SHARE_ID, R.string.action_share)); .withSubItem(R.string.action_reload, this::reloadClicked)
items.add(new FloatingMenuItem(UP_ID, R.string.action_up)); .withSubItem(R.string.action_open_browser, this::openBrowserClicked)
items.add(new FloatingMenuItem(DOWN_ID, R.string.action_down)); .withSubItem(R.string.action_share, this::shareClicked)
overflowItem = navigation.createOverflow(context, this, items); .withSubItem(R.string.action_up, this::upClicked)
.withSubItem(R.string.action_down, this::downClicked)
.build()
.build();
loadThread(loadable); loadThread(loadable);
} }
private void albumClicked(ToolbarMenuItem item) {
threadLayout.getPresenter().showAlbum();
}
private void pinClicked(ToolbarMenuItem item) {
threadLayout.getPresenter().pin();
setPinIconState(true);
updateDrawerHighlighting(loadable);
}
private void searchClicked(ToolbarMenuSubItem item) {
((ToolbarNavigationController) navigationController).showSearch();
}
private void replyClicked(ToolbarMenuSubItem item) {
threadLayout.openReply(true);
}
private void reloadClicked(ToolbarMenuSubItem item) {
threadLayout.getPresenter().requestData();
}
private void openBrowserClicked(ToolbarMenuSubItem item) {
Loadable loadable = threadLayout.getPresenter().getLoadable();
String link = loadable.site.resolvable().desktopUrl(loadable, null);
AndroidUtils.openLinkInBrowser((Activity) context, link);
}
private void shareClicked(ToolbarMenuSubItem item) {
Loadable loadable = threadLayout.getPresenter().getLoadable();
String link = loadable.site.resolvable().desktopUrl(loadable, null);
AndroidUtils.shareLink(link);
}
private void upClicked(ToolbarMenuSubItem item) {
threadLayout.getPresenter().scrollTo(0, false);
}
private void downClicked(ToolbarMenuSubItem item) {
threadLayout.getPresenter().scrollTo(-1, false);
}
@Override
public void onShow() {
super.onShow();
ThreadPresenter presenter = threadLayout.getPresenter();
if (presenter != null) {
setPinIconState(false);
}
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
@ -114,15 +161,15 @@ public class ViewThreadController extends ThreadController implements ThreadLayo
} }
public void onEvent(WatchManager.PinAddedMessage message) { public void onEvent(WatchManager.PinAddedMessage message) {
setPinIconState(); setPinIconState(true);
} }
public void onEvent(WatchManager.PinRemovedMessage message) { public void onEvent(WatchManager.PinRemovedMessage message) {
setPinIconState(); setPinIconState(true);
} }
public void onEvent(WatchManager.PinChangedMessage message) { public void onEvent(WatchManager.PinChangedMessage message) {
setPinIconState(); setPinIconState(false);
// Update title // Update title
if (message.pin.loadable == loadable) { if (message.pin.loadable == loadable) {
onShowPosts(); onShowPosts();
@ -151,16 +198,18 @@ public class ViewThreadController extends ThreadController implements ThreadLayo
this.loadable = presenter.getLoadable(); this.loadable = presenter.getLoadable();
navigation.title = loadable.title; navigation.title = loadable.title;
((ToolbarNavigationController) navigationController).toolbar.updateTitle(navigation); ((ToolbarNavigationController) navigationController).toolbar.updateTitle(navigation);
setPinIconState(presenter.isPinned()); setPinIconState(false);
updateDrawerHighlighting(loadable); updateDrawerHighlighting(loadable);
updateLeftPaneHighlighting(loadable); updateLeftPaneHighlighting(loadable);
presenter.requestInitialData(); presenter.requestInitialData();
int counter = ChanSettings.threadOpenCounter.increase(); int counter = ChanSettings.threadOpenCounter.increase();
if (counter == 2) { if (counter == 2) {
HintPopup.show(context, overflowItem.getView(), context.getString(R.string.thread_up_down_hint), -dp(1), 0); View view = navigation.findItem(ToolbarMenu.OVERFLOW_ID).getView();
HintPopup.show(context, view, context.getString(R.string.thread_up_down_hint), -dp(1), 0);
} else if (counter == 3) { } else if (counter == 3) {
HintPopup.show(context, pinItem.getView(), context.getString(R.string.thread_pin_hint), -dp(1), 0); View view = navigation.findItem(PIN_ID).getView();
HintPopup.show(context, view, context.getString(R.string.thread_pin_hint), -dp(1), 0);
} }
} }
} }
@ -173,53 +222,6 @@ public class ViewThreadController extends ThreadController implements ThreadLayo
((ToolbarNavigationController) navigationController).toolbar.updateTitle(navigation); ((ToolbarNavigationController) navigationController).toolbar.updateTitle(navigation);
} }
@Override
public void onMenuItemClicked(ToolbarMenuItem item) {
switch ((Integer) item.getId()) {
case ALBUM_ID:
threadLayout.getPresenter().showAlbum();
break;
case PIN_ID:
setPinIconState(threadLayout.getPresenter().pin());
updateDrawerHighlighting(loadable);
break;
}
}
@Override
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
Integer id = (Integer) item.getId();
switch (id) {
case REPLY_ID:
threadLayout.openReply(true);
break;
case REFRESH_ID:
threadLayout.getPresenter().requestData();
break;
case SEARCH_ID:
((ToolbarNavigationController) navigationController).showSearch();
break;
case SHARE_ID:
case OPEN_BROWSER_ID:
Loadable loadable = threadLayout.getPresenter().getLoadable();
String link = loadable.site.resolvable().desktopUrl(loadable, null);
if (id == SHARE_ID) {
AndroidUtils.shareLink(link);
} else {
AndroidUtils.openLinkInBrowser((Activity) context, link);
}
break;
case UP_ID:
case DOWN_ID:
boolean up = id == UP_ID;
threadLayout.getPresenter().scrollTo(up ? 0 : -1, false);
break;
}
}
private void updateDrawerHighlighting(Loadable loadable) { private void updateDrawerHighlighting(Loadable loadable) {
Pin pin = loadable == null ? null : watchManager.findPinByLoadable(loadable); Pin pin = loadable == null ? null : watchManager.findPinByLoadable(loadable);
@ -254,11 +256,26 @@ public class ViewThreadController extends ThreadController implements ThreadLayo
} }
} }
private void setPinIconState() { private void setPinIconState(boolean animated) {
setPinIconState(watchManager.findPinByLoadable(loadable) != null); ThreadPresenter presenter = threadLayout.getPresenter();
if (presenter != null) {
setPinIconStateDrawable(presenter.isPinned(), animated);
}
} }
private void setPinIconState(boolean pinned) { private void setPinIconStateDrawable(boolean pinned, boolean animated) {
pinItem.setImage(pinned ? R.drawable.ic_bookmark_white_24dp : R.drawable.ic_bookmark_outline_white_24dp); if (pinned == pinItemPinned) {
return;
}
pinItemPinned = pinned;
Drawable outline = context.getResources().getDrawable(
R.drawable.ic_bookmark_outline_white_24dp);
Drawable white = context.getResources().getDrawable(
R.drawable.ic_bookmark_white_24dp);
Drawable drawable = pinned ? white : outline;
navigation.findItem(PIN_ID).setImage(drawable, animated);
} }
} }

@ -62,7 +62,7 @@ public class WatchSettingsController extends SettingsController implements Compo
SwitchCompat globalSwitch = new SwitchCompat(context); SwitchCompat globalSwitch = new SwitchCompat(context);
globalSwitch.setChecked(enabled); globalSwitch.setChecked(enabled);
globalSwitch.setOnCheckedChangeListener(this); globalSwitch.setOnCheckedChangeListener(this);
navigation.rightView = globalSwitch; navigation.setRightView(globalSwitch);
populatePreferences(); populatePreferences();

@ -130,12 +130,7 @@ public class SearchLayout extends LinearLayout {
} }
public void openKeyboard() { public void openKeyboard() {
searchView.post(new Runnable() { searchView.post(() -> AndroidUtils.requestViewAndKeyboardFocus(searchView));
@Override
public void run() {
AndroidUtils.requestViewAndKeyboardFocus(searchView);
}
});
} }
public interface SearchLayoutCallback { public interface SearchLayoutCallback {

@ -17,16 +17,18 @@
*/ */
package org.floens.chan.ui.toolbar; package org.floens.chan.ui.toolbar;
import android.content.Context;
import android.view.View; import android.view.View;
import org.floens.chan.ui.view.FloatingMenu; import org.floens.chan.R;
import org.floens.chan.ui.view.FloatingMenuItem;
import java.util.List;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
/**
* The navigation properties for a Controller. Controls common properties that parent controlers
* need to know, such as the title of the controller.
* <p>
* This is also used to set up the toolbar menu, see {@link #buildMenu()}.
*/
public class NavigationItem { public class NavigationItem {
public String title = ""; public String title = "";
public String subtitle = ""; public String subtitle = "";
@ -39,18 +41,110 @@ public class NavigationItem {
boolean search = false; boolean search = false;
String searchText; String searchText;
public ToolbarMenu menu; protected ToolbarMenu menu;
public ToolbarMiddleMenu middleMenu; protected ToolbarMiddleMenu middleMenu;
public View rightView; protected View rightView;
public ToolbarMenuItem createOverflow(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, List<FloatingMenuItem> items) { public boolean hasArrow() {
ToolbarMenuItem overflow = menu.createOverflow(callback); return hasBack || search;
FloatingMenu overflowMenu = new FloatingMenu(context, overflow.getView(), items);
overflow.setSubMenu(overflowMenu);
return overflow;
} }
public void setTitle(int resId) { public void setTitle(int resId) {
title = getString(resId); title = getString(resId);
} }
public MenuBuilder buildMenu() {
return new MenuBuilder(this);
}
public void setMiddleMenu(ToolbarMiddleMenu middleMenu) {
this.middleMenu = middleMenu;
}
public void setRightView(View view) {
rightView = view;
}
public ToolbarMenuItem findItem(int id) {
if (menu != null) {
return menu.findItem(id);
}
return null;
}
public ToolbarMenuSubItem findSubItem(int id) {
if (menu != null) {
return menu.findSubItem(id);
}
return null;
}
public static class MenuBuilder {
private final NavigationItem navigationItem;
private final ToolbarMenu menu;
public MenuBuilder(NavigationItem navigationItem) {
this.navigationItem = navigationItem;
menu = new ToolbarMenu(null);
}
public MenuBuilder withItem(int drawable, ToolbarMenuItem.ClickCallback clicked) {
return withItem(-1, drawable, clicked);
}
public MenuBuilder withItem(int id, int drawable, ToolbarMenuItem.ClickCallback clicked) {
return withItem(new ToolbarMenuItem(id, drawable, clicked));
}
public MenuBuilder withItem(ToolbarMenuItem menuItem) {
menu.addItem(menuItem);
return this;
}
public MenuOverflowBuilder withOverflow() {
return new MenuOverflowBuilder(
this,
new ToolbarMenuItem(
ToolbarMenu.OVERFLOW_ID,
R.drawable.ic_more_vert_white_24dp,
ToolbarMenuItem::showSubmenu));
}
public ToolbarMenu build() {
navigationItem.menu = menu;
return menu;
}
}
public static class MenuOverflowBuilder {
private final MenuBuilder menuBuilder;
private final ToolbarMenuItem menuItem;
public MenuOverflowBuilder(MenuBuilder menuBuilder, ToolbarMenuItem menuItem) {
this.menuBuilder = menuBuilder;
this.menuItem = menuItem;
}
public MenuOverflowBuilder withSubItem(int text, ToolbarMenuSubItem.ClickCallback clicked) {
return withSubItem(-1, getString(text), clicked);
}
public MenuOverflowBuilder withSubItem(String text, ToolbarMenuSubItem.ClickCallback clicked) {
return withSubItem(-1, text, clicked);
}
public MenuOverflowBuilder withSubItem(int id, int text, ToolbarMenuSubItem.ClickCallback clicked) {
return withSubItem(id, getString(text), clicked);
}
public MenuOverflowBuilder withSubItem(int id, String text, ToolbarMenuSubItem.ClickCallback clicked) {
menuItem.addSubItem(new ToolbarMenuSubItem(id, text, clicked));
return this;
}
public MenuBuilder build() {
return menuBuilder.withItem(menuItem);
}
}
} }

@ -17,45 +17,31 @@
*/ */
package org.floens.chan.ui.toolbar; package org.floens.chan.ui.toolbar;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.Gravity; import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator; import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.ui.drawable.ArrowMenuDrawable; import org.floens.chan.ui.drawable.ArrowMenuDrawable;
import org.floens.chan.ui.drawable.DropdownArrowDrawable;
import org.floens.chan.ui.layout.SearchLayout;
import org.floens.chan.ui.view.LoadView;
import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.hideKeyboard;
import static org.floens.chan.utils.AndroidUtils.removeFromParentView;
import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground;
public class Toolbar extends LinearLayout implements View.OnClickListener { public class Toolbar extends LinearLayout implements
View.OnClickListener, ToolbarPresenter.Callback, ToolbarContainer.Callback {
public static final int TOOLBAR_COLLAPSE_HIDE = 1000000; public static final int TOOLBAR_COLLAPSE_HIDE = 1000000;
public static final int TOOLBAR_COLLAPSE_SHOW = -1000000; public static final int TOOLBAR_COLLAPSE_SHOW = -1000000;
@ -74,23 +60,18 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
} }
}; };
private ToolbarPresenter presenter;
private ImageView arrowMenuView; private ImageView arrowMenuView;
private ArrowMenuDrawable arrowMenuDrawable; private ArrowMenuDrawable arrowMenuDrawable;
private LoadView navigationItemContainer; private ToolbarContainer navigationItemContainer;
private ToolbarCallback callback; private ToolbarCallback callback;
private boolean openKeyboardAfterSearchViewCreated = false;
private int lastScrollDeltaOffset; private int lastScrollDeltaOffset;
private int scrollOffset; private int scrollOffset;
private List<ToolbarCollapseCallback> collapseCallbacks = new ArrayList<>(); private List<ToolbarCollapseCallback> collapseCallbacks = new ArrayList<>();
private boolean transitioning = false;
private NavigationItem fromItem;
private LinearLayout fromView;
private NavigationItem toItem;
private LinearLayout toView;
public Toolbar(Context context) { public Toolbar(Context context) {
this(context, null); this(context, null);
} }
@ -106,7 +87,13 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
@Override @Override
public boolean dispatchTouchEvent(MotionEvent ev) { public boolean dispatchTouchEvent(MotionEvent ev) {
return transitioning || super.dispatchTouchEvent(ev); return isTransitioning() || super.dispatchTouchEvent(ev);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
} }
public int getToolbarHeight() { public int getToolbarHeight() {
@ -143,7 +130,10 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
scrollOffset = Math.max(0, Math.min(getHeight(), scrollOffset)); scrollOffset = Math.max(0, Math.min(getHeight(), scrollOffset));
if (animated) { if (animated) {
animate().translationY(-scrollOffset).setDuration(300).setInterpolator(new DecelerateInterpolator(2f)).start(); animate().translationY(-scrollOffset)
.setDuration(300)
.setInterpolator(new DecelerateInterpolator(2f))
.start();
boolean collapse = scrollOffset > 0; boolean collapse = scrollOffset > 0;
for (ToolbarCollapseCallback c : collapseCallbacks) { for (ToolbarCollapseCallback c : collapseCallbacks) {
@ -181,94 +171,45 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
} }
} }
// public void updateNavigation() { public void openSearch() {
// closeSearchInternal(); presenter.openSearch();
// setNavigationItem(false, false, toItem);
// }
public NavigationItem getNavigationItem() {
return toItem;
} }
public boolean openSearch() { public boolean closeSearch() {
return openSearchInternal(); return presenter.closeSearch();
} }
public boolean closeSearch() { public boolean isTransitioning() {
return closeSearchInternal(); return navigationItemContainer.isTransitioning();
} }
public void setNavigationItem(final boolean animate, final boolean pushing, final NavigationItem item) { public void setNavigationItem(final boolean animate, final boolean pushing, final NavigationItem item) {
setNavigationItemInternal(animate, pushing, item); ToolbarPresenter.AnimationStyle animationStyle;
if (!animate) {
animationStyle = ToolbarPresenter.AnimationStyle.NONE;
} else if (pushing) {
animationStyle = ToolbarPresenter.AnimationStyle.PUSH;
} else {
animationStyle = ToolbarPresenter.AnimationStyle.POP;
}
presenter.set(item, animationStyle);
} }
public void setArrowMenuIconShown(boolean show) { public void setArrowMenuIconShown(boolean show) {
arrowMenuView.setVisibility(show ? View.VISIBLE : View.GONE); arrowMenuView.setVisibility(show ? View.VISIBLE : View.GONE);
} }
public boolean isTransitioning() {
return transitioning;
}
public void beginTransition(NavigationItem newItem) { public void beginTransition(NavigationItem newItem) {
if (transitioning) { presenter.startTransition(newItem, ToolbarPresenter.TransitionAnimationStyle.POP);
throw new IllegalStateException("beginTransition called when already transitioning");
}
attachNavigationItem(newItem);
navigationItemContainer.addView(toView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
transitioning = true;
} }
public void transitionProgress(float progress, boolean pushing) { public void transitionProgress(float progress) {
if (!transitioning) { presenter.setTransitionProgress(progress);
throw new IllegalStateException("transitionProgress called while not transitioning");
}
progress = Math.max(0f, Math.min(1f, progress));
final int offset = dp(16);
toView.setTranslationY((pushing ? offset : -offset) * (1f - progress));
toView.setAlpha(progress);
if (fromItem != null) {
fromView.setTranslationY((pushing ? -offset : offset) * progress);
fromView.setAlpha(1f - progress);
}
float arrowEnd = toItem.hasBack || toItem.search ? 1f : 0f;
if (arrowMenuDrawable.getProgress() != arrowEnd) {
arrowMenuDrawable.setProgress(toItem.hasBack || toItem.search ? progress : 1f - progress);
}
} }
public void finishTransition(boolean finished) { public void finishTransition(boolean completed) {
if (!transitioning) { presenter.stopTransition(completed);
throw new IllegalStateException("finishTransition called when not transitioning");
}
if (finished) {
if (fromItem != null) {
// From a search otherwise
if (fromItem != toItem) {
removeNavigationItem(fromItem, fromView);
fromView = null;
}
}
setArrowMenuProgress(toItem.hasBack || toItem.search ? 1f : 0f);
} else {
removeNavigationItem(toItem, toView);
setArrowMenuProgress(fromItem.hasBack || fromItem.search ? 1f : 0f);
toItem = fromItem;
toView = fromView;
}
fromItem = null;
fromView = null;
transitioning = false;
} }
public void setCallback(ToolbarCallback callback) { public void setCallback(ToolbarCallback callback) {
@ -282,38 +223,12 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
} }
} }
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
public void setArrowMenuProgress(float progress) {
arrowMenuDrawable.setProgress(progress);
}
public void setShowArrowMenu(boolean show) {
arrowMenuView.setVisibility(show ? VISIBLE : GONE);
}
public ArrowMenuDrawable getArrowMenuDrawable() { public ArrowMenuDrawable getArrowMenuDrawable() {
return arrowMenuDrawable; return arrowMenuDrawable;
} }
public void updateTitle(NavigationItem navigationItem) { public void updateTitle(NavigationItem navigationItem) {
LinearLayout view = navigationItem == fromItem ? fromView : (navigationItem == toItem ? toView : null); presenter.update(navigationItem);
if (view != null) {
TextView titleView = view.findViewById(R.id.title);
if (titleView != null) {
titleView.setText(navigationItem.title);
}
if (!TextUtils.isEmpty(navigationItem.subtitle)) {
TextView subtitleView = view.findViewById(R.id.subtitle);
if (subtitleView != null) {
subtitleView.setText(navigationItem.subtitle);
}
}
}
} }
private void init() { private void init() {
@ -321,6 +236,13 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
if (isInEditMode()) return; if (isInEditMode()) return;
presenter = new ToolbarPresenter();
presenter.create(this);
initView();
}
private void initView() {
FrameLayout leftButtonContainer = new FrameLayout(getContext()); FrameLayout leftButtonContainer = new FrameLayout(getContext());
addView(leftButtonContainer, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); addView(leftButtonContainer, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
@ -333,11 +255,17 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
setRoundItemBackground(arrowMenuView); setRoundItemBackground(arrowMenuView);
leftButtonContainer.addView(arrowMenuView, new FrameLayout.LayoutParams(getResources().getDimensionPixelSize(R.dimen.toolbar_height), FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER_VERTICAL)); int toolbarSize = getResources().getDimensionPixelSize(R.dimen.toolbar_height);
FrameLayout.LayoutParams leftButtonContainerLp = new FrameLayout.LayoutParams(
toolbarSize, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER_VERTICAL);
leftButtonContainer.addView(arrowMenuView, leftButtonContainerLp);
navigationItemContainer = new LoadView(getContext()); navigationItemContainer = new ToolbarContainer(getContext());
addView(navigationItemContainer, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)); addView(navigationItemContainer, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f));
navigationItemContainer.setCallback(this);
navigationItemContainer.setArrowMenu(arrowMenuDrawable);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (getElevation() == 0f) { if (getElevation() == 0f) {
setElevation(dp(4f)); setElevation(dp(4f));
@ -345,216 +273,56 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
} }
} }
private boolean openSearchInternal() { @Override
if (toItem != null && !toItem.search) { public void showForNavigationItem(
toItem.search = true; NavigationItem item, ToolbarPresenter.AnimationStyle animation) {
openKeyboardAfterSearchViewCreated = true; navigationItemContainer.set(item, animation);
setNavigationItemInternal(true, false, toItem);
callback.onSearchVisibilityChanged(toItem, true);
return true;
} else {
return false;
}
} }
private boolean closeSearchInternal() { @Override
if (toItem != null && toItem.search) { public void containerStartTransition(
toItem.search = false; NavigationItem item, ToolbarPresenter.TransitionAnimationStyle animation) {
toItem.searchText = null; navigationItemContainer.startTransition(item, animation);
setNavigationItemInternal(true, false, toItem);
callback.onSearchVisibilityChanged(toItem, false);
return true;
} else {
return false;
}
} }
private void setNavigationItemInternal(boolean animate, final boolean pushing, NavigationItem newItem) { @Override
if (transitioning) { public void containerStopTransition(boolean didComplete) {
throw new IllegalStateException("setNavigationItemInternal called when already transitioning"); navigationItemContainer.stopTransition(didComplete);
}
attachNavigationItem(newItem);
transitioning = true;
if (fromItem == toItem) {
// Search toggled
navigationItemContainer.setListener(new LoadView.Listener() {
@Override
public void onLoadViewRemoved(View view) {
// Remove the menu from the navigation item
((ViewGroup) view).removeAllViews();
finishTransition(true);
navigationItemContainer.setListener(null);
}
});
navigationItemContainer.setView(toView, animate);
animateArrow(toItem.hasBack || toItem.search);
} else {
navigationItemContainer.addView(toView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
if (animate) {
toView.setAlpha(0f);
final ValueAnimator animator = ObjectAnimator.ofFloat(0f, 1f);
animator.setDuration(300);
animator.setInterpolator(new DecelerateInterpolator(2f));
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finishTransition(true);
}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
transitionProgress((float) animation.getAnimatedValue(), pushing);
}
});
if (!pushing) {
animator.setStartDelay(100);
}
// hack to avoid the animation jumping when the current frame has a lot to do,
// which is often because setting a new navigationitem is almost always done
// when setting a new controller.
post(new Runnable() {
@Override
public void run() {
animator.start();
}
});
} else {
arrowMenuDrawable.setProgress(toItem.hasBack || toItem.search ? 1f : 0f);
finishTransition(true);
}
}
} }
private void attachNavigationItem(NavigationItem newItem) { @Override
if (transitioning) { public void containerSetTransitionProgress(float progress) {
throw new IllegalStateException("attachNavigationItem called while transitioning"); navigationItemContainer.setTransitionProgress(progress);
}
fromItem = toItem;
fromView = toView;
toItem = newItem;
toView = createNavigationItemView(toItem);
if (!toItem.search) {
AndroidUtils.hideKeyboard(navigationItemContainer);
}
} }
private void removeNavigationItem(NavigationItem item, LinearLayout view) { @Override
if (!transitioning) { public void searchInput(String input) {
throw new IllegalStateException("removeNavigationItem called while not transitioning"); presenter.searchInput(input);
}
view.removeAllViews();
navigationItemContainer.removeView(view);
} }
private LinearLayout createNavigationItemView(final NavigationItem item) { @Override
if (item.search) { public String searchHint(NavigationItem item) {
return createSearchLayout(item); return callback.getSearchHint(item);
} else {
return createNavigationLayout(item);
}
} }
@NonNull @Override
private LinearLayout createNavigationLayout(NavigationItem item) { public void onSearchVisibilityChanged(NavigationItem item, boolean visible) {
@SuppressLint("InflateParams") callback.onSearchVisibilityChanged(item, visible);
LinearLayout menu = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.toolbar_menu, null);
menu.setGravity(Gravity.CENTER_VERTICAL);
FrameLayout titleContainer = menu.findViewById(R.id.title_container);
final TextView titleView = menu.findViewById(R.id.title);
titleView.setTypeface(AndroidUtils.ROBOTO_MEDIUM);
titleView.setText(item.title);
titleView.setTextColor(0xffffffff);
if (item.middleMenu != null) {
int arrowColor = getAttrColor(getContext(), R.attr.dropdown_light_color);
int arrowPressedColor = getAttrColor(getContext(), R.attr.dropdown_light_pressed_color);
Drawable drawable = new DropdownArrowDrawable(dp(12), dp(12), true, arrowColor, arrowPressedColor);
titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null);
titleView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
item.middleMenu.show(titleView);
}
});
}
TextView subtitleView = menu.findViewById(R.id.subtitle);
if (!TextUtils.isEmpty(item.subtitle)) {
ViewGroup.LayoutParams titleParams = titleView.getLayoutParams();
titleParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
titleView.setLayoutParams(titleParams);
subtitleView.setText(item.subtitle);
subtitleView.setTextColor(0xffffffff);
titleView.setPadding(titleView.getPaddingLeft(), dp(5f), titleView.getPaddingRight(), titleView.getPaddingBottom());
} else {
titleContainer.removeView(subtitleView);
}
if (item.rightView != null) {
removeFromParentView(item.rightView);
item.rightView.setPadding(0, 0, dp(16), 0);
menu.addView(item.rightView, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
}
if (item.menu != null) { if (!visible) {
removeFromParentView(item.menu); hideKeyboard(navigationItemContainer);
menu.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
} }
return menu;
} }
@NonNull
private LinearLayout createSearchLayout(NavigationItem item) {
SearchLayout searchLayout = new SearchLayout(getContext());
searchLayout.setCallback(new SearchLayout.SearchLayoutCallback() {
@Override
public void onSearchEntered(String entered) {
item.searchText = entered;
callback.onSearchEntered(item, entered);
}
});
if (item.searchText != null) {
searchLayout.setText(item.searchText);
}
searchLayout.setHint(callback.getSearchHint(item));
if (openKeyboardAfterSearchViewCreated) { @Override
openKeyboardAfterSearchViewCreated = false; public void onSearchInput(NavigationItem item, String input) {
searchLayout.openKeyboard(); callback.onSearchEntered(item, input);
}
searchLayout.setPadding(dp(16), searchLayout.getPaddingTop(), searchLayout.getPaddingRight(), searchLayout.getPaddingBottom());
return searchLayout;
} }
private void animateArrow(boolean toArrow) { @Override
float to = toArrow ? 1f : 0f; public void updateViewForItem(NavigationItem item, boolean current) {
if (to != arrowMenuDrawable.getProgress()) { navigationItemContainer.update(item, current);
ValueAnimator arrowAnimation = ValueAnimator.ofFloat(arrowMenuDrawable.getProgress(), to);
arrowAnimation.setDuration(300);
arrowAnimation.setInterpolator(new DecelerateInterpolator(2f));
arrowAnimation.addUpdateListener(animation ->
setArrowMenuProgress((float) animation.getAnimatedValue()));
arrowAnimation.start();
}
} }
public interface ToolbarCallback { public interface ToolbarCallback {

@ -0,0 +1,530 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.toolbar;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.floens.chan.R;
import org.floens.chan.ui.drawable.ArrowMenuDrawable;
import org.floens.chan.ui.drawable.DropdownArrowDrawable;
import org.floens.chan.ui.layout.SearchLayout;
import org.floens.chan.utils.AndroidUtils;
import java.util.HashMap;
import java.util.Map;
import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.getAttrColor;
import static org.floens.chan.utils.AndroidUtils.removeFromParentView;
/**
* The container for the views created by the toolbar for the navigation items.
* <p>
* It will strictly only transition between two views. If a new view is set
* and a transition is in progress, it is stopped before adding the new view.
* <p>
* For normal animations the previousView is the view that is animated away from, and the
* currentView is the view where is animated to. The previousView is removed and cleared if the
* animation finished.
* <p>
* Transitions are user-controlled animations that can be cancelled of finished. For that the
* currentView describes the view that was originally there, and the transitionView is the view
* what is possibly transitioned to.
* <p>
* This is also the class that is responsible for the orientation and animation of the arrow-menu
* drawable.
*/
public class ToolbarContainer extends FrameLayout {
private Callback callback;
private ArrowMenuDrawable arrowMenu;
@Nullable
private ItemView previousView;
@Nullable
private ItemView currentView;
@Nullable
private ItemView transitionView;
@Nullable
private ToolbarPresenter.TransitionAnimationStyle transitionAnimationStyle;
private Map<View, Animator> animatorSet = new HashMap<>();
public ToolbarContainer(Context context) {
this(context, null);
}
public ToolbarContainer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ToolbarContainer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setCallback(Callback callback) {
this.callback = callback;
}
public void setArrowMenu(ArrowMenuDrawable arrowMenu) {
this.arrowMenu = arrowMenu;
}
public void set(NavigationItem item, ToolbarPresenter.AnimationStyle animation) {
if (transitionView != null) {
throw new IllegalStateException("Currently in transition mode");
}
endAnimations();
ItemView itemView = new ItemView(item);
previousView = currentView;
currentView = itemView;
addView(itemView.view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
if (getChildCount() > 2) {
throw new IllegalArgumentException("More than 2 child views attached");
}
// Can't run the animation if there is no previous view
// Otherwise just show it without an animation.
if (animation != ToolbarPresenter.AnimationStyle.NONE && previousView != null) {
setAnimation(itemView, previousView, animation);
} else {
if (previousView != null) {
removeItem(previousView);
previousView = null;
}
}
if (animation == ToolbarPresenter.AnimationStyle.NONE) {
setArrowProgress(1f, !currentView.item.hasArrow());
}
itemView.attach();
}
public void update(NavigationItem item, boolean current) {
// TODO
View view = viewForItem(item);
if (view != null) {
TextView titleView = view.findViewById(R.id.title);
if (titleView != null) {
titleView.setText(item.title);
}
if (!TextUtils.isEmpty(item.subtitle)) {
TextView subtitleView = view.findViewById(R.id.subtitle);
if (subtitleView != null) {
subtitleView.setText(item.subtitle);
}
}
}
}
public View viewForItem(NavigationItem item) {
ItemView itemView = itemViewForItem(item);
if (itemView == null) {
return null;
}
return itemView.view;
}
private ItemView itemViewForItem(NavigationItem item) {
if (currentView != null && item == currentView.item) {
return currentView;
} else if (previousView != null && item == previousView.item) {
return previousView;
} else if (transitionView != null && item == transitionView.item) {
return transitionView;
} else {
return null;
}
}
public boolean isTransitioning() {
return transitionView != null || previousView != null;
}
public void startTransition(
NavigationItem item, ToolbarPresenter.TransitionAnimationStyle style) {
if (transitionView != null) {
throw new IllegalStateException("Already in transition mode");
}
endAnimations();
ItemView itemView = new ItemView(item);
transitionView = itemView;
transitionAnimationStyle = style;
addView(itemView.view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
if (getChildCount() > 2) {
throw new IllegalArgumentException("More than 2 child views attached");
}
itemView.attach();
}
public void stopTransition(boolean didComplete) {
if (transitionView == null) {
throw new IllegalStateException("Not in transition mode");
}
if (didComplete) {
removeItem(currentView);
currentView = transitionView;
transitionView = null;
} else {
removeItem(transitionView);
transitionView = null;
}
if (getChildCount() != 1) {
throw new IllegalStateException("Not 1 view attached");
}
}
public void setTransitionProgress(float progress) {
if (transitionView == null) {
throw new IllegalStateException("Not in transition mode");
}
transitionProgressAnimation(progress, transitionAnimationStyle);
}
private void endAnimations() {
if (previousView != null) {
endAnimation(previousView.view);
if (previousView != null) {
throw new IllegalStateException("Animation end did not remove view");
}
}
if (currentView != null) {
endAnimation(currentView.view);
}
}
private void endAnimation(View view) {
Animator a = animatorSet.remove(view);
if (a != null) {
a.end();
}
}
private void setAnimation(ItemView view, ItemView previousView,
ToolbarPresenter.AnimationStyle animationStyle) {
if (animationStyle == ToolbarPresenter.AnimationStyle.PUSH ||
animationStyle == ToolbarPresenter.AnimationStyle.POP) {
final boolean pushing = animationStyle == ToolbarPresenter.AnimationStyle.PUSH;
// Previous animation
ValueAnimator previousAnimation = getShortAnimator();
previousAnimation.addUpdateListener(a -> {
float value = (float) a.getAnimatedValue();
setPreviousAnimationProgress(previousView.view, pushing, value);
});
previousAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animatorSet.remove(previousView.view);
removeItem(previousView);
ToolbarContainer.this.previousView = null;
}
});
if (!pushing) previousAnimation.setStartDelay(100);
animatorSet.put(previousView.view, previousAnimation);
post(previousAnimation::start);
// Current animation + arrow
view.view.setAlpha(0f);
ValueAnimator animation = getShortAnimator();
animation.addUpdateListener(a -> {
float value = (float) a.getAnimatedValue();
setAnimationProgress(view.view, pushing, value);
if (previousView.item.hasArrow() != currentView.item.hasArrow()) {
setArrowProgress(value, !currentView.item.hasArrow());
}
});
animation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animatorSet.remove(view.view);
}
});
if (!pushing) animation.setStartDelay(100);
animatorSet.put(view.view, animation);
post(animation::start);
} else if (animationStyle == ToolbarPresenter.AnimationStyle.FADE) {
// Previous animation
ValueAnimator previousAnimation =
ObjectAnimator.ofFloat(previousView.view, View.ALPHA, 1f, 0f);
previousAnimation.setDuration(300);
previousAnimation.setInterpolator(new LinearInterpolator());
previousAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animatorSet.remove(previousView.view);
removeItem(previousView);
ToolbarContainer.this.previousView = null;
}
});
animatorSet.put(previousView.view, previousAnimation);
post(previousAnimation::start);
// Current animation + arrow
view.view.setAlpha(0f);
ValueAnimator animation = ObjectAnimator.ofFloat(view.view, View.ALPHA, 0f, 1f);
animation.setDuration(300);
animation.setInterpolator(new LinearInterpolator());
animation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animatorSet.remove(view.view);
}
});
// A different animator for the arrow because that one needs the deceleration
// interpolator.
ValueAnimator arrow = ValueAnimator.ofFloat(0f, 1f);
arrow.setDuration(300);
arrow.setInterpolator(new DecelerateInterpolator(2f));
arrow.addUpdateListener(a -> {
float value = (float) a.getAnimatedValue();
if (previousView.item.hasArrow() != currentView.item.hasArrow()) {
setArrowProgress(value, !currentView.item.hasArrow());
}
});
AnimatorSet animationAndArrow = new AnimatorSet();
animationAndArrow.playTogether(animation, arrow);
animationAndArrow.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animatorSet.remove(view.view);
}
});
animatorSet.put(view.view, animationAndArrow);
post(animationAndArrow::start);
}
}
private void setPreviousAnimationProgress(View view, boolean pushing, float progress) {
final int offset = dp(16);
view.setTranslationY((pushing ? -offset : offset) * progress);
view.setAlpha(1f - progress);
}
private void setAnimationProgress(View view, boolean pushing, float progress) {
final int offset = dp(16);
view.setTranslationY((pushing ? offset : -offset) * (1f - progress));
view.setAlpha(progress);
}
private void setArrowProgress(float progress, boolean reverse) {
if (reverse) {
progress = 1f - progress;
}
progress = Math.max(0f, Math.min(1f, progress));
arrowMenu.setProgress(progress);
}
private void transitionProgressAnimation(
float progress, ToolbarPresenter.TransitionAnimationStyle style) {
progress = Math.max(0f, Math.min(1f, progress));
final int offset = dp(16);
boolean pushing = style == ToolbarPresenter.TransitionAnimationStyle.PUSH;
transitionView.view.setTranslationY((pushing ? offset : -offset) * (1f - progress));
transitionView.view.setAlpha(progress);
currentView.view.setTranslationY((pushing ? -offset : offset) * progress);
currentView.view.setAlpha(1f - progress);
if (transitionView.item.hasArrow() != currentView.item.hasArrow()) {
setArrowProgress(progress, !transitionView.item.hasArrow());
}
}
private ValueAnimator getShortAnimator() {
final ValueAnimator animator = ObjectAnimator.ofFloat(0f, 1f);
animator.setDuration(300);
animator.setInterpolator(new DecelerateInterpolator(2f));
return animator;
}
private void removeItem(ItemView item) {
item.remove();
removeView(item.view);
}
private class ItemView {
final View view;
final NavigationItem item;
@Nullable
private ToolbarMenuView menuView;
public ItemView(NavigationItem item) {
this.view = createNavigationItemView(item);
this.item = item;
}
public void attach() {
if (item.menu != null) {
menuView.attach(item.menu);
}
}
public void remove() {
if (menuView != null) {
menuView.detach();
}
}
private LinearLayout createNavigationItemView(final NavigationItem item) {
if (item.search) {
return createSearchLayout(item);
} else {
return createNavigationLayout(item);
}
}
@NonNull
private LinearLayout createNavigationLayout(NavigationItem item) {
@SuppressLint("InflateParams")
LinearLayout menu = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.toolbar_menu, null);
menu.setGravity(Gravity.CENTER_VERTICAL);
FrameLayout titleContainer = menu.findViewById(R.id.title_container);
// Title
final TextView titleView = menu.findViewById(R.id.title);
titleView.setTypeface(AndroidUtils.ROBOTO_MEDIUM);
titleView.setText(item.title);
titleView.setTextColor(0xffffffff);
// Middle title with arrow and callback
if (item.middleMenu != null) {
int arrowColor = getAttrColor(getContext(), R.attr.dropdown_light_color);
int arrowPressedColor = getAttrColor(
getContext(), R.attr.dropdown_light_pressed_color);
Drawable drawable = new DropdownArrowDrawable(
dp(12), dp(12), true, arrowColor, arrowPressedColor);
titleView.setCompoundDrawablesWithIntrinsicBounds(
null, null, drawable, null);
titleView.setOnClickListener(v -> item.middleMenu.show(titleView));
}
// Possible subtitle.
TextView subtitleView = menu.findViewById(R.id.subtitle);
if (!TextUtils.isEmpty(item.subtitle)) {
ViewGroup.LayoutParams titleParams = titleView.getLayoutParams();
titleParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
titleView.setLayoutParams(titleParams);
subtitleView.setText(item.subtitle);
subtitleView.setTextColor(0xffffffff);
titleView.setPadding(titleView.getPaddingLeft(), dp(5f),
titleView.getPaddingRight(), titleView.getPaddingBottom());
} else {
titleContainer.removeView(subtitleView);
}
// Possible view shown at the right side.
if (item.rightView != null) {
removeFromParentView(item.rightView);
item.rightView.setPadding(0, 0, dp(16), 0);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.MATCH_PARENT);
menu.addView(item.rightView, lp);
}
// Possible menu with items.
if (item.menu != null) {
menuView = new ToolbarMenuView(getContext());
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.MATCH_PARENT);
menu.addView(this.menuView, lp);
}
return menu;
}
@NonNull
private LinearLayout createSearchLayout(NavigationItem item) {
SearchLayout searchLayout = new SearchLayout(getContext());
searchLayout.setCallback(input -> {
callback.searchInput(input);
});
if (item.searchText != null) {
searchLayout.setText(item.searchText);
}
searchLayout.setHint(callback.searchHint(item));
searchLayout.setPadding(dp(16), searchLayout.getPaddingTop(), searchLayout.getPaddingRight(), searchLayout.getPaddingBottom());
return searchLayout;
}
}
public interface Callback {
void searchInput(String input);
String searchHint(NavigationItem item);
}
}

@ -18,52 +18,42 @@
package org.floens.chan.ui.toolbar; package org.floens.chan.ui.toolbar;
import android.content.Context; 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.ArrayList;
import java.util.List; import java.util.List;
import static org.floens.chan.utils.AndroidUtils.dp; public class ToolbarMenu {
public static final int OVERFLOW_ID = 1000;
public class ToolbarMenu extends LinearLayout {
private List<ToolbarMenuItem> items = new ArrayList<>();
public ToolbarMenu(Context context) {
this(context, null);
}
public ToolbarMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ToolbarMenu(Context context, AttributeSet attrs, int defStyle) { public final List<ToolbarMenuItem> items = new ArrayList<>();
super(context, attrs, defStyle);
setOrientation(HORIZONTAL); public ToolbarMenu(@Deprecated Context context) {
setGravity(Gravity.CENTER_VERTICAL);
} }
public ToolbarMenuItem addItem(ToolbarMenuItem item) { public ToolbarMenuItem addItem(ToolbarMenuItem item) {
items.add(item); items.add(item);
ImageView icon = item.getView();
if (icon != null) {
int viewIndex = Math.min(getChildCount(), item.getOrder());
addView(icon, viewIndex);
}
return item; return item;
} }
public ToolbarMenuItem createOverflow(ToolbarMenuItem.ToolbarMenuItemCallback callback) { public ToolbarMenuItem findItem(int id) {
ToolbarMenuItem overflow = addItem(new ToolbarMenuItem(getContext(), callback, 100, 100, R.drawable.ic_more_vert_white_24dp)); for (ToolbarMenuItem item : items) {
ImageView overflowImage = overflow.getView(); if (item.id.equals(id)) {
overflowImage.setLayoutParams(new LinearLayout.LayoutParams(dp(44), dp(54))); return item;
overflowImage.setPadding(dp(8), 0, dp(16), 0); }
}
return null;
}
public ToolbarMenuSubItem findSubItem(int id) {
ToolbarMenuItem overflow = findItem(OVERFLOW_ID);
if (overflow != null) {
for (ToolbarMenuSubItem subItem : overflow.subItems) {
if (subItem.id == id) {
return subItem;
}
}
}
return overflow; return null;
} }
} }

@ -17,74 +17,141 @@
*/ */
package org.floens.chan.ui.toolbar; package org.floens.chan.ui.toolbar;
import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import org.floens.chan.ui.view.FloatingMenu; import org.floens.chan.ui.view.FloatingMenu;
import org.floens.chan.ui.view.FloatingMenuItem; import org.floens.chan.ui.view.FloatingMenuItem;
import org.floens.chan.utils.Logger;
import static org.floens.chan.utils.AndroidUtils.dp; import java.util.ArrayList;
import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; import java.util.List;
public class ToolbarMenuItem implements View.OnClickListener, FloatingMenu.FloatingMenuCallback { import static org.floens.chan.utils.AndroidUtils.getRes;
private ToolbarMenuItemCallback callback; import static org.floens.chan.utils.AndroidUtils.removeFromParentView;
private Object id;
private int order;
private FloatingMenu subMenu;
private ImageView imageView; /**
* An item for the Toolbar menu. These are ImageViews with an icon, that wehen pressed call
* some callback. Add them with the NavigationItem MenuBuilder.
*/
public class ToolbarMenuItem {
private static final String TAG = "ToolbarMenuItem";
public Object id;
public int order;
public boolean overflowStyle = false;
public boolean visible = true;
public Drawable drawable;
public final List<ToolbarMenuSubItem> subItems = new ArrayList<>();
public ToolbarMenuItem(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, int order, int drawable) { private ClickCallback clicked;
this(context, callback, order, order, context.getResources().getDrawable(drawable));
// Views, only non-null if attached to ToolbarMenuView.
private ImageView view;
public ToolbarMenuItem(int id, int drawable, ClickCallback clicked) {
this.id = id;
this.drawable = getRes().getDrawable(drawable);
this.clicked = clicked;
} }
public ToolbarMenuItem(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, Object id, int order, int drawable) { public void attach(ImageView view) {
this(context, callback, id, order, context.getResources().getDrawable(drawable)); if (this.view != null) {
throw new IllegalStateException("Already attached");
}
this.view = view;
} }
public ToolbarMenuItem(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, Object id, int order, Drawable drawable) { public void detach() {
this.id = id; if (this.view == null) {
this.order = order; throw new IllegalStateException("Not attached");
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(50), dp(56)));
imageView.setImageDrawable(drawable);
setRoundItemBackground(imageView);
} }
removeFromParentView(this.view);
this.view = null;
} }
public void setImage(Drawable drawable) { public ImageView getView() {
imageView.setImageDrawable(drawable); return view;
}
public void addSubItem(ToolbarMenuSubItem subItem) {
subItems.add(subItem);
}
public void setVisible(boolean visible) {
this.visible = visible;
if (view != null) {
view.setVisibility(visible ? View.VISIBLE : View.GONE);
}
} }
public void setImage(int drawable) { public void setImage(int drawable) {
imageView.setImageResource(drawable); setImage(getRes().getDrawable(drawable));
} }
public void setSubMenu(FloatingMenu subMenu) { public void setImage(Drawable drawable) {
this.subMenu = subMenu; setImage(drawable, false);
subMenu.setCallback(this);
} }
public FloatingMenu getSubMenu() { public void setImage(Drawable drawable, boolean animated) {
return subMenu; if (view == null) {
this.drawable = drawable;
return;
}
if (!animated) {
view.setImageDrawable(drawable);
} else {
TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[]{
this.drawable.mutate(), drawable.mutate()
});
view.setImageDrawable(transitionDrawable);
transitionDrawable.setCrossFadeEnabled(true);
transitionDrawable.startTransition(100);
}
this.drawable = drawable;
}
public void setSubMenu(FloatingMenu subMenu) {
} }
@Override public void showSubmenu() {
public void onClick(View v) { if (view == null) {
if (subMenu != null && !subMenu.isShowing()) { Logger.w(TAG, "Item not attached, can't show submenu");
subMenu.show(); return;
} }
callback.onMenuItemClicked(this);
List<FloatingMenuItem> floatingMenuItems = new ArrayList<>();
List<ToolbarMenuSubItem> subItems = new ArrayList<>(this.subItems);
for (ToolbarMenuSubItem subItem : subItems) {
floatingMenuItems.add(new FloatingMenuItem(subItem.id, subItem.text, subItem.enabled));
}
FloatingMenu overflowMenu = new FloatingMenu(view.getContext(), view, floatingMenuItems);
overflowMenu.setCallback(new FloatingMenu.FloatingMenuCallback() {
@Override
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
ToolbarMenuSubItem subItem = subItems.get(floatingMenuItems.indexOf(item));
subItem.performClick();
}
@Override
public void onFloatingMenuDismissed(FloatingMenu menu) {
}
});
overflowMenu.show();
} }
public Object getId() { public Object getId() {
@ -95,22 +162,13 @@ public class ToolbarMenuItem implements View.OnClickListener, FloatingMenu.Float
return order; return order;
} }
public ImageView getView() { public void performClick() {
return imageView; if (clicked != null) {
} clicked.clicked(this);
}
@Override
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
callback.onSubMenuItemClicked(this, item);
}
@Override
public void onFloatingMenuDismissed(FloatingMenu menu) {
} }
public interface ToolbarMenuItemCallback { public interface ClickCallback {
void onMenuItemClicked(ToolbarMenuItem item); void clicked(ToolbarMenuItem item);
void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item);
} }
} }

@ -0,0 +1,64 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.toolbar;
import static org.floens.chan.utils.AndroidUtils.getString;
/**
* An item for a submenu of a ToolbarMenuItem. Most common as subitem for the overflow button.
* Add with NavigationItem MenuBuilder.
*/
public class ToolbarMenuSubItem {
public int id;
public String text;
public boolean enabled = true;
public ClickCallback clicked;
public ToolbarMenuSubItem(int id, int text, ClickCallback clicked) {
this(id, getString(text), clicked);
}
public ToolbarMenuSubItem(int id, int text, boolean enabled) {
this(id, getString(text), enabled, null);
}
public ToolbarMenuSubItem(int id, String text, ClickCallback clicked) {
this(id, text, true, clicked);
}
public ToolbarMenuSubItem(String text, ClickCallback clicked) {
this(-1, text, true, clicked);
}
public ToolbarMenuSubItem(int id, String text, boolean enabled, ClickCallback clicked) {
this.id = id;
this.text = text;
this.enabled = enabled;
this.clicked = clicked;
}
public void performClick() {
if (clicked != null) {
clicked.clicked(this);
}
}
public interface ClickCallback {
void clicked(ToolbarMenuSubItem subItem);
}
}

@ -0,0 +1,100 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.toolbar;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import java.util.Collections;
import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground;
/**
* The container view for the list of ToolbarMenuItems, a list of ImageViews.
*/
public class ToolbarMenuView extends LinearLayout {
private ToolbarMenu menu;
public ToolbarMenuView(Context context) {
this(context, null);
}
public ToolbarMenuView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ToolbarMenuView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setOrientation(HORIZONTAL);
setGravity(Gravity.CENTER_VERTICAL);
}
public void attach(ToolbarMenu menu) {
this.menu = menu;
setupMenuViews();
}
public void detach() {
for (ToolbarMenuItem item : menu.items) {
item.detach();
}
}
private void setupMenuViews() {
removeAllViews();
Collections.sort(menu.items, (a, b) -> a.order - b.order);
for (ToolbarMenuItem item : menu.items) {
ImageView imageView = new ImageView(getContext());
imageView.setOnClickListener((v) -> {
handleClick(item);
});
imageView.setFocusable(true);
imageView.setScaleType(ImageView.ScaleType.CENTER);
imageView.setVisibility(item.visible ? View.VISIBLE : View.GONE);
if (item.overflowStyle) {
imageView.setLayoutParams(new LayoutParams(dp(44), dp(56)));
imageView.setPadding(dp(8), 0, dp(16), 0);
} else {
imageView.setLayoutParams(new LinearLayout.LayoutParams(dp(50), dp(56)));
}
imageView.setImageDrawable(item.drawable);
setRoundItemBackground(imageView);
addView(imageView);
item.attach(imageView);
}
}
private void handleClick(ToolbarMenuItem item) {
item.performClick();
}
}

@ -0,0 +1,159 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.toolbar;
public class ToolbarPresenter {
public enum AnimationStyle {
NONE,
PUSH,
POP,
FADE
}
public enum TransitionAnimationStyle {
PUSH,
POP
}
private Callback callback;
private NavigationItem item;
private NavigationItem search;
private NavigationItem transition;
void create(Callback callback) {
this.callback = callback;
}
void set(NavigationItem newItem, AnimationStyle animation) {
cancelTransitionIfNeeded();
if (closeSearchIfNeeded()) {
animation = AnimationStyle.FADE;
}
item = newItem;
callback.showForNavigationItem(item, animation);
}
void update(NavigationItem updatedItem) {
callback.updateViewForItem(updatedItem, updatedItem == item);
}
void startTransition(NavigationItem newItem, TransitionAnimationStyle animation) {
cancelTransitionIfNeeded();
if (closeSearchIfNeeded()) {
callback.showForNavigationItem(item, AnimationStyle.NONE);
}
transition = newItem;
callback.containerStartTransition(transition, animation);
}
void stopTransition(boolean didComplete) {
if (transition == null) {
return;
}
callback.containerStopTransition(didComplete);
if (didComplete) {
item = transition;
callback.showForNavigationItem(item, AnimationStyle.NONE);
}
transition = null;
}
void setTransitionProgress(float progress) {
if (transition == null) {
return;
}
callback.containerSetTransitionProgress(progress);
}
void openSearch() {
if (search != null) {
return;
}
cancelTransitionIfNeeded();
search = new NavigationItem();
search.search = true;
callback.showForNavigationItem(search, AnimationStyle.FADE);
callback.onSearchVisibilityChanged(item, true);
}
boolean closeSearch() {
if (search == null) {
return false;
}
search = null;
set(item, AnimationStyle.FADE);
callback.onSearchVisibilityChanged(item, false);
return true;
}
private void cancelTransitionIfNeeded() {
if (transition != null) {
callback.containerStopTransition(false);
transition = null;
}
}
private boolean closeSearchIfNeeded() {
// Cancel search
if (search != null) {
search = null;
callback.onSearchVisibilityChanged(item, false);
return true;
}
return false;
}
void searchInput(String input) {
if (search == null) {
return;
}
search.searchText = input;
callback.onSearchInput(item, search.searchText);
}
interface Callback {
void showForNavigationItem(NavigationItem item, AnimationStyle animation);
void containerStartTransition(NavigationItem item, TransitionAnimationStyle animation);
void containerStopTransition(boolean didComplete);
void containerSetTransitionProgress(float progress);
void onSearchVisibilityChanged(NavigationItem item, boolean visible);
void onSearchInput(NavigationItem item, String input);
void updateViewForItem(NavigationItem item, boolean current);
}
}

@ -68,6 +68,10 @@ public class CustomScaleImageView extends SubsamplingScaleImageView {
public void onPreviewLoadError(Exception e) { public void onPreviewLoadError(Exception e) {
} }
@Override
public void onPreviewReleased() {
}
@Override @Override
public void onImageLoadError(Exception e) { public void onImageLoadError(Exception e) {
Logger.w(TAG, "onImageLoadError", e); Logger.w(TAG, "onImageLoadError", e);

@ -27,7 +27,6 @@ import android.view.ViewTreeObserver;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ListAdapter; import android.widget.ListAdapter;
import android.widget.PopupWindow;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
@ -202,16 +201,13 @@ public class FloatingMenu {
}; };
anchor.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener); anchor.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() { popupWindow.setOnDismissListener(() -> {
@Override if (anchor.getViewTreeObserver().isAlive()) {
public void onDismiss() { anchor.getViewTreeObserver().removeGlobalOnLayoutListener(globalLayoutListener);
if (anchor.getViewTreeObserver().isAlive()) {
anchor.getViewTreeObserver().removeGlobalOnLayoutListener(globalLayoutListener);
}
globalLayoutListener = null;
popupWindow = null;
callback.onFloatingMenuDismissed(FloatingMenu.this);
} }
globalLayoutListener = null;
popupWindow = null;
callback.onFloatingMenuDismissed(FloatingMenu.this);
}); });
popupWindow.show(); popupWindow.show();

@ -22,9 +22,7 @@ import android.content.ContextWrapper;
import android.content.Intent; import android.content.Intent;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.MediaPlayer; import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.StrictMode;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
@ -40,9 +38,10 @@ import com.android.volley.toolbox.ImageLoader.ImageContainer;
import com.davemorrissey.labs.subscaleview.ImageSource; import com.davemorrissey.labs.subscaleview.ImageSource;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.cache.FileCacheListener;
import org.floens.chan.core.cache.FileCache; import org.floens.chan.core.cache.FileCache;
import org.floens.chan.core.cache.FileCacheDownloader; import org.floens.chan.core.cache.FileCacheDownloader;
import org.floens.chan.core.cache.FileCacheListener;
import org.floens.chan.core.cache.FileCacheProvider;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
@ -386,16 +385,10 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
private void setVideoFile(final File file) { private void setVideoFile(final File file) {
if (ChanSettings.videoOpenExternal.get()) { if (ChanSettings.videoOpenExternal.get()) {
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "video/*"); intent.setDataAndType(FileCacheProvider.getUriForFile(file), "video/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
{
StrictMode.VmPolicy vmPolicy = StrictMode.getVmPolicy();
StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX);
AndroidUtils.openIntent(intent); AndroidUtils.openIntent(intent);
StrictMode.setVmPolicy(vmPolicy);
}
onModeLoaded(Mode.MOVIE, videoView); onModeLoaded(Mode.MOVIE, videoView);
} else { } else {

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!-- Corresponds to the file path chosen in the NetModule.getCacheDir method -->
<cache-path
name="fc"
path="filecache/" />
<external-cache-path
name="exfc"
path="filecache/" />
</paths>

@ -5,7 +5,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.1.2' classpath 'com.android.tools.build:gradle:3.1.3'
} }
} }

Loading…
Cancel
Save