/* * Overchan Android (Meta Imageboard Client) * Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan> * * 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 nya.miku.wishmaster.ui; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import nya.miku.wishmaster.R; import nya.miku.wishmaster.api.ChanModule; import nya.miku.wishmaster.api.interfaces.CancellableTask; import nya.miku.wishmaster.api.models.SimpleBoardModel; import nya.miku.wishmaster.api.models.UrlPageModel; import nya.miku.wishmaster.cache.PagesCache; import nya.miku.wishmaster.cache.SerializableBoardsList; import nya.miku.wishmaster.common.Async; import nya.miku.wishmaster.common.Logger; import nya.miku.wishmaster.common.MainApplication; import nya.miku.wishmaster.http.interactive.InteractiveException; import nya.miku.wishmaster.lib.ClickableToast; import nya.miku.wishmaster.ui.settings.ApplicationSettings; import nya.miku.wishmaster.ui.tabs.TabModel; import nya.miku.wishmaster.ui.tabs.TabsState; import nya.miku.wishmaster.ui.tabs.UrlHandler; import nya.miku.wishmaster.ui.theme.ThemeUtils; import android.content.Context; import android.content.res.Resources; import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.ContextMenu; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ContextMenu.ContextMenuInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; public class BoardsListFragment extends Fragment implements AdapterView.OnItemClickListener, View.OnClickListener { public static final String TAG = "BoardsListFragment"; private boolean isFailInstance = false; private PagesCache pagesCache = MainApplication.getInstance().pagesCache; private ChanModule chan; private MainActivity activity; private Resources resources; private ApplicationSettings settings; private Database database; private CancellableTask currentTask; private TabModel tabModel; private String startItem; private int startItemTop; private View rootView; private View loadingView; private View errorView; private TextView errorTextView; private ListView listView; private EditText boardField; private Button buttonGo; private SimpleBoardModel[] boardsList; private BoardsListAdapter adapter; private List<QuickAccess.Entry> quickAccessList; private ClickableToast toast; public static BoardsListFragment newInstance(long tabId) { TabsState tabsState = MainApplication.getInstance().tabsState; if (tabsState == null) throw new IllegalStateException("tabsState was not initialized in the MainApplication singleton"); TabModel model = tabsState.findTabById(tabId); if (model == null) throw new IllegalArgumentException("cannot find tab with id "+tabId); if (model.pageModel.type != UrlPageModel.TYPE_INDEXPAGE) { throw new IllegalArgumentException("pageModel.type != INDEXPAGE (this fragment can show only boardslists)"); } BoardsListFragment fragment = new BoardsListFragment(); Bundle args = new Bundle(1); args.putLong("TabModelId", tabId); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activity = (MainActivity) getActivity(); resources = MainApplication.getInstance().resources; settings = MainApplication.getInstance().settings; database = MainApplication.getInstance().database; TabsState tabsState = MainApplication.getInstance().tabsState; if (tabsState == null) throw new IllegalStateException("tabsState was not initialized in the MainApplication singleton"); tabModel = tabsState.findTabById(getArguments().getLong("TabModelId")); if (tabModel == null) { isFailInstance = true; return; } if (tabModel.forceUpdate) { tabModel.forceUpdate = false; MainApplication.getInstance().serializer.serializeTabsState(tabsState); saveHistory(); } chan = MainApplication.getInstance().getChanModule(tabModel.pageModel.chanName); setHasOptionsMenu(true); quickAccessList = QuickAccess.getQuickAccessFromPreferences(); if (quickAccessList.isEmpty()) { toast = ClickableToast.showText(activity, resources.getString(R.string.boardslist_quickaccess_tip), new ClickableToast.OnClickListener() { @Override public void onClick() { try { activity.openOptionsMenu(); } catch (Exception e) { Logger.e(TAG, e); } } }, Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { MenuItem itemUpdate = menu.add(Menu.NONE, R.id.menu_update, 101, resources.getString(R.string.menu_update)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { itemUpdate.setIcon(ThemeUtils.getActionbarIcon(activity.getTheme(), resources, R.attr.actionRefresh)); CompatibilityImpl.setShowAsActionIfRoom(itemUpdate); } else { itemUpdate.setIcon(R.drawable.ic_menu_refresh); } if (quickAccessList == null) return; for (QuickAccess.Entry entry : quickAccessList) if (entry.boardName == null && entry.chan != null && entry.chan.getChanName().equals(chan.getChanName())) return; menu.add(Menu.NONE, R.id.menu_quickaccess_add, 102, resources.getString(R.string.menu_quickaccess_add)). setIcon(R.drawable.ic_menu_add_bookmark); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); switch (id) { case R.id.menu_update: update(true); return true; case R.id.menu_quickaccess_add: QuickAccess.Entry newEntry = new QuickAccess.Entry(); newEntry.chan = chan; quickAccessList.add(0, newEntry); QuickAccess.saveQuickAccessToPreferences(quickAccessList); item.setVisible(false); return true; } return super.onContextItemSelected(item); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (isFailInstance) { Toast.makeText(activity, R.string.error_unknown, Toast.LENGTH_LONG).show(); return new View(activity); } startItem = tabModel.startItemNumber; startItemTop = tabModel.startItemTop; rootView = inflater.inflate(R.layout.boardslist_fragment, container, false); loadingView = rootView.findViewById(R.id.boardslist_loading); errorView = rootView.findViewById(R.id.boardslist_error); errorTextView = (TextView)errorView.findViewById(R.id.frame_error_text); listView = (ListView)rootView.findViewById(android.R.id.list); listView.setOnItemClickListener(this); registerForContextMenu(listView); boardField = (EditText) rootView.findViewById(R.id.boardslist_board_field); boardField.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { onClick(v); return true; } return false; } }); buttonGo = (Button) rootView.findViewById(R.id.boardslist_btn_go); buttonGo.setOnClickListener(this); activity.setTitle(chan.getDisplayingName()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) CompatibilityImpl.setActionBarCustomFavicon(activity, chan.getChanFavicon()); update(false); return rootView; } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (adapter != null) { hideKeyboard(listView); SimpleBoardModel boardModel = adapter.getItem(position).model; if (boardModel != null) { UrlPageModel model = getUrlModel(boardModel.boardName); String url = chan.buildUrl(model); UrlHandler.open(url, activity); } } } @Override public void onClick(View v) { String boardName = boardField.getText().toString(); if (boardName.length() == 0) return; hideKeyboard(v); UrlPageModel model = getUrlModel(boardName); try { String url = chan.buildUrl(model); UrlHandler.open(url, activity); } catch (Exception e) { Logger.e(TAG, e); } } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); if (adapter == null) return; try { if (v.getId() == android.R.id.list) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; SimpleBoardModel boardModel = adapter.getItem(info.position).model; if (boardModel != null) { UrlPageModel model = getUrlModel(boardModel.boardName); model = UrlHandler.getPageModel(chan.buildUrl(model)); if (model != null) { boolean isFavorite = database.isFavorite(model.chanName, model.boardName, Integer.toString(model.boardPage), null); menu.add(Menu.NONE, R.id.context_menu_favorites_from_fragment, 1, isFavorite ? R.string.context_menu_remove_favorites : R.string.context_menu_add_favorites); for (QuickAccess.Entry entry : quickAccessList) if (entry.boardName != null && entry.chan != null) if (entry.chan.getChanName().equals(model.chanName) && entry.boardName.equals(model.boardName)) return; menu.add(Menu.NONE, R.id.context_menu_quickaccess_add, 2, R.string.context_menu_quickaccess_add); } } } } catch (Exception e) { Logger.e(TAG, e); } } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); try { if (item.getItemId() == R.id.context_menu_favorites_from_fragment) { String url = chan.buildUrl(getUrlModel(adapter.getItem(menuInfo.position).model.boardName)); UrlPageModel model = UrlHandler.getPageModel(url); if (model != null) { if (database.isFavorite(model.chanName, model.boardName, Integer.toString(model.boardPage), null)) { database.removeFavorite(model.chanName, model.boardName, Integer.toString(model.boardPage), null); } else { database.addFavorite(model.chanName, model.boardName, Integer.toString(model.boardPage), null, getString(R.string.tabs_title_boardpage_first, model.boardName), url); } updateListSavePosition(); return true; } } else if (item.getItemId() == R.id.context_menu_quickaccess_add) { QuickAccess.Entry newEntry = new QuickAccess.Entry(); newEntry.chan = chan; SimpleBoardModel simleBoardModel = adapter.getItem(menuInfo.position).model; newEntry.boardName = simleBoardModel.boardName; newEntry.boardDescription = simleBoardModel.boardDescription; quickAccessList.add(0, newEntry); QuickAccess.saveQuickAccessToPreferences(quickAccessList); return true; } } catch (Exception e) { Logger.e(TAG, e); } return false; } private UrlPageModel getUrlModel(String boardName) { UrlPageModel model = new UrlPageModel(); model.chanName = chan.getChanName(); model.type = UrlPageModel.TYPE_BOARDPAGE; model.boardName = boardName; model.boardPage = UrlPageModel.DEFAULT_FIRST_PAGE; return model; } private void hideKeyboard(View v) { try { InputMethodManager imm = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(v.getWindowToken(), 0); } catch (Exception e) { Logger.e(TAG, e); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); saveCurrentPostPosition(); } @Override public void onDestroyView() { super.onDestroyView(); saveCurrentPostPosition(); if (toast != null) toast.hide(); } @Override public void onDestroy() { super.onDestroy(); if (listView != null) { listView.setOnLongClickListener(null); listView.setAdapter(null); } } private void saveCurrentPostPosition() { if (tabModel != null && listView != null && listView.getChildCount() > 0 && adapter != null) { View v = listView.getChildAt(0); int position = listView.getPositionForView(v); BoardsListEntry model = adapter.getItem(position); tabModel.startItemNumber = model.isSeparator ? model.category : model.model.boardName; tabModel.startItemTop = v == null ? 0 : v.getTop(); MainApplication.getInstance().serializer.serializeTabsState(MainApplication.getInstance().tabsState); } } private void switchToLoadingView() { loadingView.setVisibility(View.VISIBLE); errorView.setVisibility(View.GONE); listView.setVisibility(View.GONE); } private String fixErrorMessage(String message) { if (message == null || message.length() == 0) { return resources.getString(R.string.error_unknown); } return message; } private void switchToErrorView(String message) { loadingView.setVisibility(View.GONE); errorView.setVisibility(View.VISIBLE); listView.setVisibility(View.GONE); errorTextView.setText(fixErrorMessage(message)); } private void switchToListView() { loadingView.setVisibility(View.GONE); errorView.setVisibility(View.GONE); listView.setVisibility(View.VISIBLE); } private void showToastError(String message) { Toast.makeText(activity, fixErrorMessage(message), Toast.LENGTH_LONG).show(); } private void saveHistory() { MainApplication.getInstance().database.addHistory(tabModel.pageModel.chanName, null, null, null, tabModel.title, tabModel.webUrl); } /** * Обновить список (в случае изменения параметра отображения NSFW досок) */ public void updateList() { update(false); } private void updateListSavePosition() { try { View v = listView.getChildAt(0); int position = listView.getPositionForView(v); BoardsListEntry model = adapter.getItem(position); startItem = model.isSeparator ? model.category : model.model.boardName; startItemTop = v == null ? 0 : v.getTop(); } catch (Exception e) { Logger.e(TAG, e); } updateList(); } private void update(boolean forceUpdate) { listView.setAdapter(null); adapter = null; switchToLoadingView(); if (currentTask != null) { currentTask.cancel(); } BoardsListGetter boardsListGetter = new BoardsListGetter(forceUpdate); currentTask = boardsListGetter; Async.runAsync(boardsListGetter); } private class BoardsListGetter extends CancellableTask.BaseCancellableTask implements Runnable { private final boolean forceUpdate; public BoardsListGetter(boolean forceUpdate) { this.forceUpdate = forceUpdate; } @Override public void run() { if (forceUpdate) saveHistory(); SerializableBoardsList fromCache = pagesCache.getSerializableBoardsList(tabModel.hash); if (fromCache == null || fromCache.chanName == null || !fromCache.chanName.equals(chan.getChanName())) fromCache = null; if (fromCache != null) boardsList = fromCache.boards; if (fromCache == null || forceUpdate) { try { SimpleBoardModel[] fromChan = chan.getBoardsList(null, this, boardsList); SerializableBoardsList serializableBoardsList = new SerializableBoardsList(); serializableBoardsList.boards = fromChan; serializableBoardsList.chanName = chan.getChanName(); pagesCache.putSerializableBoardsList(tabModel.hash, serializableBoardsList); boardsList = fromChan; } catch (Exception e) { Logger.e(TAG, e); if (isCancelled()) return; if (e instanceof InteractiveException) { ((InteractiveException) e).handle(activity, BoardsListGetter.this, new InteractiveException.Callback() { @Override public void onSuccess() { update(true); } @Override public void onError(String message) { if (boardsList == null) { switchToErrorView(message); } else { showToastError(message); update(false); } } }); } else { final String message = e.getMessage(); Async.runOnUiThread(new Runnable() { @Override public void run() { if (boardsList == null) { switchToErrorView(message); } else { showToastError(message); } } }); } } } if (isCancelled()) return; if (boardsList == null) return; adapter = new BoardsListAdapter(BoardsListFragment.this); int startItemSearch = -1; if (startItem != null) { for (int i=0; i<adapter.getCount(); ++i) { String curItem = adapter.getItem(i).isSeparator ? adapter.getItem(i).category : adapter.getItem(i).model.boardName; if (curItem.equals(startItem)) { startItemSearch = i; break; } } startItem = null; } final int startItemPosition = startItemSearch; Async.runOnUiThread(new Runnable() { @Override public void run() { listView.setAdapter(adapter); listView.setSelectionFromTop(startItemPosition, startItemTop); switchToListView(); } }); } } static class BoardsListEntry { public final SimpleBoardModel model; public final String category; public final boolean isSeparator; public BoardsListEntry(SimpleBoardModel model) { this.model = model; this.category = null; isSeparator = false; } public BoardsListEntry(String category) { this.model = null; this.category = category; isSeparator = true; } } static class BoardsListAdapter extends ArrayAdapter<BoardsListEntry> { private static final int ITEM_VIEW_TYPE_BOARD = 0; private static final int ITEM_VIEW_TYPE_SEPARATOR = 1; private LayoutInflater inflater; private Resources resources; public BoardsListAdapter(BoardsListFragment fragment) { super(fragment.activity, 0); this.inflater = LayoutInflater.from(fragment.activity); this.resources = fragment.resources; String lastCategory = ""; LinkedHashMap<String, String> favBoards = new LinkedHashMap<>(); for (String board : fragment.database.getFavoriteBoards(fragment.chan)) favBoards.put(board, ""); if (!favBoards.isEmpty()) { lastCategory = resources.getString(R.string.boardslist_favorite_boards); add(new BoardsListEntry(lastCategory)); for (int i=0; i<fragment.boardsList.length; ++i) if (favBoards.containsKey(fragment.boardsList[i].boardName)) favBoards.put(fragment.boardsList[i].boardName, fragment.boardsList[i].boardDescription); for (Map.Entry<String, String> entry : favBoards.entrySet()) { SimpleBoardModel model = new SimpleBoardModel(); model.chan = fragment.chan.getChanName(); model.boardName = entry.getKey(); model.boardDescription = entry.getValue(); model.boardCategory = lastCategory; add(new BoardsListEntry(model)); } } for (int i=0; i<fragment.boardsList.length; ++i) { if (!fragment.settings.showNSFWBoards() && fragment.boardsList[i].nsfw) continue; String curCategory = fragment.boardsList[i].boardCategory != null ? fragment.boardsList[i].boardCategory : ""; if (!curCategory.equals(lastCategory)) { add(new BoardsListEntry(curCategory)); lastCategory = curCategory; } add(new BoardsListEntry(fragment.boardsList[i])); } } @Override public int getViewTypeCount() { return 2; } @Override public int getItemViewType(int position) { return this.getItem(position).isSeparator ? ITEM_VIEW_TYPE_SEPARATOR : ITEM_VIEW_TYPE_BOARD; } @Override public boolean isEnabled(int position) { return !this.getItem(position).isSeparator; } @Override public View getView(int position, View convertView, ViewGroup parent) { BoardsListEntry item = this.getItem(position); if (convertView == null) { convertView = inflater.inflate(item.isSeparator ? R.layout.list_separator : android.R.layout.simple_list_item_1, parent, false); } if (item.isSeparator) { ((TextView) convertView).setText(item.category); } else { TextView text = (TextView) convertView.findViewById(android.R.id.text1); text.setText(resources.getString(R.string.boardslist_format, item.model.boardName, item.model.boardDescription)); } return convertView; } } }