add 4chan archive support

reads the html because there is no api endpoint for it.
adds a new `archive` boolean to the Board model.
refactor-toolbar
Floens 8 years ago
parent 162a0b2f68
commit 9c4f4334f9
  1. 10
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java
  2. 47
      Clover/app/src/main/java/org/floens/chan/core/model/Archive.java
  3. 5
      Clover/app/src/main/java/org/floens/chan/core/model/orm/Board.java
  4. 62
      Clover/app/src/main/java/org/floens/chan/core/net/HtmlReaderRequest.java
  5. 138
      Clover/app/src/main/java/org/floens/chan/core/presenter/ArchivePresenter.java
  6. 3
      Clover/app/src/main/java/org/floens/chan/core/presenter/BrowsePresenter.java
  7. 5
      Clover/app/src/main/java/org/floens/chan/core/site/Site.java
  8. 10
      Clover/app/src/main/java/org/floens/chan/core/site/SiteActions.java
  9. 2
      Clover/app/src/main/java/org/floens/chan/core/site/SiteEndpoints.java
  10. 9
      Clover/app/src/main/java/org/floens/chan/core/site/common/CommonSite.java
  11. 29
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java
  12. 57
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4ArchiveRequest.java
  13. 3
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4BoardsRequest.java
  14. 211
      Clover/app/src/main/java/org/floens/chan/ui/controller/ArchiveController.java
  15. 31
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  16. 34
      Clover/app/src/main/res/layout/cell_archive.xml
  17. 63
      Clover/app/src/main/res/layout/controller_archive.xml
  18. 4
      Clover/app/src/main/res/values/strings.xml
  19. 6
      docs/database.txt

@ -45,7 +45,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
private static final String TAG = "DatabaseHelper"; private static final String TAG = "DatabaseHelper";
private static final String DATABASE_NAME = "ChanDB"; private static final String DATABASE_NAME = "ChanDB";
private static final int DATABASE_VERSION = 22; private static final int DATABASE_VERSION = 23;
public Dao<Pin, Integer> pinDao; public Dao<Pin, Integer> pinDao;
public Dao<Loadable, Integer> loadableDao; public Dao<Loadable, Integer> loadableDao;
@ -230,6 +230,14 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
SiteService.addSiteForLegacy(); SiteService.addSiteForLegacy();
} }
if (oldVersion < 23) {
try {
pinDao.executeRawNoArgs("ALTER TABLE board ADD COLUMN \"archive\" INTEGER;");
} catch (SQLException e) {
Logger.e(TAG, "Error upgrading to version 14", e);
}
}
} }
public void reset() { public void reset() {

@ -0,0 +1,47 @@
/*
* 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.model;
import java.util.List;
public class Archive {
public final List<ArchiveItem> items;
public static Archive fromItems(List<ArchiveItem> items) {
return new Archive(items);
}
private Archive(List<ArchiveItem> items) {
this.items = items;
}
public static class ArchiveItem {
public final String description;
public final int id;
public static ArchiveItem fromDescriptionId(String description, int id) {
return new ArchiveItem(description, id);
}
private ArchiveItem(String description, int id) {
this.description = description;
this.id = id;
}
}
}

@ -131,6 +131,9 @@ public class Board implements SiteReference {
@DatabaseField @DatabaseField
public String description; public String description;
@DatabaseField
public boolean archive = false;
@Deprecated // public, at least @Deprecated // public, at least
public Board() { public Board() {
} }
@ -200,6 +203,7 @@ public class Board implements SiteReference {
trollFlags = o.trollFlags; trollFlags = o.trollFlags;
mathTags = o.mathTags; mathTags = o.mathTags;
description = o.description; description = o.description;
archive = o.archive;
} }
/** /**
@ -238,6 +242,7 @@ public class Board implements SiteReference {
b.trollFlags = trollFlags; b.trollFlags = trollFlags;
b.mathTags = mathTags; b.mathTags = mathTags;
b.description = description; b.description = description;
b.archive = archive;
return b; return b;
} }
} }

@ -0,0 +1,62 @@
/*
* 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.net;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.Response.Listener;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpHeaderParser;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.ByteArrayInputStream;
import java.io.IOException;
public abstract class HtmlReaderRequest<T> extends Request<T> {
protected final Listener<T> listener;
public HtmlReaderRequest(String url, Listener<T> listener, Response.ErrorListener errorListener) {
super(Request.Method.GET, url, errorListener);
this.listener = listener;
}
@Override
protected void deliverResponse(T response) {
listener.onResponse(response);
}
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
try {
Document document = Jsoup.parse(new ByteArrayInputStream(response.data),
HttpHeaderParser.parseCharset(response.headers), getUrl());
T result = readDocument(document);
return Response.success(result, HttpHeaderParser.parseCacheHeaders(response));
} catch (IOException e) {
return Response.error(new VolleyError(e));
}
}
public abstract T readDocument(Document document) throws IOException;
}

@ -0,0 +1,138 @@
/*
* 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 org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.model.Archive;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.site.SiteActions;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import static android.text.TextUtils.isEmpty;
public class ArchivePresenter implements SiteActions.ArchiveListener {
private DatabaseManager databaseManager;
private Callback callback;
private Board board;
private boolean inRequest = false;
private String filter;
private List<Archive.ArchiveItem> items = new ArrayList<>();
private List<Archive.ArchiveItem> filteredItems = new ArrayList<>();
@Inject
public ArchivePresenter(DatabaseManager databaseManager) {
this.databaseManager = databaseManager;
}
public void create(Callback callback, Board board) {
this.callback = callback;
this.board = board;
loadArchive();
}
public void onRefresh() {
if (!inRequest) {
loadArchive();
}
}
private void loadArchive() {
inRequest = true;
callback.showError(false);
board.site.actions().archive(board, this);
}
public void onSearchEntered(String query) {
filterArchive(query);
}
public void onSearchVisibility(boolean visible) {
if (!visible) {
filterArchive(null);
}
}
public void onItemClicked(Archive.ArchiveItem item) {
Loadable loadable = databaseManager.getDatabaseLoadableManager()
.get(Loadable.forThread(board.site, board, item.id));
callback.openThread(loadable);
}
@Override
public void onArchive(Archive archive) {
inRequest = false;
callback.hideRefreshing();
callback.showList();
items = archive.items;
updateWithFilter();
}
@Override
public void onArchiveError() {
inRequest = false;
callback.hideRefreshing();
callback.showError(true);
}
private void filterArchive(String query) {
filter = query;
updateWithFilter();
}
private void updateWithFilter() {
filteredItems.clear();
if (isEmpty(filter)) {
filteredItems.addAll(items);
} else {
for (Archive.ArchiveItem item : items) {
if (filterApplies(item, filter)) {
filteredItems.add(item);
}
}
}
callback.setArchiveItems(filteredItems);
}
private boolean filterApplies(Archive.ArchiveItem item, String filter) {
return item.description.toLowerCase(Locale.ENGLISH).contains(filter.toLowerCase());
}
public interface Callback {
void setArchiveItems(List<Archive.ArchiveItem> items);
void hideRefreshing();
void showList();
void showError(boolean show);
void openThread(Loadable loadable);
}
}

@ -118,11 +118,14 @@ public class BrowsePresenter implements Observer {
private void loadBoard(Board board) { private void loadBoard(Board board) {
currentBoard = board; currentBoard = board;
callback.loadBoard(getLoadableForBoard(board)); callback.loadBoard(getLoadableForBoard(board));
callback.showArchiveOption(board.site.boardFeature(Site.BoardFeature.ARCHIVE, board));
} }
public interface Callback { public interface Callback {
void loadBoard(Loadable loadable); void loadBoard(Loadable loadable);
void loadSiteSetup(Site site); void loadSiteSetup(Site site);
void showArchiveOption(boolean show);
} }
} }

@ -79,6 +79,11 @@ public interface Site {
*/ */
// TODO(multisite) use this // TODO(multisite) use this
POSTING_SPOILER, POSTING_SPOILER,
/**
* This board support loading the archive, a list of threads that are locked after expiring.
*/
ARCHIVE,
} }
/** /**

@ -17,6 +17,8 @@
*/ */
package org.floens.chan.core.site; package org.floens.chan.core.site;
import org.floens.chan.core.model.Archive;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.site.http.DeleteRequest; import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.DeleteResponse; import org.floens.chan.core.site.http.DeleteResponse;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
@ -63,6 +65,14 @@ public interface SiteActions {
void onDeleteError(HttpCall httpCall); void onDeleteError(HttpCall httpCall);
} }
void archive(Board board, ArchiveListener archiveListener);
interface ArchiveListener {
void onArchive(Archive archive);
void onArchiveError();
}
/* TODO(multi-site) this login mechanism is probably not generic enough right now, /* TODO(multi-site) this login mechanism is probably not generic enough right now,
* especially if we're thinking about what a login really is * especially if we're thinking about what a login really is
* We'll expand this later when we have a better idea of what other sites require. * We'll expand this later when we have a better idea of what other sites require.

@ -43,6 +43,8 @@ public interface SiteEndpoints {
HttpUrl boards(); HttpUrl boards();
HttpUrl archive(Board board);
HttpUrl reply(Loadable thread); HttpUrl reply(Loadable thread);
HttpUrl delete(Post post); HttpUrl delete(Post post);

@ -357,6 +357,11 @@ public abstract class CommonSite extends SiteBase {
return null; return null;
} }
@Override
public HttpUrl archive(Board board) {
return null;
}
@Override @Override
public HttpUrl reply(Loadable thread) { public HttpUrl reply(Loadable thread) {
return null; return null;
@ -522,6 +527,10 @@ public abstract class CommonSite extends SiteBase {
} }
} }
@Override
public void archive(Board board, ArchiveListener archiveListener) {
}
@Override @Override
public void login(LoginRequest loginRequest, LoginListener loginListener) { public void login(LoginRequest loginRequest, LoginListener loginListener) {
} }

@ -30,16 +30,15 @@ import org.floens.chan.core.settings.Setting;
import org.floens.chan.core.settings.SettingProvider; import org.floens.chan.core.settings.SettingProvider;
import org.floens.chan.core.settings.SharedPreferencesSettingProvider; import org.floens.chan.core.settings.SharedPreferencesSettingProvider;
import org.floens.chan.core.settings.StringSetting; import org.floens.chan.core.settings.StringSetting;
import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.Boards; import org.floens.chan.core.site.Boards;
import org.floens.chan.core.site.SiteUrlHandler;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteActions; import org.floens.chan.core.site.SiteActions;
import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.SiteBase; import org.floens.chan.core.site.SiteBase;
import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.SiteIcon; import org.floens.chan.core.site.SiteIcon;
import org.floens.chan.core.site.SiteRequestModifier; import org.floens.chan.core.site.SiteRequestModifier;
import org.floens.chan.core.site.parser.ChanReader; import org.floens.chan.core.site.SiteUrlHandler;
import org.floens.chan.core.site.common.CommonReplyHttpCall; import org.floens.chan.core.site.common.CommonReplyHttpCall;
import org.floens.chan.core.site.common.FutabaChanReader; import org.floens.chan.core.site.common.FutabaChanReader;
import org.floens.chan.core.site.http.DeleteRequest; import org.floens.chan.core.site.http.DeleteRequest;
@ -47,6 +46,7 @@ import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.LoginRequest; import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.LoginResponse; import org.floens.chan.core.site.http.LoginResponse;
import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.site.parser.ChanReader;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
@ -174,6 +174,11 @@ public class Chan4 extends SiteBase {
.host("sys.4chan.org") .host("sys.4chan.org")
.build(); .build();
private final HttpUrl b = new HttpUrl.Builder()
.scheme("https")
.host("boards.4chan.org")
.build();
@Override @Override
public HttpUrl catalog(Board board) { public HttpUrl catalog(Board board) {
return a.newBuilder() return a.newBuilder()
@ -248,6 +253,14 @@ public class Chan4 extends SiteBase {
.build(); .build();
} }
@Override
public HttpUrl archive(Board board) {
return b.newBuilder()
.addPathSegment(board.code)
.addPathSegment("archive")
.build();
}
@Override @Override
public HttpUrl reply(Loadable loadable) { public HttpUrl reply(Loadable loadable) {
return sys.newBuilder() return sys.newBuilder()
@ -332,6 +345,13 @@ public class Chan4 extends SiteBase {
})); }));
} }
@Override
public void archive(Board board, ArchiveListener archiveListener) {
requestQueue.add(new Chan4ArchiveRequest(Chan4.this, board,
archiveListener::onArchive,
error -> archiveListener.onArchiveError()));
}
@Override @Override
public void post(Reply reply, final PostListener postListener) { public void post(Reply reply, final PostListener postListener) {
httpCallManager.makeHttpCall(new Chan4ReplyCall(Chan4.this, reply), new HttpCall.HttpCallback<CommonReplyHttpCall>() { httpCallManager.makeHttpCall(new Chan4ReplyCall(Chan4.this, reply), new HttpCall.HttpCallback<CommonReplyHttpCall>() {
@ -519,12 +539,13 @@ public class Chan4 extends SiteBase {
case POSTING_SPOILER: case POSTING_SPOILER:
// depends if the board supports it. // depends if the board supports it.
return board.spoilers; return board.spoilers;
case ARCHIVE:
return board.archive;
default: default:
return false; return false;
} }
} }
@Override @Override
public SiteEndpoints endpoints() { public SiteEndpoints endpoints() {
return endpoints; return endpoints;

@ -0,0 +1,57 @@
/*
* 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.site.sites.chan4;
import com.android.volley.Response;
import org.floens.chan.core.model.Archive;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.net.HtmlReaderRequest;
import org.floens.chan.core.site.Site;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class Chan4ArchiveRequest extends HtmlReaderRequest<Archive> {
public Chan4ArchiveRequest(Site site, Board board,
Response.Listener<Archive> listener,
Response.ErrorListener errorListener) {
super(site.endpoints().archive(board).toString(), listener, errorListener);
}
@Override
public Archive readDocument(Document document) throws IOException {
List<Archive.ArchiveItem> items = new ArrayList<>();
Element table = document.getElementById("arc-list");
Element tableBody = table.getElementsByTag("tbody").first();
Elements trs = tableBody.getElementsByTag("tr");
for (Element tr : trs) {
Elements dataElements = tr.getElementsByTag("td");
String description = dataElements.get(1).text();
int id = Integer.parseInt(dataElements.get(0).text());
items.add(Archive.ArchiveItem.fromDescriptionId(description, id));
}
return Archive.fromItems(items);
}
}

@ -161,6 +161,9 @@ public class Chan4BoardsRequest extends JsonReaderRequest<List<Board>> {
case "meta_description": case "meta_description":
board.description = reader.nextString(); board.description = reader.nextString();
break; break;
case "is_archived":
board.archive = reader.nextInt() == 1;
break;
default: default:
reader.skipValue(); reader.skipValue();
break; break;

@ -0,0 +1,211 @@
/*
* 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.content.Context;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.floens.chan.R;
import org.floens.chan.controller.Controller;
import org.floens.chan.core.model.Archive;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.presenter.ArchivePresenter;
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.CrossfadeView;
import org.floens.chan.ui.view.DividerItemDecoration;
import org.floens.chan.ui.view.FastScrollerHelper;
import org.floens.chan.ui.view.FloatingMenuItem;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import static org.floens.chan.Chan.inject;
public class ArchiveController extends Controller implements ArchivePresenter.Callback,
ToolbarMenuItem.ToolbarMenuItemCallback,
ToolbarNavigationController.ToolbarSearchCallback,
SwipeRefreshLayout.OnRefreshListener {
private static final int SEARCH_ID = 1;
private CrossfadeView crossfadeView;
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView archiveRecyclerview;
private View progress;
private View errorView;
@Inject
private ArchivePresenter presenter;
private ArchiveAdapter adapter;
private Board board;
public ArchiveController(Context context) {
super(context);
}
public void setBoard(Board board) {
this.board = board;
}
@Override
public void onCreate() {
super.onCreate();
inject(this);
// Inflate
view = inflateRes(R.layout.controller_archive);
// Navigation
navigation.title = context.getString(R.string.archive_title, BoardHelper.getName(board));
ToolbarMenu menu = new ToolbarMenu(context);
navigation.menu = menu;
menu.addItem(new ToolbarMenuItem(context, this, SEARCH_ID, R.drawable.ic_search_white_24dp));
// View binding
crossfadeView = view.findViewById(R.id.crossfade);
swipeRefreshLayout = view.findViewById(R.id.swipe_refresh);
archiveRecyclerview = view.findViewById(R.id.recycler_view);
progress = view.findViewById(R.id.progress);
errorView = view.findViewById(R.id.error_text);
// Adapters
adapter = new ArchiveAdapter();
// View setup
archiveRecyclerview.setLayoutManager(new LinearLayoutManager(context));
archiveRecyclerview.setAdapter(adapter);
archiveRecyclerview.addItemDecoration(
new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
FastScrollerHelper.create(archiveRecyclerview);
// archiveRecyclerview.setVerticalScrollBarEnabled(false);
crossfadeView.toggle(false, false);
swipeRefreshLayout.setOnRefreshListener(this);
// Presenter
presenter.create(this, board);
}
@Override
public void onMenuItemClicked(ToolbarMenuItem item) {
((ToolbarNavigationController) navigationController).showSearch();
}
@Override
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
}
@Override
public void onSearchEntered(String entered) {
presenter.onSearchEntered(entered);
}
@Override
public void onSearchVisibilityChanged(boolean visible) {
presenter.onSearchVisibility(visible);
}
@Override
public void onRefresh() {
presenter.onRefresh();
}
@Override
public void setArchiveItems(List<Archive.ArchiveItem> items) {
adapter.setArchiveItems(items);
}
@Override
public void hideRefreshing() {
swipeRefreshLayout.setRefreshing(false);
}
@Override
public void showList() {
crossfadeView.toggle(true, true);
}
@Override
public void showError(boolean show) {
progress.setVisibility(show ? View.GONE : View.VISIBLE);
errorView.setVisibility(show ? View.VISIBLE : View.GONE);
}
@Override
public void openThread(Loadable loadable) {
ViewThreadController threadController = new ViewThreadController(context);
threadController.setLoadable(loadable);
navigationController.pushController(threadController);
}
private void onItemClicked(Archive.ArchiveItem item) {
presenter.onItemClicked(item);
}
private class ArchiveAdapter extends RecyclerView.Adapter<ArchiveCell> {
private List<Archive.ArchiveItem> archiveItems = new ArrayList<>();
@Override
public int getItemCount() {
return archiveItems.size();
}
@Override
public ArchiveCell onCreateViewHolder(ViewGroup parent, int viewType) {
return new ArchiveCell(LayoutInflater.from(context)
.inflate(R.layout.cell_archive, parent, false));
}
@Override
public void onBindViewHolder(ArchiveCell holder, int position) {
Archive.ArchiveItem archiveItem = archiveItems.get(position);
holder.item = archiveItem;
holder.text.setText(archiveItem.description);
}
public void setArchiveItems(List<Archive.ArchiveItem> archiveItems) {
this.archiveItems = archiveItems;
notifyDataSetChanged();
}
}
private class ArchiveCell extends RecyclerView.ViewHolder {
private TextView text;
private Archive.ArchiveItem item;
public ArchiveCell(View itemView) {
super(itemView);
itemView.setOnClickListener(v -> onItemClicked(item));
text = itemView.findViewById(R.id.text);
}
}
}

@ -59,6 +59,7 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
private static final int VIEW_MODE_ID = 104; private static final int VIEW_MODE_ID = 104;
private static final int ORDER_ID = 105; private static final int ORDER_ID = 105;
private static final int OPEN_BROWSER_ID = 106; private static final int OPEN_BROWSER_ID = 106;
private static final int ARCHIVE_ID = 107;
@Inject @Inject
BrowsePresenter presenter; BrowsePresenter presenter;
@ -70,6 +71,7 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
private ToolbarMenuItem search; private ToolbarMenuItem search;
private ToolbarMenuItem refresh; private ToolbarMenuItem refresh;
private ToolbarMenuItem overflow; private ToolbarMenuItem overflow;
private FloatingMenuItem archive;
public BrowseController(Context context) { public BrowseController(Context context) {
super(context); super(context);
@ -129,6 +131,11 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
viewModeMenuItem = new FloatingMenuItem(VIEW_MODE_ID, postViewMode == ChanSettings.PostViewMode.LIST ? viewModeMenuItem = new FloatingMenuItem(VIEW_MODE_ID, postViewMode == ChanSettings.PostViewMode.LIST ?
R.string.action_switch_catalog : R.string.action_switch_board); R.string.action_switch_catalog : R.string.action_switch_board);
items.add(viewModeMenuItem); items.add(viewModeMenuItem);
archive = new FloatingMenuItem(ARCHIVE_ID, R.string.thread_view_archive);
items.add(archive);
archive.setEnabled(false);
items.add(new FloatingMenuItem(ORDER_ID, R.string.action_order)); items.add(new FloatingMenuItem(ORDER_ID, R.string.action_order));
items.add(new FloatingMenuItem(OPEN_BROWSER_ID, R.string.action_open_browser)); items.add(new FloatingMenuItem(OPEN_BROWSER_ID, R.string.action_open_browser));
items.add(new FloatingMenuItem(SHARE_ID, R.string.action_share)); items.add(new FloatingMenuItem(SHARE_ID, R.string.action_share));
@ -207,6 +214,25 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
handleOrder(presenter); handleOrder(presenter);
break; break;
case ARCHIVE_ID:
openArchive();
break;
}
}
private void openArchive() {
Board board = presenter.currentBoard();
if (board == null) {
return;
}
ArchiveController archiveController = new ArchiveController(context);
archiveController.setBoard(board);
if (doubleNavigationController != null) {
doubleNavigationController.pushController(archiveController);
} else {
navigationController.pushController(archiveController);
} }
} }
@ -316,6 +342,11 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
} }
} }
@Override
public void showArchiveOption(boolean show) {
archive.setEnabled(show);
}
@Override @Override
public void openPin(Pin pin) { public void openPin(Pin pin) {
showThread(pin.loadable); showThread(pin.loadable);

@ -0,0 +1,34 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:minHeight="48dp"
android:orientation="horizontal">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="8dp"
android:textColor="?text_color_primary"
android:textSize="14sp" />
</LinearLayout>

@ -0,0 +1,63 @@
<?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/>.
-->
<org.floens.chan.ui.view.CrossfadeView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/crossfade"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?backcolor">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp" />
</android.support.v4.widget.SwipeRefreshLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:padding="8dp"
android:visibility="gone">
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<TextView
android:id="@+id/error_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/archive_error"
android:visibility="gone" />
</FrameLayout>
</org.floens.chan.ui.view.CrossfadeView>

@ -175,6 +175,7 @@ Re-enable this permission in the app settings if you permanently disabled it."</
<string name="thread_empty_select">Please select a thread</string> <string name="thread_empty_select">Please select a thread</string>
<string name="thread_up_down_hint">Scroll to top/bottom</string> <string name="thread_up_down_hint">Scroll to top/bottom</string>
<string name="thread_pin_hint">Bookmark this thread</string> <string name="thread_pin_hint">Bookmark this thread</string>
<string name="thread_view_archive">Archive</string>
<string name="intro_title">Clover</string> <string name="intro_title">Clover</string>
<string name="intro_content">Browse your favorite imageboards</string> <string name="intro_content">Browse your favorite imageboards</string>
@ -273,6 +274,9 @@ Re-enable this permission in the app settings if you permanently disabled it."</
<string name="saved_reply_clear_confirm">Clear posting history?</string> <string name="saved_reply_clear_confirm">Clear posting history?</string>
<string name="saved_reply_clear_confirm_button">Clear</string> <string name="saved_reply_clear_confirm_button">Clear</string>
<string name="archive_title">%s archive</string>
<string name="archive_error">Error loading archive</string>
<string name="drawer_board">Board</string> <string name="drawer_board">Board</string>
<string name="drawer_catalog">Catalog</string> <string name="drawer_catalog">Catalog</string>
<string name="drawer_pinned">Bookmarked threads</string> <string name="drawer_pinned">Bookmarked threads</string>

@ -66,9 +66,13 @@ Changes in version 21:
ALTER TABLE loadable ADD COLUMN lastLoaded default -1; ALTER TABLE loadable ADD COLUMN lastLoaded default -1;
Changes in version 22: Changes in version 22: (also configures the 4chan site with id 0)
CREATE TABLE `site` (`configuration` VARCHAR , `id` INTEGER PRIMARY KEY AUTOINCREMENT , `userSettings` VARCHAR ); CREATE TABLE `site` (`configuration` VARCHAR , `id` INTEGER PRIMARY KEY AUTOINCREMENT , `userSettings` VARCHAR );
ALTER TABLE loadable ADD COLUMN site INTEGER default 0; ALTER TABLE loadable ADD COLUMN site INTEGER default 0;
ALTER TABLE board ADD COLUMN site INTEGER default 0; ALTER TABLE board ADD COLUMN site INTEGER default 0;
ALTER TABLE savedreply ADD COLUMN site INTEGER default 0; ALTER TABLE savedreply ADD COLUMN site INTEGER default 0;
ALTER TABLE threadhide ADD COLUMN site INTEGER default 0; ALTER TABLE threadhide ADD COLUMN site INTEGER default 0;
Changes in version 23:
ALTER TABLE board ADD COLUMN "archive" INTEGER;

Loading…
Cancel
Save