complete board setup controller, delete old board edit controller

redo the browse controller middle menu to support multiple sites.
multisite
Floens 8 years ago
parent 8a86b2d7c3
commit 99ce139bf8
  1. 11
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseBoardManager.java
  2. 10
      Clover/app/src/main/java/org/floens/chan/core/di/AppModule.java
  3. 73
      Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java
  4. 136
      Clover/app/src/main/java/org/floens/chan/core/presenter/BoardSetupPresenter.java
  5. 131
      Clover/app/src/main/java/org/floens/chan/core/presenter/BrowsePresenter.java
  6. 2
      Clover/app/src/main/java/org/floens/chan/core/presenter/SitesSetupPresenter.java
  7. 14
      Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java
  8. 8
      Clover/app/src/main/java/org/floens/chan/ui/adapter/DrawerAdapter.java
  9. 478
      Clover/app/src/main/java/org/floens/chan/ui/controller/BoardEditController.java
  10. 31
      Clover/app/src/main/java/org/floens/chan/ui/controller/BoardSetupController.java
  11. 356
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  12. 4
      Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java
  13. 21
      Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
  14. 4
      Clover/app/src/main/java/org/floens/chan/ui/controller/SetupController.java
  15. 17
      Clover/app/src/main/java/org/floens/chan/ui/controller/SitesSetupController.java
  16. 3
      Clover/app/src/main/java/org/floens/chan/ui/helper/BoardHelper.java
  17. 24
      Clover/app/src/main/java/org/floens/chan/ui/layout/BoardAddLayout.java
  18. 237
      Clover/app/src/main/java/org/floens/chan/ui/layout/BrowseBoardsFloatingMenu.java
  19. 8
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/NavigationItem.java
  20. 13
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
  21. 13
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMenu.java
  22. 24
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/ToolbarMiddleMenu.java
  23. 73
      Clover/app/src/main/java/org/floens/chan/ui/view/FloatingMenu.java
  24. 69
      Clover/app/src/main/res/layout/cell_board_edit.xml
  25. 19
      Clover/app/src/main/res/layout/cell_board_suggestion.xml
  26. 26
      Clover/app/src/main/res/layout/cell_browse_board.xml
  27. 55
      Clover/app/src/main/res/layout/cell_browse_site.xml
  28. 22
      Clover/app/src/main/res/values/strings.xml

@ -33,6 +33,17 @@ public class DatabaseBoardManager {
}; };
} }
public Callable<Void> update(final Board board) {
return new Callable<Void>() {
@Override
public Void call() throws Exception {
helper.boardsDao.update(board);
return null;
}
};
}
public Callable<Void> createAll(final List<Board> boards) { public Callable<Void> createAll(final List<Board> boards) {
return new Callable<Void>() { return new Callable<Void>() {
@Override @Override

@ -13,24 +13,23 @@ import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.manager.FilterEngine; import org.floens.chan.core.manager.FilterEngine;
import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.presenter.BoardSetupPresenter;
import org.floens.chan.core.presenter.SitesSetupPresenter;
import org.floens.chan.core.site.SiteManager;
import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.net.BitmapLruImageCache; import org.floens.chan.core.net.BitmapLruImageCache;
import org.floens.chan.core.presenter.BoardSetupPresenter;
import org.floens.chan.core.presenter.ImageViewerPresenter; import org.floens.chan.core.presenter.ImageViewerPresenter;
import org.floens.chan.core.presenter.ReplyPresenter; import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.core.presenter.SitesSetupPresenter;
import org.floens.chan.core.presenter.ThreadPresenter; import org.floens.chan.core.presenter.ThreadPresenter;
import org.floens.chan.core.receiver.WatchUpdateReceiver; import org.floens.chan.core.receiver.WatchUpdateReceiver;
import org.floens.chan.core.saver.ImageSaveTask; import org.floens.chan.core.saver.ImageSaveTask;
import org.floens.chan.core.site.SiteManager;
import org.floens.chan.core.site.common.ChanReaderRequest;
import org.floens.chan.core.site.http.HttpCallManager; import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.core.site.sites.chan4.Chan4; import org.floens.chan.core.site.sites.chan4.Chan4;
import org.floens.chan.core.site.common.ChanReaderRequest;
import org.floens.chan.core.update.UpdateManager; import org.floens.chan.core.update.UpdateManager;
import org.floens.chan.ui.activity.BoardActivity; import org.floens.chan.ui.activity.BoardActivity;
import org.floens.chan.ui.adapter.DrawerAdapter; import org.floens.chan.ui.adapter.DrawerAdapter;
import org.floens.chan.ui.adapter.PostsFilter; import org.floens.chan.ui.adapter.PostsFilter;
import org.floens.chan.ui.controller.BoardEditController;
import org.floens.chan.ui.controller.BoardSetupController; import org.floens.chan.ui.controller.BoardSetupController;
import org.floens.chan.ui.controller.BrowseController; import org.floens.chan.ui.controller.BrowseController;
import org.floens.chan.ui.controller.DeveloperSettingsController; import org.floens.chan.ui.controller.DeveloperSettingsController;
@ -74,7 +73,6 @@ import dagger.Provides;
DeveloperSettingsController.class, DeveloperSettingsController.class,
BoardActivity.class, BoardActivity.class,
ThreadPresenter.class, ThreadPresenter.class,
BoardEditController.class,
FilterEngine.class, FilterEngine.class,
BrowseController.class, BrowseController.class,
FilterLayout.class, FilterLayout.class,

@ -17,16 +17,21 @@
*/ */
package org.floens.chan.core.manager; package org.floens.chan.core.manager;
import android.util.Pair;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.site.Boards; import org.floens.chan.core.site.Boards;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteManager;
import org.floens.chan.core.site.Sites; import org.floens.chan.core.site.Sites;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Observable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -63,14 +68,19 @@ public class BoardManager {
}; };
private final DatabaseManager databaseManager; private final DatabaseManager databaseManager;
private final SiteManager siteManager;
private final List<Board> savedBoards = new ArrayList<>(); private final List<Board> savedBoards = new ArrayList<>();
private final List<Pair<Site, List<Board>>> sitesWithBoards = new ArrayList<>();
private final SavedBoards savedBoardsObservable = new SavedBoards();
@Inject @Inject
public BoardManager(DatabaseManager databaseManager) { public BoardManager(DatabaseManager databaseManager, SiteManager siteManager) {
this.databaseManager = databaseManager; this.databaseManager = databaseManager;
this.siteManager = siteManager;
loadBoards(); updateSavedBoards();
fetchLimitedSitesTheirBoards(); fetchLimitedSitesTheirBoards();
} }
@ -92,37 +102,44 @@ public class BoardManager {
return savedBoards; return savedBoards;
} }
public SavedBoards getSavedBoardsObservable() {
return savedBoardsObservable;
}
public List<Board> getSiteBoards(Site site) { public List<Board> getSiteBoards(Site site) {
return databaseManager.runTaskSync( return databaseManager.runTaskSync(
databaseManager.getDatabaseBoardManager().getSiteBoards(site)); databaseManager.getDatabaseBoardManager().getSiteBoards(site));
} }
public List<Board> getSiteSavedBoards(Site site) { public List<Board> getSiteSavedBoards(Site site) {
return databaseManager.runTaskSync( List<Board> boards = databaseManager.runTaskSync(
databaseManager.getDatabaseBoardManager().getSiteSavedBoards(site)); databaseManager.getDatabaseBoardManager().getSiteSavedBoards(site));
Collections.sort(boards, ORDER_SORT);
return boards;
} }
public void saveBoard(Board board) { public void saveBoard(Board board) {
board.saved = true; setSaved(board, true);
databaseManager.runTaskSync(databaseManager.getDatabaseBoardManager().createOrUpdate(board));
loadBoards();
} }
public void unsaveBoard(Board board) { public void unsaveBoard(Board board) {
board.saved = false; setSaved(board, false);
databaseManager.runTaskSync(databaseManager.getDatabaseBoardManager().createOrUpdate(board));
loadBoards();
} }
private void loadBoards() { public void updateBoardOrder(Board board, int order) {
savedBoards.clear(); board.order = order;
savedBoards.addAll(databaseManager.runTaskSync(databaseManager.getDatabaseBoardManager().getSavedBoards())); databaseManager.runTask(databaseManager.getDatabaseBoardManager().update(board), new DatabaseManager.TaskResult<Void>() {
@Override
public void onComplete(Void result) {
updateSavedBoards();
}
});
}
EventBus.getDefault().post(new BoardsChangedMessage()); private void setSaved(Board board, boolean saved) {
board.saved = saved;
databaseManager.runTaskSync(databaseManager.getDatabaseBoardManager().createOrUpdate(board));
updateSavedBoards();
} }
private void fetchLimitedSitesTheirBoards() { private void fetchLimitedSitesTheirBoards() {
@ -145,6 +162,28 @@ public class BoardManager {
databaseManager.runTask(databaseManager.getDatabaseBoardManager().createAll(boards.boards)); databaseManager.runTask(databaseManager.getDatabaseBoardManager().createAll(boards.boards));
} }
private void updateSavedBoards() {
savedBoards.clear();
savedBoards.addAll(databaseManager.runTaskSync(databaseManager.getDatabaseBoardManager().getSavedBoards()));
sitesWithBoards.clear();
for (Site site : Sites.allSites()) {
List<Board> siteBoards = getSiteSavedBoards(site);
sitesWithBoards.add(new Pair<>(site, siteBoards));
}
// TODO: remove, use the observable
EventBus.getDefault().post(new BoardsChangedMessage());
savedBoardsObservable.notifyObservers();
}
public class SavedBoards extends Observable {
public List<Pair<Site, List<Board>>> get() {
return sitesWithBoards;
}
}
/*private void appendBoards(Boards response) { /*private void appendBoards(Boards response) {
List<Board> boardsToAddWs = new ArrayList<>(); List<Board> boardsToAddWs = new ArrayList<>();
List<Board> boardsToAddNws = new ArrayList<>(); List<Board> boardsToAddNws = new ArrayList<>();

@ -63,25 +63,29 @@ public class BoardSetupPresenter {
callback.setSavedBoards(savedBoards); callback.setSavedBoards(savedBoards);
} }
public void setAddCallback(AddCallback addCallback) { public void addClicked() {
callback.showAddDialog();
}
public void bindAddDialog(AddCallback addCallback) {
this.addCallback = addCallback; this.addCallback = addCallback;
suggestions.clear();
selectedSuggestions.clear(); searchEntered(null);
} }
public void addClicked() { public void unbindAddDialog() {
callback.showAddDialog(); this.addCallback = null;
suggestions.clear();
selectedSuggestions.clear();
} }
public void onSuggestionClicked(BoardSuggestion suggestion) { public void onSuggestionClicked(BoardSuggestion suggestion) {
suggestion.checked = !suggestion.checked; suggestion.checked = !suggestion.checked;
String code = suggestion.board.code;
if (suggestion.checked) { if (suggestion.checked) {
selectedSuggestions.add(code); selectedSuggestions.add(suggestion.getCode());
} else { } else {
selectedSuggestions.remove(code); selectedSuggestions.remove(suggestion.getCode());
} }
addCallback.updateSuggestions();
} }
public List<BoardSuggestion> getSuggestions() { public List<BoardSuggestion> getSuggestions() {
@ -89,27 +93,62 @@ public class BoardSetupPresenter {
} }
public void onAddDialogPositiveClicked() { public void onAddDialogPositiveClicked() {
for (BoardSuggestion suggestion : suggestions) { int count = 0;
if (suggestion.checked) {
boardManager.saveBoard(suggestion.board); if (site.boardsType() == Site.BoardsType.DYNAMIC) {
updateSavedBoards(); for (Board board : boardManager.getSiteBoards(site)) {
callback.setSavedBoards(savedBoards); if (selectedSuggestions.contains(board.code)) {
boardManager.saveBoard(board);
boardManager.updateBoardOrder(board, savedBoards.size());
count++;
}
}
} else {
for (String suggestion : selectedSuggestions) {
// TODO(multisite)
Board board = new Board(site, suggestion, suggestion, true, true);
boardManager.saveBoard(board);
count++;
} }
} }
updateSavedBoards();
callback.setSavedBoards(savedBoards);
callback.boardsWereAdded(count);
} }
public void move(int from, int to) { public void move(int from, int to) {
Board item = savedBoards.remove(from); Board item = savedBoards.remove(from);
savedBoards.add(to, item); savedBoards.add(to, item);
int min = Math.min(from, to);
int max = Math.max(from, to);
for (int i = min; i <= max; i++) {
boardManager.updateBoardOrder(savedBoards.get(i), i);
}
callback.setSavedBoards(savedBoards); callback.setSavedBoards(savedBoards);
} }
public void remove(int position) { public void remove(int position) {
Board board = savedBoards.remove(position); Board board = savedBoards.remove(position);
boardManager.unsaveBoard(board); boardManager.unsaveBoard(board);
for (int i = position; i < savedBoards.size(); i++) {
boardManager.updateBoardOrder(savedBoards.get(i), i);
}
updateSavedBoards();
callback.setSavedBoards(savedBoards);
callback.showRemovedSnackbar(board);
}
public void undoRemoveBoard(Board board) {
boardManager.saveBoard(board);
// TODO
boardManager.updateBoardOrder(board, savedBoards.size());
updateSavedBoards(); updateSavedBoards();
callback.setSavedBoards(savedBoards); callback.setSavedBoards(savedBoards);
} }
@ -118,32 +157,48 @@ public class BoardSetupPresenter {
callback.finish(); callback.finish();
} }
public void searchEntered(final String query) { public void searchEntered(String userQuery) {
if (suggestionCall != null) { if (suggestionCall != null) {
suggestionCall.cancel(); suggestionCall.cancel();
} }
final String query = userQuery == null ? null :
userQuery.replace("/", "").replace("\\", "");
suggestionCall = BackgroundUtils.runWithExecutor(executor, new Callable<List<BoardSuggestion>>() { suggestionCall = BackgroundUtils.runWithExecutor(executor, new Callable<List<BoardSuggestion>>() {
@Override @Override
public List<BoardSuggestion> call() throws Exception { public List<BoardSuggestion> call() throws Exception {
List<BoardSuggestion> suggestions = new ArrayList<>(); List<BoardSuggestion> suggestions = new ArrayList<>();
if (site.boardsType() == Site.BoardsType.DYNAMIC) { if (site.boardsType() == Site.BoardsType.DYNAMIC) {
List<Board> siteBoards = boardManager.getSiteBoards(site); List<Board> siteBoards = boardManager.getSiteBoards(site);
List<Board> toSearch = new ArrayList<>(); List<Board> allUnsavedBoards = new ArrayList<>();
for (Board siteBoard : siteBoards) { for (Board siteBoard : siteBoards) {
if (!siteBoard.saved) { if (!siteBoard.saved) {
toSearch.add(siteBoard); allUnsavedBoards.add(siteBoard);
} }
} }
List<Board> search = BoardHelper.search(toSearch, query);
for (Board board : search) { List<Board> toSuggest;
if (query == null || query.equals("")) {
toSuggest = new ArrayList<>(allUnsavedBoards.size());
for (Board b : allUnsavedBoards) {
if (b.workSafe) toSuggest.add(b);
}
for (Board b : allUnsavedBoards) {
if (!b.workSafe) toSuggest.add(b);
}
toSuggest = allUnsavedBoards;
} else {
toSuggest = BoardHelper.search(allUnsavedBoards, query);
}
for (Board board : toSuggest) {
BoardSuggestion suggestion = new BoardSuggestion(board); BoardSuggestion suggestion = new BoardSuggestion(board);
suggestions.add(suggestion); suggestions.add(suggestion);
} }
} else { } else {
// TODO if (query != null && !query.equals("")) {
suggestions.add(new BoardSuggestion(null)); suggestions.add(new BoardSuggestion(query));
}
} }
return suggestions; return suggestions;
@ -167,7 +222,7 @@ public class BoardSetupPresenter {
private void updateSuggestions(List<BoardSuggestion> suggestions) { private void updateSuggestions(List<BoardSuggestion> suggestions) {
this.suggestions = suggestions; this.suggestions = suggestions;
for (BoardSuggestion suggestion : this.suggestions) { for (BoardSuggestion suggestion : this.suggestions) {
suggestion.checked = selectedSuggestions.contains(suggestion.board.code); suggestion.checked = selectedSuggestions.contains(suggestion.getCode());
} }
} }
@ -176,7 +231,11 @@ public class BoardSetupPresenter {
void setSavedBoards(List<Board> savedBoards); void setSavedBoards(List<Board> savedBoards);
void showRemovedSnackbar(Board board);
void finish(); void finish();
void boardsWereAdded(int count);
} }
public interface AddCallback { public interface AddCallback {
@ -184,24 +243,47 @@ public class BoardSetupPresenter {
} }
public static class BoardSuggestion { public static class BoardSuggestion {
public final Board board; private final Board board;
private final String code;
private boolean checked = false; private boolean checked = false;
public BoardSuggestion(Board board) { BoardSuggestion(Board board) {
this.board = board; this.board = board;
this.code = board.code;
}
BoardSuggestion(String code) {
this.board = null;
this.code = code;
} }
public String getName() { public String getName() {
return BoardHelper.getName(board); if (board != null) {
return BoardHelper.getName(board);
} else {
return "/" + code + "/";
}
} }
public String getDescription() { public String getDescription() {
return BoardHelper.getDescription(board); if (board != null) {
return BoardHelper.getDescription(board);
} else {
return "";
}
}
public String getCode() {
return code;
} }
public boolean isChecked() { public boolean isChecked() {
return checked; return checked;
} }
public long getId() {
return code.hashCode();
}
} }
} }

@ -0,0 +1,131 @@
/*
* 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.presenter;
import android.util.Pair;
import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.site.Site;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import javax.inject.Inject;
public class BrowsePresenter implements Observer {
private final DatabaseManager databaseManager;
private final BoardManager boardManager;
private Callback callback;
private boolean hadBoards = false;
private Board currentBoard;
private BoardManager.SavedBoards savedBoardsObservable;
@Inject
public BrowsePresenter(DatabaseManager databaseManager, BoardManager boardManager) {
this.databaseManager = databaseManager;
this.boardManager = boardManager;
savedBoardsObservable = boardManager.getSavedBoardsObservable();
hadBoards = hasBoards();
}
public void create(Callback callback) {
this.callback = callback;
savedBoardsObservable.addObserver(this);
}
public void destroy() {
savedBoardsObservable.deleteObserver(this);
}
public Board currentBoard() {
return currentBoard;
}
public void setBoard(Board board) {
loadBoard(board);
}
public void onBoardsFloatingMenuBoardClicked(Board board) {
loadBoard(board);
}
public void loadWithDefaultBoard() {
loadBoard(firstBoard());
}
public void onBoardsFloatingMenuSiteClicked(Site site) {
callback.loadSiteSetup(site);
}
public BoardManager.SavedBoards getSavedBoardsObservable() {
return boardManager.getSavedBoardsObservable();
}
@Override
public void update(Observable o, Object arg) {
if (o == savedBoardsObservable) {
if (!hadBoards && hasBoards()) {
hadBoards = true;
loadWithDefaultBoard();
}
}
}
private boolean hasBoards() {
for (Pair<Site, List<Board>> siteListPair : savedBoardsObservable.get()) {
if (!siteListPair.second.isEmpty()) {
return true;
}
}
return false;
}
private Board firstBoard() {
for (Pair<Site, List<Board>> siteListPair : savedBoardsObservable.get()) {
if (!siteListPair.second.isEmpty()) {
return siteListPair.second.get(0);
}
}
return null;
}
private Loadable getLoadableForBoard(Board board) {
return databaseManager.getDatabaseLoadableManager().get(Loadable.forCatalog(board));
}
private void loadBoard(Board board) {
currentBoard = board;
callback.loadBoard(getLoadableForBoard(board));
}
public interface Callback {
void loadBoard(Loadable loadable);
void loadSiteSetup(Site site);
}
}

@ -53,7 +53,7 @@ public class SitesSetupPresenter {
} }
public boolean mayExit() { public boolean mayExit() {
return false; return sites.size() > 0;
} }
public void onShowDialogClicked() { public void onShowDialogClicked() {

@ -20,6 +20,7 @@ package org.floens.chan.ui.activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.net.Uri;
import android.nfc.NdefMessage; import android.nfc.NdefMessage;
import android.nfc.NfcAdapter; import android.nfc.NfcAdapter;
import android.nfc.NfcEvent; import android.nfc.NfcEvent;
@ -34,6 +35,7 @@ import android.view.ViewGroup;
import org.floens.chan.Chan; import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.chan.ChanHelper;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.controller.NavigationController; import org.floens.chan.controller.NavigationController;
import org.floens.chan.core.database.DatabaseLoadableManager; import org.floens.chan.core.database.DatabaseLoadableManager;
@ -133,7 +135,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat
private void setupFromStateOrFreshLaunch(Bundle savedInstanceState) { private void setupFromStateOrFreshLaunch(Bundle savedInstanceState) {
boolean loadDefault = true; boolean loadDefault = true;
/*if (savedInstanceState != null) { if (savedInstanceState != null) {
// Restore the activity state from the previously saved state. // Restore the activity state from the previously saved state.
ChanState chanState = savedInstanceState.getParcelable(STATE_KEY); ChanState chanState = savedInstanceState.getParcelable(STATE_KEY);
if (chanState == null) { if (chanState == null) {
@ -144,7 +146,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat
if (boardThreadPair != null && boardThreadPair.first != null) { if (boardThreadPair != null && boardThreadPair.first != null) {
loadDefault = false; loadDefault = false;
browseController.loadBoard(boardThreadPair.first.board); browseController.setBoard(boardThreadPair.first.board);
if (boardThreadPair.second != null) { if (boardThreadPair.second != null) {
browseController.showThread(boardThreadPair.second); browseController.showThread(boardThreadPair.second);
@ -158,7 +160,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat
Loadable fromUri = ChanHelper.getLoadableFromStartUri(data); Loadable fromUri = ChanHelper.getLoadableFromStartUri(data);
if (fromUri != null) { if (fromUri != null) {
loadDefault = false; loadDefault = false;
browseController.loadBoard(fromUri.board); browseController.setBoard(fromUri.board);
if (fromUri.isThreadMode()) { if (fromUri.isThreadMode()) {
browseController.showThread(fromUri, false); browseController.showThread(fromUri, false);
@ -175,15 +177,15 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat
.show(); .show();
} }
} }
}*/ }
// Not from a state or from an url, launch the setup controller if no boards are setup up yet, // Not from a state or from an url, launch the setup controller if no boards are setup up yet,
// otherwise load the default saved board. // otherwise load the default saved board.
if (loadDefault) { if (loadDefault) {
if (true || boardManager.getSavedBoards().isEmpty()) { if (boardManager.getSavedBoards().isEmpty()) {
setupWithNoBoards(); setupWithNoBoards();
} else { } else {
browseController.loadDefault(); browseController.loadWithDefaultBoard();
} }
} }
} }

@ -159,11 +159,11 @@ public class DrawerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
LinkHolder linkHolder = (LinkHolder) holder; LinkHolder linkHolder = (LinkHolder) holder;
switch (position) { switch (position) {
case 0: case 0:
linkHolder.text.setText(R.string.settings_board_edit); linkHolder.text.setText(R.string.drawer_sites);
theme().listAddDrawable.apply(linkHolder.image); theme().listAddDrawable.apply(linkHolder.image);
break; break;
case 1: case 1:
linkHolder.text.setText(R.string.history_screen); linkHolder.text.setText(R.string.drawer_history);
theme().historyDrawable.apply(linkHolder.image); theme().historyDrawable.apply(linkHolder.image);
break; break;
} }
@ -400,7 +400,7 @@ public class DrawerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public void onClick(View v) { public void onClick(View v) {
switch (getAdapterPosition()) { switch (getAdapterPosition()) {
case 0: case 0:
callback.openBoardEditor(); callback.openSites();
break; break;
case 1: case 1:
callback.openHistory(); callback.openHistory();
@ -452,7 +452,7 @@ public class DrawerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
void onPinLongClocked(Pin pin); void onPinLongClocked(Pin pin);
void openBoardEditor(); void openSites();
void openHistory(); void openHistory();
} }

@ -1,478 +0,0 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.controller;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.floens.chan.R;
import org.floens.chan.controller.Controller;
import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.model.orm.Board;
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.view.FloatingMenuItem;
import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import static org.floens.chan.Chan.getGraph;
import static org.floens.chan.ui.theme.ThemeHelper.theme;
import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.fixSnackbarText;
import static org.floens.chan.utils.AndroidUtils.getString;
public class BoardEditController extends Controller implements View.OnClickListener, ToolbarMenuItem.ToolbarMenuItemCallback {
private static final int OPTION_SORT_A_Z = 1;
@Inject
BoardManager boardManager;
private RecyclerView recyclerView;
private BoardEditAdapter adapter;
private FloatingActionButton add;
private ItemTouchHelper itemTouchHelper;
private List<Board> boards;
public BoardEditController(Context context) {
super(context);
}
@Override
public void onCreate() {
super.onCreate();
getGraph().inject(this);
navigationItem.setTitle(R.string.board_edit);
List<FloatingMenuItem> items = new ArrayList<>();
items.add(new FloatingMenuItem(OPTION_SORT_A_Z, R.string.board_edit_sort_a_z));
navigationItem.menu = new ToolbarMenu(context);
navigationItem.createOverflow(context, this, items);
navigationItem.swipeable = false;
view = inflateRes(R.layout.controller_board_edit);
recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
add = (FloatingActionButton) view.findViewById(R.id.add);
add.setOnClickListener(this);
theme().applyFabColor(add);
boards = boardManager.getSavedBoards();
adapter = new BoardEditAdapter();
recyclerView.setAdapter(adapter);
itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
boolean isBoardItem = viewHolder.getAdapterPosition() > 0;
int dragFlags = isBoardItem ? ItemTouchHelper.UP | ItemTouchHelper.DOWN : 0;
int swipeFlags = isBoardItem ? ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT : 0;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
if (to > 0) {
Board item = boards.remove(from - 1);
boards.add(to - 1, item);
adapter.notifyItemMoved(from, to);
return true;
} else {
return false;
}
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
final int position = viewHolder.getAdapterPosition();
final Board board = boards.get(position - 1);
board.saved = false;
boards.remove(position - 1);
adapter.notifyItemRemoved(position);
Snackbar snackbar = Snackbar.make(view, context.getString(R.string.board_edit_board_removed, board.name), Snackbar.LENGTH_LONG);
fixSnackbarText(context, snackbar);
snackbar.setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
board.saved = true;
boards.add(position - 1, board);
adapter.notifyDataSetChanged();
}
});
snackbar.show();
}
});
itemTouchHelper.attachToRecyclerView(recyclerView);
}
@Override
public void onMenuItemClicked(ToolbarMenuItem item) {
}
@Override
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
if (((Integer) item.getId()) == OPTION_SORT_A_Z) {
Collections.sort(boards, new Comparator<Board>() {
@Override
public int compare(Board lhs, Board rhs) {
return lhs.code.compareTo(rhs.code);
}
});
adapter.notifyDataSetChanged();
}
}
@Override
public void onDestroy() {
super.onDestroy();
for (int i = 0; i < boards.size(); i++) {
boards.get(i).order = i;
}
// TODO(multisite)
// boardManager.flushOrderAndSaved();
}
@Override
public void onClick(View v) {
if (v == add) {
showAddBoardDialog();
}
}
private void showAddBoardDialog() {
LinearLayout wrap = new LinearLayout(context);
wrap.setPadding(dp(16), dp(16), dp(16), 0);
final AutoCompleteTextView text = new AutoCompleteTextView(context);
text.setSingleLine();
wrap.addView(text, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
FillAdapter fillAdapter = new FillAdapter(context, 0);
fillAdapter.setEditingList(boards);
fillAdapter.setAutoCompleteView(text);
text.setAdapter(fillAdapter);
text.setThreshold(1);
text.setDropDownHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
text.setHint(R.string.board_add_hint);
text.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN);
AlertDialog dialog = new AlertDialog.Builder(context)
.setPositiveButton(R.string.add, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface d, int which) {
String value = text.getText().toString();
if (!TextUtils.isEmpty(value)) {
addBoard(value.toLowerCase(Locale.ENGLISH));
}
}
}).setNegativeButton(R.string.cancel, null)
.setTitle(R.string.board_add)
.setView(wrap)
.create();
AndroidUtils.requestKeyboardFocus(dialog, text);
dialog.show();
}
private void addBoard(String value) {
value = value.replace(" ", "");
value = value.replace("/", "");
value = value.replace("\\", "");
// Duplicate
for (Board board : boards) {
if (board.code.equals(value)) {
new AlertDialog.Builder(context).setMessage(R.string.board_add_duplicate).setPositiveButton(R.string.ok, null).show();
return;
}
}
// Normal add
List<Board> all = boardManager.getSavedBoards();
for (Board board : all) {
if (board.code.equals(value)) {
board.saved = true;
boards.add(board);
adapter.notifyDataSetChanged();
recyclerView.smoothScrollToPosition(boards.size());
Snackbar snackbar = Snackbar.make(view, getString(R.string.board_add_success) + " " + board.name, Snackbar.LENGTH_LONG);
fixSnackbarText(context, snackbar);
snackbar.show();
return;
}
}
// Unknown
new AlertDialog.Builder(context)
.setTitle(R.string.board_add_unknown_title)
.setMessage(context.getString(R.string.board_add_unknown, value))
.setPositiveButton(R.string.ok, null)
.show();
}
private class FillAdapter extends ArrayAdapter<String> implements Filterable {
private List<Board> currentlyEditing;
private View autoCompleteView;
private final Filter filter;
private final List<Board> filtered = new ArrayList<>();
public FillAdapter(Context context, int resource) {
super(context, resource);
filter = new Filter() {
@Override
protected synchronized FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
if (TextUtils.isEmpty(constraint) || (constraint.toString().startsWith(" "))) {
results.values = null;
results.count = 0;
} else {
List<Board> keys = getFiltered(constraint.toString());
results.values = keys;
results.count = keys.size();
}
return results;
}
@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
filtered.clear();
if (results.values != null) {
filtered.addAll((List<Board>) results.values);
} else {
filtered.addAll(getBoards());
}
notifyDataSetChanged();
}
};
}
public void setEditingList(List<Board> list) {
currentlyEditing = list;
}
public void setAutoCompleteView(View autoCompleteView) {
this.autoCompleteView = autoCompleteView;
}
@Override
public int getCount() {
return filtered.size();
}
@Override
public String getItem(int position) {
return filtered.get(position).code;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
@SuppressLint("ViewHolder")
TextView view = (TextView) LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);
Board b = filtered.get(position);
view.setText("/" + b.code + "/ - " + b.name);
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
AndroidUtils.hideKeyboard(autoCompleteView);
}
return false;
}
});
return view;
}
@Override
public Filter getFilter() {
return filter;
}
private List<Board> getFiltered(String filter) {
String lowered = filter.toLowerCase(Locale.ENGLISH);
List<Board> list = new ArrayList<>();
for (Board b : getBoards()) {
if ((b.name.toLowerCase(Locale.ENGLISH).contains(lowered) || b.code.toLowerCase(Locale.ENGLISH)
.contains(lowered))) {
list.add(b);
}
}
return list;
}
private boolean haveBoard(String value) {
for (Board b : currentlyEditing) {
if (b.code.equals(value))
return true;
}
return false;
}
private List<Board> getBoards() {
// Lets be cheaty here: if the user has nsfw boards in the list,
// show them in the autofiller, hide them otherwise.
/*boolean showUnsafe = false;
for (Board has : currentlyEditing) {
if (!has.workSafe) {
showUnsafe = true;
break;
}
}*/
List<Board> s = new ArrayList<>();
for (Board b : boardManager.getSavedBoards()) {
if (!haveBoard(b.code)/* && (showUnsafe || b.workSafe)*/) {
s.add(b);
}
}
return s;
}
}
private class BoardEditAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private int TYPE_ITEM = 0;
private int TYPE_HEADER = 1;
public BoardEditAdapter() {
setHasStableIds(true);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_ITEM) {
return new BoardEditItem(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_board_edit, parent, false));
} else {
return new BoardEditHeader(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_board_edit_header, parent, false));
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == TYPE_HEADER) {
BoardEditHeader header = (BoardEditHeader) holder;
header.text.setText(R.string.board_edit_header);
} else {
BoardEditItem item = (BoardEditItem) holder;
Board board = boards.get(position - 1);
item.text.setText(BoardHelper.getName(board));
item.description.setText(BoardHelper.getDescription(board));
}
}
@Override
public int getItemViewType(int position) {
return position == 0 ? TYPE_HEADER : TYPE_ITEM;
}
@Override
public int getItemCount() {
return boards.size() + 1;
}
@Override
public long getItemId(int position) {
if (getItemViewType(position) == TYPE_HEADER) {
return -1;
} else {
return boards.get(position - 1).id;
}
}
}
private class BoardEditItem extends RecyclerView.ViewHolder {
private ImageView thumb;
private TextView text;
private TextView description;
public BoardEditItem(View itemView) {
super(itemView);
thumb = (ImageView) itemView.findViewById(R.id.thumb);
text = (TextView) itemView.findViewById(R.id.text);
description = (TextView) itemView.findViewById(R.id.description);
thumb.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
itemTouchHelper.startDrag(BoardEditItem.this);
}
return false;
}
});
}
}
private class BoardEditHeader extends RecyclerView.ViewHolder {
private TextView text;
public BoardEditHeader(View itemView) {
super(itemView);
text = (TextView) itemView.findViewById(R.id.text);
text.setTypeface(AndroidUtils.ROBOTO_MEDIUM_ITALIC);
}
}
}

@ -23,6 +23,7 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
@ -51,6 +52,7 @@ import javax.inject.Inject;
import static org.floens.chan.Chan.getGraph; import static org.floens.chan.Chan.getGraph;
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.fixSnackbarText;
import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.getAttrColor;
public class BoardSetupController extends Controller implements View.OnClickListener, BoardSetupPresenter.Callback { public class BoardSetupController extends Controller implements View.OnClickListener, BoardSetupPresenter.Callback {
@ -104,6 +106,7 @@ public class BoardSetupController extends Controller implements View.OnClickList
// Navigation // Navigation
navigationItem.title = context.getString(R.string.setup_board_title, site.name()); navigationItem.title = context.getString(R.string.setup_board_title, site.name());
navigationItem.swipeable = false;
// View binding // View binding
savedBoardsRecycler = view.findViewById(R.id.boards_recycler); savedBoardsRecycler = view.findViewById(R.id.boards_recycler);
@ -168,6 +171,34 @@ public class BoardSetupController extends Controller implements View.OnClickList
savedAdapter.setSavedBoards(savedBoards); savedAdapter.setSavedBoards(savedBoards);
} }
@Override
public void showRemovedSnackbar(final Board board) {
Snackbar snackbar = Snackbar.make(view,
context.getString(R.string.setup_board_removed, BoardHelper.getName(board)),
Snackbar.LENGTH_LONG);
fixSnackbarText(context, snackbar);
snackbar.setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
presenter.undoRemoveBoard(board);
}
});
snackbar.show();
}
@Override
public void boardsWereAdded(int count) {
savedBoardsRecycler.smoothScrollToPosition(savedAdapter.getItemCount());
String boardText = context.getResources().getQuantityString(R.plurals.board, count, count);
String text = context.getString(R.string.setup_board_added, boardText);
Snackbar snackbar = Snackbar.make(view, text, Snackbar.LENGTH_LONG);
fixSnackbarText(context, snackbar);
snackbar.show();
}
@Override @Override
public void finish() { public void finish() {
navigationController.popController(); navigationController.popController();

@ -20,29 +20,26 @@ package org.floens.chan.ui.controller;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.view.LayoutInflater;
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.BaseAdapter;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.chan.ChanUrls;
import org.floens.chan.controller.Controller;
import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.model.orm.Board; 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.model.orm.Pin; import org.floens.chan.core.model.orm.Pin;
import org.floens.chan.core.presenter.BrowsePresenter;
import org.floens.chan.core.presenter.ThreadPresenter; 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.core.site.Site;
import org.floens.chan.ui.adapter.PostsFilter; 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.ThreadLayout; import org.floens.chan.ui.layout.ThreadLayout;
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.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;
@ -55,7 +52,7 @@ import javax.inject.Inject;
import static org.floens.chan.Chan.getGraph; import static org.floens.chan.Chan.getGraph;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class BrowseController extends ThreadController implements ToolbarMenuItem.ToolbarMenuItemCallback, ThreadLayout.ThreadLayoutCallback, FloatingMenu.FloatingMenuCallback { public class BrowseController extends ThreadController implements ToolbarMenuItem.ToolbarMenuItemCallback, ThreadLayout.ThreadLayoutCallback, BrowsePresenter.Callback {
private static final int SEARCH_ID = 1; private static final int SEARCH_ID = 1;
private static final int REFRESH_ID = 2; private static final int REFRESH_ID = 2;
private static final int REPLY_ID = 101; private static final int REPLY_ID = 101;
@ -65,14 +62,10 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
private static final int OPEN_BROWSER_ID = 106; private static final int OPEN_BROWSER_ID = 106;
@Inject @Inject
DatabaseManager databaseManager; BrowsePresenter presenter;
@Inject
BoardManager boardManager;
private ChanSettings.PostViewMode postViewMode; private ChanSettings.PostViewMode postViewMode;
private PostsFilter.Order order; private PostsFilter.Order order;
private List<FloatingMenuItem> boardItems;
private FloatingMenuItem viewModeMenuItem; private FloatingMenuItem viewModeMenuItem;
private ToolbarMenuItem search; private ToolbarMenuItem search;
@ -88,16 +81,38 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
super.onCreate(); super.onCreate();
getGraph().inject(this); getGraph().inject(this);
// Initialization
postViewMode = ChanSettings.boardViewMode.get(); postViewMode = ChanSettings.boardViewMode.get();
order = PostsFilter.Order.find(ChanSettings.boardOrder.get()); order = PostsFilter.Order.find(ChanSettings.boardOrder.get());
threadLayout.setPostViewMode(postViewMode); threadLayout.setPostViewMode(postViewMode);
threadLayout.getPresenter().setOrder(order); threadLayout.getPresenter().setOrder(order);
// Navigation
initNavigation();
}
@Override
public void onDestroy() {
super.onDestroy();
presenter.destroy();
}
public void setBoard(Board board) {
presenter.setBoard(board);
}
public void loadWithDefaultBoard() {
presenter.loadWithDefaultBoard();
}
private void initNavigation() {
// Navigation item
navigationItem.hasDrawer = true; navigationItem.hasDrawer = true;
navigationItem.middleMenu = new FloatingMenu(context);
navigationItem.middleMenu.setCallback(this);
loadBoards();
setupMiddleNavigation();
// Toolbar menu
ToolbarMenu menu = new ToolbarMenu(context); ToolbarMenu menu = new ToolbarMenu(context);
navigationItem.menu = menu; navigationItem.menu = menu;
navigationItem.hasBack = false; navigationItem.hasBack = false;
@ -105,6 +120,7 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
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
overflow = menu.createOverflow(this); overflow = menu.createOverflow(this);
List<FloatingMenuItem> items = new ArrayList<>(); List<FloatingMenuItem> items = new ArrayList<>();
@ -119,6 +135,32 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
items.add(new FloatingMenuItem(OPEN_BROWSER_ID, R.string.action_open_browser)); items.add(new FloatingMenuItem(OPEN_BROWSER_ID, R.string.action_open_browser));
overflow.setSubMenu(new FloatingMenu(context, overflow.getView(), items)); overflow.setSubMenu(new FloatingMenu(context, overflow.getView(), items));
// Presenter
presenter.create(this);
}
private void setupMiddleNavigation() {
navigationItem.middleMenu = new ToolbarMiddleMenu() {
@SuppressLint("InflateParams")
@Override
public void show(View anchor) {
BrowseBoardsFloatingMenu boardsFloatingMenu =
new BrowseBoardsFloatingMenu(presenter.getSavedBoardsObservable());
boardsFloatingMenu.show(anchor, presenter.currentBoard());
boardsFloatingMenu.setCallback(new BrowseBoardsFloatingMenu.Callback() {
@Override
public void onBoardClicked(Board item) {
presenter.onBoardsFloatingMenuBoardClicked(item);
}
@Override
public void onSiteClicked(Site site) {
presenter.onBoardsFloatingMenuSiteClicked(site);
}
});
}
};
} }
@Override @Override
@ -152,104 +194,126 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
Integer id = (Integer) item.getId(); Integer id = (Integer) item.getId();
switch (id) { switch (id) {
case REPLY_ID: case REPLY_ID:
threadLayout.openReply(true); handleReply();
break; break;
case SHARE_ID: case SHARE_ID:
case OPEN_BROWSER_ID: case OPEN_BROWSER_ID:
if (presenter.isBound()) { handleShareAndOpenInBrowser(presenter, id);
String link = ChanUrls.getCatalogUrlDesktop(presenter.getLoadable().boardCode);
if (id == SHARE_ID) {
AndroidUtils.shareLink(link);
} else {
AndroidUtils.openLinkInBrowser((Activity) context, link);
}
}
break; break;
case VIEW_MODE_ID: case VIEW_MODE_ID:
if (postViewMode == ChanSettings.PostViewMode.LIST) { handleViewMode();
postViewMode = ChanSettings.PostViewMode.CARD;
} else {
postViewMode = ChanSettings.PostViewMode.LIST;
}
ChanSettings.boardViewMode.set(postViewMode);
viewModeMenuItem.setText(context.getString(
postViewMode == ChanSettings.PostViewMode.LIST ? R.string.action_switch_catalog : R.string.action_switch_board));
threadLayout.setPostViewMode(postViewMode);
break; break;
case ORDER_ID: case ORDER_ID:
List<FloatingMenuItem> items = new ArrayList<>(); handleOrder(presenter);
for (PostsFilter.Order order : PostsFilter.Order.values()) {
int nameId = 0;
switch (order) {
case BUMP:
nameId = R.string.order_bump;
break;
case REPLY:
nameId = R.string.order_reply;
break;
case IMAGE:
nameId = R.string.order_image;
break;
case NEWEST:
nameId = R.string.order_newest;
break;
case OLDEST:
nameId = R.string.order_oldest;
break;
}
String name = getString(nameId); break;
if (order == this.order) { }
name = "\u2713 " + name; // Checkmark }
}
items.add(new FloatingMenuItem(order, name)); private void handleReply() {
} threadLayout.openReply(true);
}
FloatingMenu menu = new FloatingMenu(context, overflow.getView(), items); private void handleShareAndOpenInBrowser(ThreadPresenter presenter, Integer id) {
menu.setCallback(new FloatingMenu.FloatingMenuCallback() { if (presenter.isBound()) {
@Override String link = ChanUrls.getCatalogUrlDesktop(presenter.getLoadable().boardCode);
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
PostsFilter.Order order = (PostsFilter.Order) item.getId();
ChanSettings.boardOrder.set(order.name);
BrowseController.this.order = order;
presenter.setOrder(order);
}
@Override if (id == SHARE_ID) {
public void onFloatingMenuDismissed(FloatingMenu menu) { AndroidUtils.shareLink(link);
} } else {
}); AndroidUtils.openLinkInBrowser((Activity) context, link);
menu.show(); }
}
}
break; private void handleViewMode() {
if (postViewMode == ChanSettings.PostViewMode.LIST) {
postViewMode = ChanSettings.PostViewMode.CARD;
} else {
postViewMode = ChanSettings.PostViewMode.LIST;
} }
ChanSettings.boardViewMode.set(postViewMode);
int viewModeText = postViewMode == ChanSettings.PostViewMode.LIST ?
R.string.action_switch_catalog : R.string.action_switch_board;
viewModeMenuItem.setText(context.getString(viewModeText));
threadLayout.setPostViewMode(postViewMode);
} }
@Override private void handleOrder(final ThreadPresenter presenter) {
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) { List<FloatingMenuItem> items = new ArrayList<>();
if (menu == navigationItem.middleMenu) { for (PostsFilter.Order order : PostsFilter.Order.values()) {
if (item instanceof FloatingMenuItemBoard) { int nameId = 0;
loadBoard(((FloatingMenuItemBoard) item).board); switch (order) {
} else { case BUMP:
Controller boardEditController = new BoardSetupController(context); nameId = R.string.order_bump;
if (doubleNavigationController != null) { break;
doubleNavigationController.pushController(boardEditController); case REPLY:
} else { nameId = R.string.order_reply;
navigationController.pushController(boardEditController); break;
} case IMAGE:
menu.dismiss(); nameId = R.string.order_image;
break;
case NEWEST:
nameId = R.string.order_newest;
break;
case OLDEST:
nameId = R.string.order_oldest;
break;
}
String name = getString(nameId);
if (order == this.order) {
name = "\u2713 " + name; // Checkmark
} }
items.add(new FloatingMenuItem(order, name));
} }
FloatingMenu menu = new FloatingMenu(context, overflow.getView(), items);
menu.setCallback(new FloatingMenu.FloatingMenuCallback() {
@Override
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
PostsFilter.Order order = (PostsFilter.Order) item.getId();
ChanSettings.boardOrder.set(order.name);
BrowseController.this.order = order;
presenter.setOrder(order);
}
@Override
public void onFloatingMenuDismissed(FloatingMenu menu) {
}
});
menu.show();
}
@Override
public void loadBoard(Loadable loadable) {
String name = BoardHelper.getName(loadable.board);
loadable.title = name;
navigationItem.title = name;
ThreadPresenter presenter = threadLayout.getPresenter();
presenter.unbindLoadable();
presenter.bindLoadable(loadable);
presenter.requestData();
((ToolbarNavigationController) navigationController).toolbar.updateTitle(navigationItem);
} }
@Override @Override
public void onFloatingMenuDismissed(FloatingMenu menu) { public void loadSiteSetup(Site site) {
SiteSetupController siteSetupController = new SiteSetupController(context);
siteSetupController.setSite(site);
if (doubleNavigationController != null) {
doubleNavigationController.pushController(siteSetupController);
} else {
navigationController.pushController(siteSetupController);
}
} }
@Override @Override
@ -316,110 +380,4 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
navigationController.pushController(viewThreadController, animated); navigationController.pushController(viewThreadController, animated);
} }
} }
public void onEvent(BoardManager.BoardsChangedMessage event) {
loadBoards();
}
public void loadDefault() {
List<Board> savedBoards = boardManager.getSavedBoards();
if (!savedBoards.isEmpty()) {
loadBoard(savedBoards.get(0));
}
}
public void loadBoard(Board board) {
Loadable loadable = databaseManager.getDatabaseLoadableManager().get(Loadable.forCatalog(board));
loadable.title = BoardHelper.getName(board);
navigationItem.title = BoardHelper.getName(board);
ThreadPresenter presenter = threadLayout.getPresenter();
presenter.unbindLoadable();
presenter.bindLoadable(loadable);
presenter.requestData();
for (FloatingMenuItem item : boardItems) {
if (((FloatingMenuItemBoard) item).board == board) {
navigationItem.middleMenu.setSelectedItem(item);
break;
}
}
((ToolbarNavigationController) navigationController).toolbar.updateTitle(navigationItem);
}
/**
* Load the menu with saved boards. Called on creation of this controller and when there's a
* board change event.
*/
private void loadBoards() {
List<Board> boards = boardManager.getSavedBoards();
boolean wasEmpty = boardItems == null || boardItems.isEmpty();
boardItems = new ArrayList<>();
for (Board board : boards) {
FloatingMenuItem item = new FloatingMenuItemBoard(board);
boardItems.add(item);
}
navigationItem.middleMenu.setItems(boardItems);
navigationItem.middleMenu.setAdapter(new BoardsAdapter(context, boardItems));
if (wasEmpty) {
loadDefault();
}
}
private static class FloatingMenuItemBoard extends FloatingMenuItem {
public Board board;
public FloatingMenuItemBoard(Board board) {
super(board.id, BoardHelper.getName(board));
this.board = board;
}
}
private static class BoardsAdapter extends BaseAdapter {
private final Context context;
private List<FloatingMenuItem> items;
public BoardsAdapter(Context context, List<FloatingMenuItem> items) {
this.context = context;
this.items = items;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// No recycling, can't use itemtypes
@SuppressLint("ViewHolder")
TextView textView = (TextView) LayoutInflater.from(context).inflate(R.layout.toolbar_menu_item, parent, false);
textView.setText(getItem(position));
if (position < items.size()) {
textView.setTypeface(AndroidUtils.ROBOTO_MEDIUM);
} else {
textView.setTypeface(AndroidUtils.ROBOTO_MEDIUM_ITALIC);
}
return textView;
}
@Override
public int getCount() {
return items.size() + 1;
}
@Override
public String getItem(int position) {
if (position >= 0 && position < items.size()) {
return items.get(position).getText();
} else {
return context.getString(R.string.thread_board_select_add);
}
}
@Override
public long getItemId(int position) {
return position;
}
}
} }

@ -224,8 +224,8 @@ public class DrawerController extends Controller implements DrawerAdapter.Callba
} }
@Override @Override
public void openBoardEditor() { public void openSites() {
openController(new BoardEditController(context)); openController(new SitesSetupController(context));
} }
@Override @Override

@ -31,10 +31,10 @@ import android.widget.Toast;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Sites; import org.floens.chan.core.site.Sites;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.animation.AnimationUtils;
import org.floens.chan.ui.helper.HintPopup; import org.floens.chan.ui.helper.HintPopup;
import org.floens.chan.ui.helper.RefreshUIMessage; import org.floens.chan.ui.helper.RefreshUIMessage;
import org.floens.chan.ui.settings.BooleanSettingView; import org.floens.chan.ui.settings.BooleanSettingView;
@ -48,7 +48,6 @@ 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.FloatingMenuItem;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.ui.animation.AnimationUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -67,7 +66,6 @@ public class MainSettingsController extends SettingsController implements Toolba
private ListSettingView<ChanSettings.MediaAutoLoadMode> imageAutoLoadView; private ListSettingView<ChanSettings.MediaAutoLoadMode> imageAutoLoadView;
private ListSettingView<ChanSettings.MediaAutoLoadMode> videoAutoLoadView; private ListSettingView<ChanSettings.MediaAutoLoadMode> videoAutoLoadView;
private LinkSettingView boardEditorView;
private LinkSettingView saveLocation; private LinkSettingView saveLocation;
private LinkSettingView watchLink; private LinkSettingView watchLink;
private LinkSettingView passLink; private LinkSettingView passLink;
@ -154,10 +152,6 @@ public class MainSettingsController extends SettingsController implements Toolba
} }
} }
public void onEvent(BoardManager.BoardsChangedMessage message) {
updateBoardLinkDescription();
}
public void onEvent(ChanSettings.SettingChanged setting) { public void onEvent(ChanSettings.SettingChanged setting) {
if (setting.setting == ChanSettings.saveLocation) { if (setting.setting == ChanSettings.saveLocation) {
setSaveLocationDescription(); setSaveLocationDescription();
@ -203,14 +197,6 @@ public class MainSettingsController extends SettingsController implements Toolba
private void populatePreferences() { private void populatePreferences() {
// General group // General group
SettingsGroup general = new SettingsGroup(R.string.settings_group_general); SettingsGroup general = new SettingsGroup(R.string.settings_group_general);
boardEditorView = new LinkSettingView(this, R.string.settings_board_edit, 0, new View.OnClickListener() {
@Override
public void onClick(View v) {
navigationController.pushController(new BoardEditController(context));
}
});
general.add(boardEditorView);
updateBoardLinkDescription();
watchLink = (LinkSettingView) general.add(new LinkSettingView(this, R.string.settings_watch, 0, new View.OnClickListener() { watchLink = (LinkSettingView) general.add(new LinkSettingView(this, R.string.settings_watch, 0, new View.OnClickListener() {
@Override @Override
@ -446,11 +432,6 @@ public class MainSettingsController extends SettingsController implements Toolba
groups.add(about); groups.add(about);
} }
private void updateBoardLinkDescription() {
List<Board> savedBoards = boardManager.getSavedBoards();
boardEditorView.setDescription(context.getResources().getQuantityString(R.plurals.board, savedBoards.size(), savedBoards.size()));
}
private void setSaveLocationDescription() { private void setSaveLocationDescription() {
saveLocation.setDescription(ChanSettings.saveLocation.get()); saveLocation.setDescription(ChanSettings.saveLocation.get());
} }

@ -66,7 +66,9 @@ public class SetupController extends ToolbarNavigationController implements Setu
@Override @Override
public void moveToSiteSetup() { public void moveToSiteSetup() {
replaceController(new SitesSetupController(context), true); SitesSetupController sitesSetupController = new SitesSetupController(context);
sitesSetupController.showDoneCheckmark();
replaceController(sitesSetupController, true);
} }
private void replaceController(Controller to, boolean showToolbar) { private void replaceController(Controller to, boolean showToolbar) {

@ -79,11 +79,6 @@ public class SitesSetupController extends StyledToolbarNavigationController impl
// Navigation // Navigation
navigationItem.setTitle(R.string.setup_sites_title); navigationItem.setTitle(R.string.setup_sites_title);
navigationItem.swipeable = false;
navigationItem.menu = new ToolbarMenu(context);
doneMenuItem = navigationItem.menu.addItem(
new ToolbarMenuItem(context, this, DONE_ID, 0, R.drawable.ic_done_white_24dp));
doneMenuItem.getView().setAlpha(0f);
// View binding // View binding
sitesRecyclerview = view.findViewById(R.id.sites_recycler); sitesRecyclerview = view.findViewById(R.id.sites_recycler);
@ -102,6 +97,14 @@ public class SitesSetupController extends StyledToolbarNavigationController impl
presenter.create(this); presenter.create(this);
} }
public void showDoneCheckmark() {
navigationItem.swipeable = false;
navigationItem.menu = new ToolbarMenu(context);
doneMenuItem = navigationItem.menu.addItem(
new ToolbarMenuItem(context, this, DONE_ID, 0, R.drawable.ic_done_white_24dp));
doneMenuItem.getView().setAlpha(0f);
}
@Override @Override
public void onMenuItemClicked(ToolbarMenuItem item) { public void onMenuItemClicked(ToolbarMenuItem item) {
if ((Integer) item.getId() == DONE_ID) { if ((Integer) item.getId() == DONE_ID) {
@ -171,7 +174,9 @@ public class SitesSetupController extends StyledToolbarNavigationController impl
@Override @Override
public void setNextAllowed(boolean nextAllowed, boolean animate) { public void setNextAllowed(boolean nextAllowed, boolean animate) {
doneMenuItem.getView().animate().alpha(nextAllowed ? 1f : 0f).start(); if (doneMenuItem != null) {
doneMenuItem.getView().animate().alpha(nextAllowed ? 1f : 0f).start();
}
} }
private void onSiteCellSettingsClicked(Site site) { private void onSiteCellSettingsClicked(Site site) {

@ -20,7 +20,6 @@ package org.floens.chan.ui.helper;
import android.util.Pair; import android.util.Pair;
import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Board;
import org.floens.chan.utils.Logger;
import org.jsoup.parser.Parser; import org.jsoup.parser.Parser;
import java.util.ArrayList; import java.util.ArrayList;
@ -70,8 +69,6 @@ public class BoardHelper {
int name = FuzzySearch.ratio(board.name, query); int name = FuzzySearch.ratio(board.name, query);
int description = FuzzySearch.weightedRatio(String.valueOf(getDescription(board)), query); int description = FuzzySearch.weightedRatio(String.valueOf(getDescription(board)), query);
Logger.d(TAG, board.code + " = code = " + code + ", name = " + name + ", desc = " + description);
return code * 4 + return code * 4 +
name * 5 + name * 5 +
Math.max(0, description - 30) * 8; Math.max(0, description - 30) * 8;

@ -25,14 +25,13 @@ import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.CheckBox;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.presenter.BoardSetupPresenter; import org.floens.chan.core.presenter.BoardSetupPresenter;
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;
@ -75,22 +74,22 @@ public class BoardAddLayout extends LinearLayout implements SearchLayout.SearchL
search.setTextColor(getAttrColor(getContext(), R.attr.text_color_primary)); search.setTextColor(getAttrColor(getContext(), R.attr.text_color_primary));
search.setHintColor(getAttrColor(getContext(), R.attr.text_color_hint)); search.setHintColor(getAttrColor(getContext(), R.attr.text_color_hint));
search.setClearButtonImage(R.drawable.ic_clear_black_24dp); search.setClearButtonImage(R.drawable.ic_clear_black_24dp);
search.openKeyboard();
suggestionsRecycler.setLayoutManager(new LinearLayoutManager(getContext())); suggestionsRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
suggestionsRecycler.setAdapter(suggestionsAdapter); suggestionsRecycler.setAdapter(suggestionsAdapter);
suggestionsRecycler.requestFocus();
} }
@Override @Override
protected void onAttachedToWindow() { protected void onAttachedToWindow() {
super.onAttachedToWindow(); super.onAttachedToWindow();
presenter.setAddCallback(this); presenter.bindAddDialog(this);
} }
@Override @Override
protected void onDetachedFromWindow() { protected void onDetachedFromWindow() {
super.onDetachedFromWindow(); super.onDetachedFromWindow();
presenter.setAddCallback(null); presenter.unbindAddDialog();
} }
@Override @Override
@ -120,6 +119,15 @@ public class BoardAddLayout extends LinearLayout implements SearchLayout.SearchL
} }
private class SuggestionsAdapter extends RecyclerView.Adapter<SuggestionCell> { private class SuggestionsAdapter extends RecyclerView.Adapter<SuggestionCell> {
public SuggestionsAdapter() {
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
return presenter.getSuggestions().get(position).getId();
}
@Override @Override
public int getItemCount() { public int getItemCount() {
return presenter.getSuggestions().size(); return presenter.getSuggestions().size();
@ -138,14 +146,13 @@ public class BoardAddLayout extends LinearLayout implements SearchLayout.SearchL
holder.setSuggestion(boardSuggestion); holder.setSuggestion(boardSuggestion);
holder.text.setText(boardSuggestion.getName()); holder.text.setText(boardSuggestion.getName());
holder.description.setText(boardSuggestion.getDescription()); holder.description.setText(boardSuggestion.getDescription());
holder.check.setVisibility(boardSuggestion.isChecked() ? View.VISIBLE : View.INVISIBLE);
} }
} }
private class SuggestionCell extends RecyclerView.ViewHolder implements OnClickListener { private class SuggestionCell extends RecyclerView.ViewHolder implements OnClickListener {
private TextView text; private TextView text;
private TextView description; private TextView description;
private ImageView check; private CheckBox check;
private BoardSetupPresenter.BoardSuggestion suggestion; private BoardSetupPresenter.BoardSuggestion suggestion;
@ -155,19 +162,20 @@ public class BoardAddLayout extends LinearLayout implements SearchLayout.SearchL
text = itemView.findViewById(R.id.text); text = itemView.findViewById(R.id.text);
description = itemView.findViewById(R.id.description); description = itemView.findViewById(R.id.description);
check = itemView.findViewById(R.id.check); check = itemView.findViewById(R.id.check);
theme().doneDrawable.apply(check);
itemView.setOnClickListener(this); itemView.setOnClickListener(this);
} }
public void setSuggestion(BoardSetupPresenter.BoardSuggestion suggestion) { public void setSuggestion(BoardSetupPresenter.BoardSuggestion suggestion) {
this.suggestion = suggestion; this.suggestion = suggestion;
check.setChecked(suggestion.isChecked());
} }
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (v == itemView) { if (v == itemView) {
onSuggestionClicked(suggestion); onSuggestionClicked(suggestion);
check.setChecked(suggestion.isChecked());
} }
} }
} }

@ -0,0 +1,237 @@
/*
* 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.layout;
import android.annotation.SuppressLint;
import android.graphics.drawable.Drawable;
import android.util.Pair;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import org.floens.chan.R;
import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteIcon;
import org.floens.chan.ui.helper.BoardHelper;
import org.floens.chan.ui.view.FloatingMenu;
import org.floens.chan.utils.AndroidUtils;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import static org.floens.chan.utils.AndroidUtils.dp;
public class BrowseBoardsFloatingMenu implements Observer, AdapterView.OnItemClickListener {
private FloatingMenu floatingMenu;
private BoardManager.SavedBoards savedBoards;
private BrowseBoardsAdapter adapter;
private Callback callback;
public BrowseBoardsFloatingMenu(BoardManager.SavedBoards savedBoards) {
this.savedBoards = savedBoards;
this.savedBoards.addObserver(this);
}
private void onDismissed() {
savedBoards.deleteObserver(this);
}
public void setCallback(Callback callback) {
this.callback = callback;
}
public void show(View anchor, Board selectedBoard) {
floatingMenu = new FloatingMenu(anchor.getContext());
floatingMenu.setCallback(new FloatingMenu.FloatingMenuCallbackAdapter() {
@Override
public void onFloatingMenuDismissed(FloatingMenu menu) {
onDismissed();
}
});
floatingMenu.setManageItems(false);
floatingMenu.setAnchor(anchor, Gravity.LEFT, dp(5), dp(5));
floatingMenu.setPopupWidth(FloatingMenu.POPUP_WIDTH_ANCHOR);
adapter = new BrowseBoardsAdapter();
floatingMenu.setAdapter(adapter);
floatingMenu.setOnItemClickListener(this);
floatingMenu.setSelectedPosition(resolveCurrentIndex(selectedBoard));
floatingMenu.show();
}
@Override
public void update(Observable o, Object arg) {
if (o == savedBoards) {
adapter.notifyDataSetChanged();
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Pair<Site, Board> siteOrBoard = getAtPosition(position);
if (siteOrBoard.second != null) {
callback.onBoardClicked(siteOrBoard.second);
} else {
callback.onSiteClicked(siteOrBoard.first);
}
floatingMenu.dismiss();
}
private int getCount() {
int count = 0;
for (Pair<Site, List<Board>> siteListPair : savedBoards.get()) {
count += 1;
count += siteListPair.second.size();
}
return count;
}
private int resolveCurrentIndex(Board board) {
int position = 0;
for (Pair<Site, List<Board>> siteListPair : savedBoards.get()) {
position += 1;
for (Board other : siteListPair.second) {
if (board == other) {
return position;
}
position++;
}
}
return 0;
}
private Pair<Site, Board> getAtPosition(int position) {
for (Pair<Site, List<Board>> siteListPair : savedBoards.get()) {
if (position == 0) {
return new Pair<>(siteListPair.first, null);
}
position -= 1;
if (position < siteListPair.second.size()) {
return new Pair<>(null, siteListPair.second.get(position));
}
position -= siteListPair.second.size();
}
throw new IllegalArgumentException();
}
private class BrowseBoardsAdapter extends BaseAdapter {
final int TYPE_SITE = 0;
final int TYPE_BOARD = 1;
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public int getItemViewType(int position) {
Pair<Site, Board> siteOrBoard = getAtPosition(position);
if (siteOrBoard.first != null) {
return TYPE_SITE;
} else if (siteOrBoard.second != null) {
return TYPE_BOARD;
} else {
throw new IllegalArgumentException();
}
}
@Override
public int getCount() {
return BrowseBoardsFloatingMenu.this.getCount();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@SuppressLint("ViewHolder")
@Override
public View getView(int position, View view, ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
Pair<Site, Board> siteOrBoard = getAtPosition(position);
if (siteOrBoard.first != null) {
Site site = siteOrBoard.first;
if (view == null) {
view = inflater.inflate(R.layout.cell_browse_site, parent, false);
}
View divider = view.findViewById(R.id.divider);
final ImageView image = view.findViewById(R.id.image);
TextView text = view.findViewById(R.id.text);
divider.setVisibility(position == 0 ? View.GONE : View.VISIBLE);
final SiteIcon icon = site.icon();
image.setTag(icon);
icon.get(new SiteIcon.SiteIconResult() {
@Override
public void onSiteIcon(SiteIcon siteIcon, Drawable drawable) {
if (image.getTag() == icon) {
image.setImageDrawable(drawable);
}
}
});
text.setTypeface(AndroidUtils.ROBOTO_MEDIUM);
text.setText(site.name());
return view;
} else {
Board board = siteOrBoard.second;
if (view == null) {
view = inflater.inflate(R.layout.cell_browse_board, parent, false);
}
TextView text = (TextView) view;
text.setTypeface(AndroidUtils.ROBOTO_MEDIUM);
text.setText(BoardHelper.getName(board));
return text;
}
}
}
public interface Callback {
void onBoardClicked(Board item);
void onSiteClicked(Site site);
}
}

@ -30,10 +30,8 @@ import static org.floens.chan.utils.AndroidUtils.getString;
public class NavigationItem { public class NavigationItem {
public String title = ""; public String title = "";
public String subtitle = ""; public String subtitle = "";
public ToolbarMenu menu;
public boolean hasBack = true; public boolean hasBack = true;
public FloatingMenu middleMenu;
public View rightView;
public boolean hasDrawer = false; public boolean hasDrawer = false;
public boolean handlesToolbarInset = false; public boolean handlesToolbarInset = false;
public boolean swipeable = true; public boolean swipeable = true;
@ -41,6 +39,10 @@ public class NavigationItem {
boolean search = false; boolean search = false;
String searchText; String searchText;
public ToolbarMenu menu;
public ToolbarMiddleMenu middleMenu;
public View rightView;
public ToolbarMenuItem createOverflow(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, List<FloatingMenuItem> items) { public ToolbarMenuItem createOverflow(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, List<FloatingMenuItem> items) {
ToolbarMenuItem overflow = menu.createOverflow(callback); ToolbarMenuItem overflow = menu.createOverflow(callback);
FloatingMenu overflowMenu = new FloatingMenu(context, overflow.getView(), items); FloatingMenu overflowMenu = new FloatingMenu(context, overflow.getView(), items);

@ -43,7 +43,6 @@ 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.drawable.DropdownArrowDrawable;
import org.floens.chan.ui.layout.SearchLayout; import org.floens.chan.ui.layout.SearchLayout;
import org.floens.chan.ui.view.FloatingMenu;
import org.floens.chan.ui.view.LoadView; import org.floens.chan.ui.view.LoadView;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
@ -479,15 +478,15 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
titleView.setTextColor(0xffffffff); titleView.setTextColor(0xffffffff);
if (item.middleMenu != null) { if (item.middleMenu != null) {
item.middleMenu.setAnchor(titleView, Gravity.LEFT, dp(5), dp(5)); 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, getAttrColor(getContext(), R.attr.dropdown_light_color), 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.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null);
titleView.setOnClickListener(new OnClickListener() { titleView.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
item.middleMenu.show(); item.middleMenu.show(titleView);
} }
}); });
} }
@ -515,10 +514,6 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
menu.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); menu.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
} }
if (item.middleMenu != null) {
item.middleMenu.setPopupWidth(FloatingMenu.POPUP_WIDTH_ANCHOR);
}
return menu; return menu;
} }
} }

@ -34,21 +34,16 @@ public class ToolbarMenu extends LinearLayout {
private List<ToolbarMenuItem> items = new ArrayList<>(); private List<ToolbarMenuItem> items = new ArrayList<>();
public ToolbarMenu(Context context) { public ToolbarMenu(Context context) {
super(context); this(context, null);
init();
} }
public ToolbarMenu(Context context, AttributeSet attrs) { public ToolbarMenu(Context context, AttributeSet attrs) {
super(context, attrs); this(context, attrs, 0);
init();
} }
public ToolbarMenu(Context context, AttributeSet attrs, int defStyleAttr) { public ToolbarMenu(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyle);
init();
}
private void init() {
setOrientation(HORIZONTAL); setOrientation(HORIZONTAL);
setGravity(Gravity.CENTER_VERTICAL); setGravity(Gravity.CENTER_VERTICAL);
} }

@ -0,0 +1,24 @@
/*
* 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.view.View;
public interface ToolbarMiddleMenu {
void show(View anchor);
}

@ -50,9 +50,12 @@ public class FloatingMenu {
private int anchorOffsetY; private int anchorOffsetY;
private int popupWidth = POPUP_WIDTH_AUTO; private int popupWidth = POPUP_WIDTH_AUTO;
private int popupHeight = -1; private int popupHeight = -1;
private boolean manageItems = true;
private List<FloatingMenuItem> items; private List<FloatingMenuItem> items;
private FloatingMenuItem selectedItem; private FloatingMenuItem selectedItem;
private int selectedPosition;
private ListAdapter adapter; private ListAdapter adapter;
private AdapterView.OnItemClickListener itemClickListener;
private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener; private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;
private ListPopupWindow popupWindow; private ListPopupWindow popupWindow;
@ -93,13 +96,20 @@ public class FloatingMenu {
} }
public void setItems(List<FloatingMenuItem> items) { public void setItems(List<FloatingMenuItem> items) {
if (!manageItems) throw new IllegalArgumentException();
this.items = items; this.items = items;
} }
public void setSelectedItem(FloatingMenuItem item) { public void setSelectedItem(FloatingMenuItem item) {
if (!manageItems) throw new IllegalArgumentException();
this.selectedItem = item; this.selectedItem = item;
} }
public void setSelectedPosition(int selectedPosition) {
if (manageItems) throw new IllegalArgumentException();
this.selectedPosition = selectedPosition;
}
public void setAdapter(ListAdapter adapter) { public void setAdapter(ListAdapter adapter) {
this.adapter = adapter; this.adapter = adapter;
if (popupWindow != null) { if (popupWindow != null) {
@ -111,6 +121,17 @@ public class FloatingMenu {
this.callback = callback; this.callback = callback;
} }
public void setManageItems(boolean manageItems) {
this.manageItems = manageItems;
}
public void setOnItemClickListener(AdapterView.OnItemClickListener listener) {
this.itemClickListener = listener;
if (popupWindow != null) {
popupWindow.setOnItemClickListener(listener);
}
}
public void show() { public void show() {
popupWindow = new ListPopupWindow(context); popupWindow = new ListPopupWindow(context);
popupWindow.setAnchorView(anchor); popupWindow.setAnchorView(anchor);
@ -130,11 +151,15 @@ public class FloatingMenu {
popupWindow.setHeight(popupHeight); popupWindow.setHeight(popupHeight);
} }
int selectedPosition = 0; int selection = 0;
for (int i = 0; i < items.size(); i++) { if (manageItems) {
if (items.get(i) == selectedItem) { for (int i = 0; i < items.size(); i++) {
selectedPosition = i; if (items.get(i) == selectedItem) {
selection = i;
}
} }
} else {
selection = this.selectedPosition;
} }
if (adapter != null) { if (adapter != null) {
@ -143,20 +168,24 @@ public class FloatingMenu {
popupWindow.setAdapter(new FloatingMenuArrayAdapter(context, R.layout.toolbar_menu_item, items)); popupWindow.setAdapter(new FloatingMenuArrayAdapter(context, R.layout.toolbar_menu_item, items));
} }
popupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { if (manageItems) {
@Override popupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { @Override
if (position >= 0 && position < items.size()) { public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
FloatingMenuItem item = items.get(position); if (position >= 0 && position < items.size()) {
if (item.isEnabled()) { FloatingMenuItem item = items.get(position);
callback.onFloatingMenuItemClicked(FloatingMenu.this, item); if (item.isEnabled()) {
popupWindow.dismiss(); callback.onFloatingMenuItemClicked(FloatingMenu.this, item);
popupWindow.dismiss();
}
} else {
callback.onFloatingMenuItemClicked(FloatingMenu.this, null);
} }
} else {
callback.onFloatingMenuItemClicked(FloatingMenu.this, null);
} }
} });
}); } else {
popupWindow.setOnItemClickListener(itemClickListener);
}
globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override @Override
@ -186,7 +215,7 @@ public class FloatingMenu {
}); });
popupWindow.show(); popupWindow.show();
popupWindow.setSelection(selectedPosition); popupWindow.setSelection(selection);
} }
public boolean isShowing() { public boolean isShowing() {
@ -206,6 +235,16 @@ public class FloatingMenu {
void onFloatingMenuDismissed(FloatingMenu menu); void onFloatingMenuDismissed(FloatingMenu menu);
} }
public static class FloatingMenuCallbackAdapter implements FloatingMenuCallback {
@Override
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
}
@Override
public void onFloatingMenuDismissed(FloatingMenu menu) {
}
}
private static class FloatingMenuArrayAdapter extends ArrayAdapter<FloatingMenuItem> { private static class FloatingMenuArrayAdapter extends ArrayAdapter<FloatingMenuItem> {
public FloatingMenuArrayAdapter(Context context, int resource, List<FloatingMenuItem> objects) { public FloatingMenuArrayAdapter(Context context, int resource, List<FloatingMenuItem> objects) {
super(context, resource, objects); super(context, resource, objects);

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal">
<ImageView
android:id="@+id/thumb"
android:layout_width="56dp"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:scaleType="center"
android:src="@drawable/ic_reorder_black_24dp"
tools:ignore="ContentDescription" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:singleLine="true"
android:textColor="?text_color_primary"
android:textSize="14sp"
tools:text="Technology" />
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingBottom="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textColor="?text_color_secondary"
android:textSize="12sp"
tools:text="Description is here" />
</LinearLayout>
</LinearLayout>

@ -22,11 +22,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:orientation="horizontal"> android:orientation="horizontal">
<CheckBox
android:id="@+id/check"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical" />
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical"> android:orientation="vertical"
android:paddingLeft="8dp">
<TextView <TextView
android:id="@+id/text" android:id="@+id/text"
@ -51,14 +58,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</LinearLayout> </LinearLayout>
<ImageView
android:id="@+id/check"
android:layout_width="56dp"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:scaleType="center"
tools:ignore="ContentDescription" />
</LinearLayout> </LinearLayout>

@ -15,20 +15,14 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="48dp"
android:background="?attr/selectableItemBackground" android:ellipsize="end"
android:orientation="horizontal"> android:gravity="center_vertical"
android:lines="1"
<TextView android:paddingLeft="16dp"
android:id="@+id/text" android:paddingRight="16dp"
android:layout_width="0dp" android:singleLine="true"
android:layout_height="wrap_content" android:textColor="?text_color_primary"
android:layout_weight="1" android:textSize="15sp" />
android:gravity="center_vertical"
android:padding="16dp"
android:textColor="?text_color_primary"
android:textSize="14sp" />
</LinearLayout>

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?><!--
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/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/divider_color" />
<ImageView
android:id="@+id/image"
android:layout_width="48dp"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:paddingLeft="16dp"
android:paddingTop="8dp"
android:scaleType="fitCenter"
tools:src="@drawable/ic_help_outline_black_24dp" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="48dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:lines="1"
android:paddingLeft="56dp"
android:paddingRight="8dp"
android:singleLine="true"
android:textColor="?text_color_primary"
android:textSize="16sp"
tools:text="Site name" />
</FrameLayout>

@ -181,22 +181,8 @@ Re-enable this permission in the app settings if you permanently disabled it."</
<string name="setup_board_title">Configure boards of %s</string> <string name="setup_board_title">Configure boards of %s</string>
<string name="setup_board_add">Add board</string> <string name="setup_board_add">Add board</string>
<string name="setup_board_removed">Removed \'%s\'</string>
<string name="saved_boards_title">Your boards</string> <string name="setup_board_added">%s added</string>
<string name="saved_boards_introduction">"Change your saved boards. These will appear in a list at the top."</string>
<string name="saved_boards_code">Board code</string>
<string name="saved_boards_code_hint">lit</string>
<string name="board_edit">Board editor</string>
<string name="board_edit_header">Add, remove and reorder your boards here.\nThe topmost board will be loaded automatically.</string>
<string name="board_add">Add board</string>
<string name="board_add_hint">Board code e.g. lit</string>
<string name="board_add_success">Added board</string>
<string name="board_add_duplicate">Board already added</string>
<string name="board_add_unknown_title">Unknown board code</string>
<string name="board_add_unknown">The board with code %1$s is not known.</string>
<string name="board_edit_sort_a_z">Sort A-Z</string>
<string name="board_edit_board_removed">Removed board \'%1$s\'</string>
<string name="filter_summary_all_boards">all boards</string> <string name="filter_summary_all_boards">all boards</string>
<string name="filter_enabled">Enabled</string> <string name="filter_enabled">Enabled</string>
@ -359,9 +345,11 @@ Re-enable this permission in the app settings if you permanently disabled it."</
<string name="image_save_failed">Saving image failed</string> <string name="image_save_failed">Saving image failed</string>
<string name="image_save_directory_error">Cannot make save directory</string> <string name="image_save_directory_error">Cannot make save directory</string>
<string name="drawer_sites">Sites</string>
<string name="drawer_history">History</string>
<string name="settings_screen">Settings</string> <string name="settings_screen">Settings</string>
<string name="settings_group_general">General</string> <string name="settings_group_general">General</string>
<string name="settings_board_edit">Boards</string>
<string name="settings_watch">Thread watcher</string> <string name="settings_watch">Thread watcher</string>
<string name="settings_pass">4chan pass</string> <string name="settings_pass">4chan pass</string>
<string name="settings_advanced_hint">Try the advanced settings!</string> <string name="settings_advanced_hint">Try the advanced settings!</string>

Loading…
Cancel
Save