/*
* 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.presentation;
import java.io.File;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.tuple.Triple;
import nya.miku.wishmaster.R;
import nya.miku.wishmaster.api.ChanModule;
import nya.miku.wishmaster.api.interfaces.CancellableTask;
import nya.miku.wishmaster.api.models.AttachmentModel;
import nya.miku.wishmaster.api.models.BoardModel;
import nya.miku.wishmaster.api.models.DeletePostModel;
import nya.miku.wishmaster.api.models.PostModel;
import nya.miku.wishmaster.api.models.SendPostModel;
import nya.miku.wishmaster.api.models.UrlPageModel;
import nya.miku.wishmaster.api.util.ChanModels;
import nya.miku.wishmaster.api.util.PageLoaderFromChan;
import nya.miku.wishmaster.cache.BitmapCache;
import nya.miku.wishmaster.cache.PagesCache;
import nya.miku.wishmaster.cache.SerializablePage;
import nya.miku.wishmaster.common.Async;
import nya.miku.wishmaster.common.Logger;
import nya.miku.wishmaster.common.MainApplication;
import nya.miku.wishmaster.containers.ReadableContainer;
import nya.miku.wishmaster.http.interactive.InteractiveException;
import nya.miku.wishmaster.lib.ClickableLinksTextView;
import nya.miku.wishmaster.lib.ClickableToast;
import nya.miku.wishmaster.lib.JellyBeanSpanFixTextView;
import nya.miku.wishmaster.lib.SwipeDismissListViewTouchListener;
import nya.miku.wishmaster.lib.pullable_layout.SwipeRefreshLayout;
import nya.miku.wishmaster.ui.AppearanceUtils;
import nya.miku.wishmaster.ui.Attachments;
import nya.miku.wishmaster.ui.BoardsListFragment;
import nya.miku.wishmaster.ui.Clipboard;
import nya.miku.wishmaster.ui.CompatibilityImpl;
import nya.miku.wishmaster.ui.Database;
import nya.miku.wishmaster.ui.MainActivity;
import nya.miku.wishmaster.ui.QuickAccess;
import nya.miku.wishmaster.ui.ReverseImageSearch;
import nya.miku.wishmaster.ui.CompatibilityUtils;
import nya.miku.wishmaster.ui.downloading.DownloadingService;
import nya.miku.wishmaster.ui.gallery.GalleryActivity;
import nya.miku.wishmaster.ui.gallery.GallerySettings;
import nya.miku.wishmaster.ui.downloading.BackgroundThumbDownloader;
import nya.miku.wishmaster.ui.posting.PostFormActivity;
import nya.miku.wishmaster.ui.posting.PostingService;
import nya.miku.wishmaster.ui.presentation.ClickableURLSpan.URLSpanClickListener;
import nya.miku.wishmaster.ui.presentation.FlowTextHelper.FloatingModel;
import nya.miku.wishmaster.ui.presentation.HtmlParser.ImageGetter;
import nya.miku.wishmaster.ui.settings.ApplicationSettings;
import nya.miku.wishmaster.ui.settings.Wifi;
import nya.miku.wishmaster.ui.settings.ApplicationSettings.StaticSettingsContainer;
import nya.miku.wishmaster.ui.tabs.TabModel;
import nya.miku.wishmaster.ui.tabs.TabsState;
import nya.miku.wishmaster.ui.tabs.TabsTrackerService;
import nya.miku.wishmaster.ui.tabs.UrlHandler;
import nya.miku.wishmaster.ui.theme.ThemeUtils;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.widget.DrawerLayout;
import android.text.Editable;
import android.text.InputType;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.util.DisplayMetrics;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
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.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
/**
* Основной фрагмент UI (показывает страницу имиджборды)
* @author miku-nyan
*
*/
public class BoardFragment extends Fragment implements AdapterView.OnItemClickListener, VolatileSpanClickListener.Listener {
private static final String TAG = "BoardFragment";
public static final String BROADCAST_PAGE_LOADED = "nya.miku.wishmaster.BROADCAST_ACTION_PAGE_LOADED";
private boolean isFailInstance = false;
private PagesCache pagesCache = MainApplication.getInstance().pagesCache;
private BitmapCache bitmapCache = MainApplication.getInstance().bitmapCache;
private ApplicationSettings settings = MainApplication.getInstance().settings;
private MainActivity activity;
private StaticSettingsContainer staticSettings;
private Resources resources;
private Database database;
private Subscriptions subscriptions;
private ReadableContainer localFile;
private ChanModule chan;
private PresentationModel presentationModel;
private volatile boolean listLoaded = false;
private static final int TYPE_THREADSLIST = 0;
private static final int TYPE_POSTSLIST = 1;
private static final int TYPE_SEARCHLIST = 2;
private int pageType;
private TabModel tabModel;
private String startItem;
private int startItemPosition = -1;
private int startItemTop;
private int firstUnreadPosition = 0;
private boolean forceUpdateFirstTime;
private int nullAdapterSavedPosition;
private int nullAdapterSavedTop;
private String nullAdapterSavedNumber;
private boolean nullAdapterIsSet = false;
private Menu menu;
private Boolean enableQuickAccessMenu = null;
private View rootView;
private View loadingView;
private View errorView;
private TextView errorTextView;
private SwipeRefreshLayout pullableLayout;
private long pullableLayoutSetRefreshingTime;
private ListView listView;
private PostsListAdapter adapter;
private View navigationBarView;
private Spinner catalogBarView;
private View searchBarView;
private boolean searchBarInitialized = false;
private String cachedSearchRequest = null;
private List<Integer> cachedSearchResults = null;
private SparseArray<Spanned> cachedSearchHighlightedSpanables = null;
private boolean searchHighlightActive = false;
private FloatingModel[] floatingModels;
private ImageGetter imageGetter;
private URLSpanClickListener spanClickListener;
private CancellableTask currentTask;
private CancellableTask imagesDownloadTask = new CancellableTask.BaseCancellableTask();
private ExecutorService imagesDownloadExecutor = Executors.newFixedThreadPool(4, Async.LOW_PRIORITY_FACTORY);
private OpenedDialogs dialogs = new OpenedDialogs();
private boolean updatingNow = false;
/** измеряется при вызове {@link #measureFloatingModels(LayoutInflater)} */
private int postItemWidth = 0;
/** измеряется при вызове {@link #measureFloatingModels(LayoutInflater)} */
private int postItemPadding = 0;
/** измеряется при вызове {@link #measureFloatingModels(LayoutInflater)} */
private int thumbnailWidth = 0;
/** измеряется при вызове {@link #measureFloatingModels(LayoutInflater)} */
private int thumbnailMargin = 0;
/** количество строк в предпросмотре (оп-посте) треда в списке тредов.
* измеряется при вызове {@link #measureFloatingModels(LayoutInflater)} */
private int maxItemLines = 0;
/** максимальная длина строки заголовка вкладки */
private static final int MAX_TITLE_LENGHT = 200;
private static final long PULLABLE_ANIMATION_DELAY = 600;
/** позиция (в адаптере) последнего выбранного элемента при создании контекстного меню из всплывающего окна
* или -1, если контекстное меню создано из listView */
private int lastContextMenuPosition;
/** выбранное вложение (аттачмент) при создании контекстного меню из превью-картинки */
private View lastContextMenuAttachment;
/** listener-обработчик, используется, т.к. при создании контекстного меню из диалога onContextItemSelected (метод фрагмента) не вызывается */
private MenuItem.OnMenuItemClickListener contextMenuListener = new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
return onContextItemSelected(item);
}
};
public static Fragment 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) {
Logger.d(TAG, "instantiating BoardsListFragment");
return BoardsListFragment.newInstance(tabId);
}
if (model.pageModel.type == UrlPageModel.TYPE_OTHERPAGE)
throw new IllegalArgumentException("page could not be handled (pageModel.type == TYPE_OTHERPAGE)");
BoardFragment fragment = new BoardFragment();
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();
staticSettings = activity.settings;
resources = MainApplication.getInstance().resources;
database = MainApplication.getInstance().database;
subscriptions = MainApplication.getInstance().subscriptions;
Wifi.updateState(activity);
TabsState tabsState = MainApplication.getInstance().tabsState;
if (tabsState == null) throw new IllegalStateException("tabsState was not initialized in the MainApplication singleton");
long tabId = getArguments().getLong("TabModelId");
tabModel = tabsState.findTabById(tabId);
if (tabModel == null) { //периодически (видно в крашрепортах ACRA) создаются инстансы с несуществующим (удалённым??) tabId
isFailInstance = true;
return;
}
startItem = tabModel.startItemNumber;
startItemTop = tabModel.startItemTop;
forceUpdateFirstTime = tabModel.forceUpdate;
firstUnreadPosition = tabModel.firstUnreadPosition;
if (tabModel.forceUpdate || tabModel.autoupdateError || tabModel.unreadPostsCount > 0) {
tabModel.forceUpdate = false;
tabModel.autoupdateError = false;
tabModel.unreadSubscriptions = false;
tabModel.unreadPostsCount = 0;
MainApplication.getInstance().serializer.serializeTabsState(tabsState);
if (activity.tabsAdapter != null) activity.tabsAdapter.notifyDataSetChanged(false);
}
chan = MainApplication.getInstance().getChanModule(tabModel.pageModel.chanName);
setHasOptionsMenu(true);
switch (tabModel.pageModel.type) {
case UrlPageModel.TYPE_BOARDPAGE:
case UrlPageModel.TYPE_CATALOGPAGE:
pageType = TYPE_THREADSLIST;
break;
case UrlPageModel.TYPE_SEARCHPAGE:
pageType = TYPE_SEARCHLIST;
break;
case UrlPageModel.TYPE_THREADPAGE:
pageType = TYPE_POSTSLIST;
break;
}
if (tabModel.type == TabModel.TYPE_LOCAL) {
try {
localFile = ReadableContainer.obtain(new File(tabModel.localFilePath));
MainApplication.getInstance().database.addSavedThread(chan.getChanName(), tabModel.title, tabModel.localFilePath);
} catch (Exception e) {
MainApplication.getInstance().database.removeSavedThread(tabModel.localFilePath);
localFile = null;
Logger.e(TAG, "cannot open local file", e);
}
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (isFailInstance) {
Logger.e(TAG, "an instance with NULL tabModel was created");
Toast.makeText(activity, R.string.error_unknown, Toast.LENGTH_LONG).show();
return new View(activity);
}
rootView = inflater.inflate(R.layout.board_fragment, container, false);
/*{
ImageView mikuView = new ImageView(activity);
mikuView.setImageResource(R.drawable.miku);
((FrameLayout) rootView.findViewById(R.id.board_main_frame)).addView(mikuView, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.RIGHT));
}*/
loadingView = rootView.findViewById(R.id.board_loading);
errorView = rootView.findViewById(R.id.board_error);
errorTextView = (TextView)errorView.findViewById(R.id.frame_error_text);
catalogBarView = (Spinner) rootView.findViewById(R.id.board_catalog_bar);
navigationBarView = rootView.findViewById(R.id.board_navigation_bar);
searchBarView = rootView.findViewById(R.id.board_search_bar);
pullableLayout = (SwipeRefreshLayout)rootView.findViewById(R.id.board_pullable_layout);
listView = (ListView)rootView.findViewById(android.R.id.list);
if (pageType != TYPE_POSTSLIST) listView.setOnItemClickListener(this);
registerForContextMenu(listView);
pullableLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
pullableLayoutSetRefreshingTime = System.currentTimeMillis();
if (tabModel.type == TabModel.TYPE_LOCAL) {
setPullableNoRefreshing();
openFromChan();
} else {
update(true, false, false);
}
}
});
BitmapCache bitmapCache = MainApplication.getInstance().bitmapCache;
imageGetter = new AsyncImageGetter(resources, R.dimen.inpost_image_size, bitmapCache,
chan, imagesDownloadExecutor, imagesDownloadTask, listView, Async.UI_HANDLER, staticSettings);
spanClickListener = new VolatileSpanClickListener(this);
floatingModels = measureFloatingModels(inflater);
activity.setTitle(tabModel.title);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
CompatibilityImpl.setActionBarCustomFavicon(activity, chan.getChanFavicon());
update(forceUpdateFirstTime, false, false);
return rootView;
}
@Override
public void onDestroy() {
super.onDestroy();
presentationModel = null; //если фрагмент всё-таки не уничтожится, позволить GC убрать хотя бы основные данные
finalizeSearchBar();
if (listView != null) {
listView.setOnCreateContextMenuListener(null);
listView.setOnItemClickListener(null);
listView.setOnTouchListener(null);
listView.setOnScrollListener(null);
listView.setAdapter(null);
}
if (pullableLayout != null) {
pullableLayout.setOnRefreshListener(null);
pullableLayout.setOnEdgeReachedListener(null);
}
if (tabModel != null && tabModel.type == TabModel.TYPE_LOCAL) {
try {
if (localFile != null) localFile.close();
} catch (Exception e) {
Logger.e(TAG, "cannot close local file", e);
}
}
imagesDownloadExecutor.shutdown();
if (tabModel != null) dialogs.onDestroyFragment(tabModel.id);
}
private void saveHistory() {
if (tabModel.type == TabModel.TYPE_LOCAL) return;
if (tabModel.pageModel.type != UrlPageModel.TYPE_BOARDPAGE && tabModel.pageModel.type != UrlPageModel.TYPE_THREADPAGE) return;
database.addHistory(
tabModel.pageModel.chanName,
tabModel.pageModel.boardName,
tabModel.pageModel.type == UrlPageModel.TYPE_BOARDPAGE ? Integer.toString(tabModel.pageModel.boardPage) : null,
tabModel.pageModel.type == UrlPageModel.TYPE_THREADPAGE ? tabModel.pageModel.threadNumber : null,
tabModel.title,
tabModel.webUrl);
}
private void updateHistoryFavorites() {
if (tabModel.type == TabModel.TYPE_LOCAL) return;
if (tabModel.pageModel.type != UrlPageModel.TYPE_BOARDPAGE && tabModel.pageModel.type != UrlPageModel.TYPE_THREADPAGE) return;
database.updateHistoryFavoritesEntries(
tabModel.pageModel.chanName,
tabModel.pageModel.boardName,
tabModel.pageModel.type == UrlPageModel.TYPE_BOARDPAGE ? Integer.toString(tabModel.pageModel.boardPage) : null,
tabModel.pageModel.type == UrlPageModel.TYPE_THREADPAGE ? tabModel.pageModel.threadNumber : null,
tabModel.title);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
MenuItem itemAddPost = menu.add(Menu.NONE, R.id.menu_add_post, 101,
resources.getString(pageType == TYPE_POSTSLIST ? R.string.menu_add_post : R.string.menu_add_thread));
MenuItem itemUpdate = menu.add(Menu.NONE, R.id.menu_update, 102,
resources.getString(tabModel.type != TabModel.TYPE_LOCAL ? R.string.menu_update : R.string.menu_from_internet));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
itemAddPost.setIcon(ThemeUtils.getActionbarIcon(activity.getTheme(), resources, R.attr.actionAddPost));
itemUpdate.setIcon(ThemeUtils.getActionbarIcon(activity.getTheme(), resources, R.attr.actionRefresh));
CompatibilityImpl.setShowAsActionIfRoom(itemAddPost);
CompatibilityImpl.setShowAsActionIfRoom(itemUpdate);
} else {
itemAddPost.setIcon(R.drawable.ic_menu_edit);
itemUpdate.setIcon(R.drawable.ic_menu_refresh);
}
menu.add(Menu.NONE, R.id.menu_catalog, 103, resources.getString(R.string.menu_catalog)).setIcon(R.drawable.ic_menu_list);
menu.add(Menu.NONE, R.id.menu_search, 104, resources.getString(R.string.menu_search)).setIcon(android.R.drawable.ic_menu_search);
menu.add(Menu.NONE, R.id.menu_save_page, 105, resources.getString(R.string.menu_save_page)).setIcon(android.R.drawable.ic_menu_save);
menu.add(Menu.NONE, R.id.menu_board_gallery, 106, resources.getString(R.string.menu_board_gallery)).setIcon(android.R.drawable.
ic_menu_slideshow);
menu.add(Menu.NONE, R.id.menu_quickaccess_add, 107, resources.getString(R.string.menu_quickaccess_add)).setIcon(R.drawable.
ic_menu_add_bookmark);
this.menu = menu;
updateMenu();
}
private void updateMenu() {
if (this.menu == null) return;
try {
boolean addPostMenuVisible = false;
boolean updateMenuVisible = false;
boolean catalogMenuVisible = false;
boolean searchMenuVisible = false;
boolean savePageMenuVisible = false;
boolean boardGallryMenuVisible = false;
boolean quickaccessAddMenuVisible = false;
if (tabModel.type != TabModel.TYPE_LOCAL && pageType != TYPE_SEARCHLIST && listLoaded &&
!presentationModel.source.boardModel.readonlyBoard) {
addPostMenuVisible = true;
}
if (tabModel.type != TabModel.TYPE_LOCAL || listLoaded) {
updateMenuVisible = true;
}
if (pageType == TYPE_THREADSLIST && listLoaded) {
if (presentationModel.source.boardModel.catalogAllowed) {
catalogMenuVisible = true;
}
if (presentationModel.source.boardModel.searchAllowed || tabModel.pageModel.type == UrlPageModel.TYPE_CATALOGPAGE) {
searchMenuVisible = true;
}
if (enableQuickAccessMenu == null) {
quickaccessAddMenuVisible = true;
String chanName = tabModel.pageModel.chanName;
String boardName = tabModel.pageModel.boardName;
for (QuickAccess.Entry entry : QuickAccess.getQuickAccessFromPreferences()) {
if (entry.boardName != null && entry.chan != null &&
entry.chan.getChanName().equals(chanName) && entry.boardName.equals(boardName)) {
quickaccessAddMenuVisible = false;
break;
}
}
enableQuickAccessMenu = Boolean.valueOf(quickaccessAddMenuVisible);
} else {
quickaccessAddMenuVisible = enableQuickAccessMenu.booleanValue();
}
}
if (pageType == TYPE_POSTSLIST && listLoaded) {
searchMenuVisible = true;
boardGallryMenuVisible = true;
}
if (tabModel.type != TabModel.TYPE_LOCAL && pageType == TYPE_POSTSLIST && listLoaded) {
savePageMenuVisible = true;
}
menu.findItem(R.id.menu_add_post).setVisible(addPostMenuVisible);
menu.findItem(R.id.menu_update).setVisible(updateMenuVisible);
menu.findItem(R.id.menu_catalog).setVisible(catalogMenuVisible);
menu.findItem(R.id.menu_search).setVisible(searchMenuVisible);
menu.findItem(R.id.menu_save_page).setVisible(savePageMenuVisible);
menu.findItem(R.id.menu_board_gallery).setVisible(boardGallryMenuVisible);
menu.findItem(R.id.menu_quickaccess_add).setVisible(quickaccessAddMenuVisible);
} catch (NullPointerException e) {
Logger.e(TAG, e);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
UrlPageModel model;
switch (item.getItemId()) {
case R.id.menu_add_post:
openPostForm(tabModel.hash, presentationModel.source.boardModel, getSendPostModel());
return true;
case R.id.menu_update:
if (tabModel.type == TabModel.TYPE_LOCAL) {
openFromChan();
} else {
update();
}
return true;
case R.id.menu_catalog:
model = new UrlPageModel();
model.chanName = chan.getChanName();
model.type = UrlPageModel.TYPE_CATALOGPAGE;
model.boardName = tabModel.pageModel.boardName;
UrlHandler.open(model, activity);
return true;
case R.id.menu_search:
initSearchBar();
searchBarView.setVisibility(View.VISIBLE);
((EditText) searchBarView.findViewById(R.id.board_search_field)).requestFocus();
return true;
case R.id.menu_save_page:
saveThisPage();
return true;
case R.id.menu_board_gallery:
openGridGallery();
return true;
case R.id.menu_quickaccess_add:
QuickAccess.Entry newEntry = new QuickAccess.Entry();
newEntry.chan = chan;
newEntry.boardName = presentationModel.source.boardModel.boardName;
newEntry.boardDescription = presentationModel.source.boardModel.boardDescription;
List<QuickAccess.Entry> quickaccessList = QuickAccess.getQuickAccessFromPreferences();
quickaccessList.add(0, newEntry);
QuickAccess.saveQuickAccessToPreferences(quickaccessList);
enableQuickAccessMenu = Boolean.FALSE;
item.setVisible(false);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
//контекстное меню для превью-аттачментов
if (v.getTag() != null && v.getTag() instanceof AttachmentModel) {
lastContextMenuAttachment = v;
AttachmentModel model = (AttachmentModel) v.getTag();
View tnView = v.findViewById(R.id.post_thumbnail_image);
if (tnView != null && tnView.getTag() == Boolean.FALSE && !downloadThumbnails()) {
menu.add(Menu.NONE, R.id.context_menu_thumb_load_thumb, 1, R.string.context_menu_show_thumbnail).
setOnMenuItemClickListener(contextMenuListener);
}
menu.add(Menu.NONE, R.id.context_menu_thumb_download, 2, R.string.context_menu_download_file);
menu.add(Menu.NONE, R.id.context_menu_thumb_copy_url, 3, R.string.context_menu_copy_url);
menu.add(Menu.NONE, R.id.context_menu_thumb_attachment_info, 4, R.string.context_menu_attachment_info);
menu.add(Menu.NONE, R.id.context_menu_thumb_reverse_search, 5, R.string.context_menu_reverse_search);
for (int id : new int[] {
R.id.context_menu_thumb_download,
R.id.context_menu_thumb_copy_url,
R.id.context_menu_thumb_attachment_info,
R.id.context_menu_thumb_reverse_search } ) {
menu.findItem(id).setOnMenuItemClickListener(contextMenuListener);
}
switch (model.type) {
case AttachmentModel.TYPE_AUDIO:
case AttachmentModel.TYPE_VIDEO:
case AttachmentModel.TYPE_OTHER_FILE:
menu.findItem(R.id.context_menu_thumb_reverse_search).setVisible(false);
break;
case AttachmentModel.TYPE_OTHER_NOTFILE:
menu.findItem(R.id.context_menu_thumb_reverse_search).setVisible(false);
menu.findItem(R.id.context_menu_thumb_download).setVisible(false);
break;
}
if (tabModel.type == TabModel.TYPE_LOCAL) {
menu.findItem(R.id.context_menu_thumb_download).setVisible(false);
}
return;
}
if (menu.findItem(R.id.context_menu_thumb_copy_url) != null) return;
//контекстное меню для обычных элементов
boolean isList = true;
lastContextMenuPosition = -1;
final PresentationItemModel model;
if (v.getId() == android.R.id.list) {
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
model = adapter.getItem(info.position);
if (model.hidden) {
return;
}
} else {
if (v.getTag() != null && v.getTag() instanceof PostsListAdapter.PostViewTag) {
PostsListAdapter.PostViewTag tag = (PostsListAdapter.PostViewTag) v.getTag();
if (!tag.isPopupDialog) return;
isList = false;
lastContextMenuPosition = tag.position;
model = adapter.getItem(lastContextMenuPosition);
} else {
return;
}
}
if (pageType == TYPE_POSTSLIST) {
menu.add(Menu.NONE, R.id.context_menu_reply, 1, R.string.context_menu_reply);
menu.add(Menu.NONE, R.id.context_menu_reply_with_quote, 2, R.string.context_menu_reply_with_quote);
menu.add(Menu.NONE, R.id.context_menu_select_text, 3, Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && isList ?
R.string.context_menu_select_text : R.string.context_menu_copy_text);
menu.add(Menu.NONE, R.id.context_menu_share, 4, R.string.context_menu_share);
menu.add(Menu.NONE, R.id.context_menu_hide, 5, R.string.context_menu_hide_post);
menu.add(Menu.NONE, R.id.context_menu_delete, 6, R.string.context_menu_delete);
menu.add(Menu.NONE, R.id.context_menu_report, 7, R.string.context_menu_report);
menu.add(Menu.NONE, R.id.context_menu_subscribe, 8, R.string.context_menu_subscribe);
if (!isList) {
for (int id : new int[] {
R.id.context_menu_reply,
R.id.context_menu_reply_with_quote,
R.id.context_menu_select_text,
R.id.context_menu_share,
R.id.context_menu_hide,
R.id.context_menu_delete,
R.id.context_menu_report,
R.id.context_menu_subscribe} ) {
menu.findItem(id).setOnMenuItemClickListener(contextMenuListener);
}
}
if (presentationModel.source.boardModel.readonlyBoard || tabModel.type == TabModel.TYPE_LOCAL) {
menu.findItem(R.id.context_menu_reply).setVisible(false);
menu.findItem(R.id.context_menu_reply_with_quote).setVisible(false);
}
if (model.isDeleted || (!presentationModel.source.boardModel.allowDeletePosts && (!presentationModel.source.boardModel.allowDeleteFiles ||
model.sourceModel.attachments == null || model.sourceModel.attachments.length == 0)) || tabModel.type == TabModel.TYPE_LOCAL) {
menu.findItem(R.id.context_menu_delete).setVisible(false);
}
if (model.isDeleted || presentationModel.source.boardModel.allowReport == BoardModel.REPORT_NOT_ALLOWED ||
tabModel.type == TabModel.TYPE_LOCAL) {
menu.findItem(R.id.context_menu_report).setVisible(false);
}
if (settings.isSubscriptionsEnabled()) {
if (subscriptions.hasSubscription(chan.getChanName(), presentationModel.source.boardModel.boardName,
presentationModel.source.pageModel.threadNumber, model.sourceModel.number)) {
menu.findItem(R.id.context_menu_subscribe).setTitle(R.string.context_menu_unsubscribe);
}
} else {
menu.findItem(R.id.context_menu_subscribe).setVisible(false);
}
} else if (pageType == TYPE_THREADSLIST && isList) {
menu.add(Menu.NONE, R.id.context_menu_open_in_new_tab, 1, R.string.context_menu_open_in_new_tab);
menu.add(Menu.NONE, R.id.context_menu_thread_preview, 2, R.string.context_menu_thread_preview);
menu.add(Menu.NONE, R.id.context_menu_reply_no_reading, 3, R.string.context_menu_reply_no_reading);
menu.add(Menu.NONE, R.id.context_menu_hide, 4, R.string.context_menu_hide_thread);
if (presentationModel.source.boardModel.readonlyBoard || tabModel.type == TabModel.TYPE_LOCAL) {
menu.findItem(R.id.context_menu_reply_no_reading).setVisible(false);
}
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
//контекстное меню для превью-аттачментов
switch (item.getItemId()) {
case R.id.context_menu_thumb_load_thumb:
bitmapCache.asyncGet(
ChanModels.hashAttachmentModel((AttachmentModel) lastContextMenuAttachment.getTag()),
((AttachmentModel) lastContextMenuAttachment.getTag()).thumbnail,
resources.getDimensionPixelSize(R.dimen.post_thumbnail_size),
chan,
null,
imagesDownloadTask,
(ImageView) lastContextMenuAttachment.findViewById(R.id.post_thumbnail_image),
imagesDownloadExecutor,
Async.UI_HANDLER,
true,
R.drawable.thumbnail_error);
return true;
case R.id.context_menu_thumb_download:
downloadFile((AttachmentModel) lastContextMenuAttachment.getTag());
return true;
case R.id.context_menu_thumb_copy_url:
String url = chan.fixRelativeUrl(((AttachmentModel) lastContextMenuAttachment.getTag()).path);
Clipboard.copyText(activity, url);
Toast.makeText(activity, resources.getString(R.string.notification_url_copied, url), Toast.LENGTH_LONG).show();
return true;
case R.id.context_menu_thumb_attachment_info:
String info = Attachments.getAttachmentInfoString(chan, ((AttachmentModel) lastContextMenuAttachment.getTag()), resources);
Toast.makeText(activity, info, Toast.LENGTH_LONG).show();
return true;
case R.id.context_menu_thumb_reverse_search:
ReverseImageSearch.openDialog(activity, chan.fixRelativeUrl(((AttachmentModel) lastContextMenuAttachment.getTag()).path));
return true;
}
//контекстное меню для обычных постов
int position = lastContextMenuPosition;
if (item.getMenuInfo() != null && item.getMenuInfo() instanceof AdapterView.AdapterContextMenuInfo) {
position = ((AdapterView.AdapterContextMenuInfo) item.getMenuInfo()).position;
}
if (nullAdapterIsSet || position == -1 || adapter.getCount() <= position) return false;
switch (item.getItemId()) {
case R.id.context_menu_open_in_new_tab:
UrlPageModel modelNewTab = new UrlPageModel();
modelNewTab.chanName = chan.getChanName();
modelNewTab.type = UrlPageModel.TYPE_THREADPAGE;
modelNewTab.boardName = tabModel.pageModel.boardName;
modelNewTab.threadNumber = adapter.getItem(position).sourceModel.parentThread;
String tabTitle = null;
String subject = adapter.getItem(position).sourceModel.subject;
if (subject != null && subject.length() != 0) {
tabTitle = subject;
} else {
Spanned spannedComment = adapter.getItem(position).spannedComment;
if (spannedComment != null) {
tabTitle = spannedComment.toString().replace('\n', ' ');
if (tabTitle.length() > MAX_TITLE_LENGHT) tabTitle = tabTitle.substring(0, MAX_TITLE_LENGHT);
}
}
if (tabTitle != null) tabTitle = resources.getString(R.string.tabs_title_threadpage_loaded, modelNewTab.boardName, tabTitle);
UrlHandler.open(modelNewTab, activity, false, tabTitle);
return true;
case R.id.context_menu_thread_preview:
showThreadPreviewDialog(position);
return true;
case R.id.context_menu_reply_no_reading:
UrlPageModel model = new UrlPageModel();
model.chanName = chan.getChanName();
model.type = UrlPageModel.TYPE_THREADPAGE;
model.boardName = tabModel.pageModel.boardName;
model.threadNumber = adapter.getItem(position).sourceModel.parentThread;
openPostForm(ChanModels.hashUrlPageModel(model), presentationModel.source.boardModel, getSendPostModel(model));
return true;
case R.id.context_menu_hide:
adapter.getItem(position).hidden = true;
database.addHidden(
tabModel.pageModel.chanName,
tabModel.pageModel.boardName,
pageType == TYPE_POSTSLIST ? tabModel.pageModel.threadNumber : adapter.getItem(position).sourceModel.number,
pageType == TYPE_POSTSLIST ? adapter.getItem(position).sourceModel.number : null);
adapter.notifyDataSetChanged();
return true;
case R.id.context_menu_reply:
openReply(position, false, null);
return true;
case R.id.context_menu_reply_with_quote:
openReply(position, true, null);
return true;
case R.id.context_menu_select_text:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && lastContextMenuPosition == -1) {
int firstPosition = listView.getFirstVisiblePosition() - listView.getHeaderViewsCount();
int wantedChild = position - firstPosition;
if (wantedChild >= 0 && wantedChild < listView.getChildCount()) {
View v = listView.getChildAt(wantedChild);
if (v != null && v.getTag() != null && v.getTag() instanceof PostsListAdapter.PostViewTag) {
((PostsListAdapter.PostViewTag)v.getTag()).commentView.startSelection();
return true;
}
}
}
Clipboard.copyText(activity, adapter.getItem(position).spannedComment.toString());
Toast.makeText(activity, resources.getString(R.string.notification_comment_copied), Toast.LENGTH_LONG).show();
return true;
case R.id.context_menu_share:
UrlPageModel sharePostUrlPageModel = new UrlPageModel();
sharePostUrlPageModel.chanName = chan.getChanName();
sharePostUrlPageModel.type = UrlPageModel.TYPE_THREADPAGE;
sharePostUrlPageModel.boardName = tabModel.pageModel.boardName;
sharePostUrlPageModel.threadNumber = tabModel.pageModel.threadNumber;
sharePostUrlPageModel.postNumber = adapter.getItem(position).sourceModel.number;
Intent sharePostIntent = new Intent(Intent.ACTION_SEND);
sharePostIntent.setType("text/plain");
sharePostIntent.putExtra(Intent.EXTRA_SUBJECT, chan.buildUrl(sharePostUrlPageModel));
sharePostIntent.putExtra(Intent.EXTRA_TEXT, adapter.getItem(position).spannedComment.toString());
startActivity(Intent.createChooser(sharePostIntent, resources.getString(R.string.share_via)));
return true;
case R.id.context_menu_delete:
DeletePostModel delModel = new DeletePostModel();
delModel.chanName = chan.getChanName();
delModel.boardName = tabModel.pageModel.boardName;
delModel.threadNumber = tabModel.pageModel.threadNumber;
delModel.postNumber = adapter.getItem(position).sourceModel.number;
runDelete(delModel,
adapter.getItem(position).sourceModel.attachments != null && adapter.getItem(position).sourceModel.attachments.length > 0);
return true;
case R.id.context_menu_report:
DeletePostModel reportModel = new DeletePostModel();
reportModel.chanName = chan.getChanName();
reportModel.boardName = tabModel.pageModel.boardName;
reportModel.threadNumber = tabModel.pageModel.threadNumber;
reportModel.postNumber = adapter.getItem(position).sourceModel.number;
runReport(reportModel);
return true;
case R.id.context_menu_subscribe:
String chanName = chan.getChanName();
String board = tabModel.pageModel.boardName;
String thread = tabModel.pageModel.threadNumber;
String post = adapter.getItem(position).sourceModel.number;
if (subscriptions.hasSubscription(chanName, board, thread, post)) {
subscriptions.removeSubscription(chanName, board, thread, post);
for (int i=position; i<adapter.getCount(); ++i) adapter.getItem(i).onUnsubscribe(post);
} else {
subscriptions.addSubscription(chanName, board, thread, post);
for (int i=position; i<adapter.getCount(); ++i) adapter.getItem(i).onSubscribe(post);
}
adapter.notifyDataSetChanged();
return true;
}
return false;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (adapter.getItem(position).hidden) {
PresentationItemModel model = adapter.getItem(position);
model.hidden = false;
database.removeHidden(
tabModel.pageModel.chanName,
tabModel.pageModel.boardName,
pageType == TYPE_POSTSLIST ? tabModel.pageModel.threadNumber : model.sourceModel.number,
pageType == TYPE_POSTSLIST ? model.sourceModel.number : null);
adapter.notifyDataSetChanged();
return;
}
if (pageType != TYPE_POSTSLIST) {
UrlPageModel model = new UrlPageModel();
model.chanName = chan.getChanName();
model.type = UrlPageModel.TYPE_THREADPAGE;
model.boardName = tabModel.pageModel.boardName;
model.threadNumber = adapter.getItem(position).sourceModel.parentThread;
if (pageType == TYPE_SEARCHLIST) {
PostModel postModel = adapter.getItem(position).sourceModel;
if (!postModel.parentThread.equals(postModel.number)) {
model.postNumber = postModel.number;
}
}
UrlHandler.open(model, activity);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
saveCurrentPostPosition();
}
@Override
public void onPause() {
super.onPause();
activity.setDrawerLock(DrawerLayout.LOCK_MODE_UNLOCKED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) CompatibilityImpl.showActionBar(activity);
saveCurrentPostPosition();
}
@Override
public void onResume() {
super.onResume();
try {
TabsTrackerService.onResumeTab(activity, tabModel.webUrl, tabModel.title);
} catch (Exception e) {
Logger.e(TAG, e);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (currentTask != null) {
currentTask.cancel();
}
if (imagesDownloadTask != null) {
imagesDownloadTask.cancel();
}
saveCurrentPostPosition();
}
private void saveCurrentPostPosition() {
try {
String startItemNumber;
int startItemTop;
if (/*pageType == TYPE_POSTSLIST && */nullAdapterIsSet) {
startItemNumber = nullAdapterSavedNumber;
startItemTop = nullAdapterSavedTop;
} else if (listView != null && listView.getChildCount() > 0 && adapter != null) {
View v = listView.getChildAt(0);
int position = listView.getPositionForView(v);
PresentationItemModel model = adapter.getItem(position);
startItemNumber = model.sourceModel.number;
startItemTop = v == null ? 0 : v.getTop();
} else return;
if (startItemTop != tabModel.startItemTop || !startItemNumber.equals(tabModel.startItemNumber)) {
tabModel.startItemNumber = startItemNumber;
tabModel.startItemTop = startItemTop;
MainApplication.getInstance().serializer.serializeTabsState(MainApplication.getInstance().tabsState);
}
} catch (Exception e) {
Logger.e(TAG, e);
}
}
private void resetFirstUnreadPosition() {
firstUnreadPosition = adapter.getCount();
tabModel.firstUnreadPosition = firstUnreadPosition;
MainApplication.getInstance().serializer.serializeTabsState(MainApplication.getInstance().tabsState);
adapter.notifyDataSetChanged();
}
@Override
public void onURLSpanClick(View v, ClickableURLSpan span, String url, String referer) {
if (presentationModel == null || presentationModel.presentationList == null) return;
if (tabModel.pageModel.type != UrlPageModel.TYPE_THREADPAGE) {
if (!url.startsWith("#")) UrlHandler.open(chan.fixRelativeUrl(url), activity);
return;
}
if (url.startsWith(PresentationItemModel.ALL_REFERENCES_URI)) {
openReferencesList(url.substring(PresentationItemModel.ALL_REFERENCES_URI.length()));
return;
}
boolean sameThread = false;
if (url.startsWith("#")) {
UrlPageModel thisThreadModel = new UrlPageModel();
thisThreadModel.chanName = chan.getChanName();
thisThreadModel.type = UrlPageModel.TYPE_THREADPAGE;
thisThreadModel.boardName = tabModel.pageModel.boardName;
thisThreadModel.threadNumber = tabModel.pageModel.threadNumber;
url = chan.buildUrl(thisThreadModel) + url;
}
String fixedUrl = chan.fixRelativeUrl(url);
UrlPageModel model = UrlHandler.getPageModel(fixedUrl);
if (model != null && model.type != UrlPageModel.TYPE_OTHERPAGE &&
(tabModel.type != TabModel.TYPE_LOCAL ?
ChanModels.hashUrlPageModel(model).equals(tabModel.hash) :
ChanModels.hashUrlPageModel(model).equals(ChanModels.hashUrlPageModel(tabModel.pageModel)))) {
sameThread = true;
}
if (sameThread) {
if (TextUtils.isEmpty(model.postNumber)) model.postNumber = model.threadNumber;
int itemPosition = -1;
for (int i=0; i<presentationModel.presentationList.size(); ++i) {
if (presentationModel.presentationList.get(i).sourceModel.number.equals(model.postNumber)) {
itemPosition = i;
break;
}
}
if (itemPosition != -1) {
if (settings.isPopupLinks()) {
String refererPost = null;
if (referer != null) {
if (referer.startsWith(PresentationItemModel.POST_REFERER)) {
refererPost = referer.substring(PresentationItemModel.POST_REFERER.length());
} else {
try {
refererPost = UrlHandler.getPageModel(referer).postNumber;
} catch (Exception e) {}
}
}
boolean tabletMode = settings.isRealTablet() && !staticSettings.repliesOnlyQuantity;
showPostPopupDialog(itemPosition, tabletMode, getSpanCoordinates(v, span), refererPost);
} else {
listView.setSelection(itemPosition);
}
} else {
Toast.makeText(activity, R.string.notification_post_not_found, Toast.LENGTH_LONG).show();
}
} else {
UrlHandler.open(fixedUrl, activity);
}
}
/**
* Измеряет thumbnail view и создаёт модели обтекания его текстом.
* Также сохраняет ширину thumbnail view в поле {@link #thumbnailWidth}
*/
private FloatingModel[] measureFloatingModels(LayoutInflater inflater) {
Point displaySize = AppearanceUtils.getDisplaySize(activity.getWindowManager().getDefaultDisplay());
LinearLayout view = (LinearLayout)inflater.inflate(R.layout.post_item_layout, (ViewGroup) rootView, false);
TextView commentView = (TextView)view.findViewById(R.id.post_comment);
TextPaint textPaint = commentView.getPaint();
int textLineHeight = Math.max(1, commentView.getLineHeight());
int rootWidth = (int) (displaySize.x * settings.getRootViewWeight());
postItemPadding = view.getPaddingLeft() + view.getPaddingRight();
int textWidth = postItemWidth = rootWidth - postItemPadding;
View thumbnailView = view.findViewById(R.id.post_thumbnail);
ViewGroup.MarginLayoutParams thumbnailLayoutParams = (ViewGroup.MarginLayoutParams)thumbnailView.getLayoutParams();
thumbnailMargin = thumbnailLayoutParams.leftMargin + thumbnailLayoutParams.rightMargin;
View attachmentTypeView = thumbnailView.findViewById(R.id.post_thumbnail_attachment_type);
FloatingModel[] floatingModels = new FloatingModel[2];
attachmentTypeView.setVisibility(View.GONE);
thumbnailView.measure(displaySize.x, displaySize.y);
Point thumbnailSize = new Point(thumbnailMargin + thumbnailView.getMeasuredWidth(), thumbnailView.getMeasuredHeight());
floatingModels[0] = new FloatingModel(thumbnailSize, textWidth, textPaint);
attachmentTypeView.setVisibility(View.VISIBLE);
thumbnailView.measure(displaySize.x, displaySize.y);
thumbnailSize = new Point(thumbnailMargin + thumbnailView.getMeasuredWidth(), thumbnailView.getMeasuredHeight());
floatingModels[1] = new FloatingModel(thumbnailSize, textWidth, textPaint);
thumbnailWidth = thumbnailSize.x;
maxItemLines = divcell(thumbnailSize.y, textLineHeight);
return floatingModels;
}
private void switchToLoadingView() {
loadingView.setVisibility(View.VISIBLE);
errorView.setVisibility(View.GONE);
pullableLayout.setVisibility(View.GONE);
catalogBarView.setVisibility(View.GONE);
searchBarView.setVisibility(View.GONE);
navigationBarView.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) {
switchToErrorView(message, false);
}
private void switchToErrorView(String message, boolean silent) {
if (listLoaded) {
setPullableNoRefreshing();
if (!silent) showUpdateError(message);
return;
}
loadingView.setVisibility(View.GONE);
errorView.setVisibility(View.VISIBLE);
pullableLayout.setVisibility(View.GONE);
catalogBarView.setVisibility(View.GONE);
searchBarView.setVisibility(View.GONE);
navigationBarView.setVisibility(View.GONE);
errorTextView.setText(fixErrorMessage(message));
}
private void showUpdateError(String message) {
Toast.makeText(activity, fixErrorMessage(message), Toast.LENGTH_LONG).show();
}
private void switchToListView() {
loadingView.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
pullableLayout.setVisibility(View.VISIBLE);
catalogBarView.setVisibility(tabModel.pageModel.type == UrlPageModel.TYPE_CATALOGPAGE ? View.VISIBLE : View.GONE);
navigationBarView.setVisibility(tabModel.pageModel.type == UrlPageModel.TYPE_BOARDPAGE ? View.VISIBLE : View.GONE);
searchBarView.setVisibility(View.GONE);
setNavigationCatalogBar();
}
private class PageGetter extends CancellableTask.BaseCancellableTask implements Runnable {
private final boolean forceUpdate;
private final boolean silent;
private PageLoaderFromChan pageLoader = null;
private final boolean isThreadPage;
public PageGetter(boolean forceUpdate, boolean silent) {
this.forceUpdate = forceUpdate;
this.silent = silent;
isThreadPage = pageType == TYPE_POSTSLIST;
}
@Override
public void run() {
if (forceUpdate) saveHistory();
while (TabsTrackerService.getCurrentUpdatingTabId() == tabModel.id) Thread.yield();
//обработать случай, когда вкладка - локально сохранённая страница
if (tabModel.type == TabModel.TYPE_LOCAL) {
if (!forceUpdate) {
presentationModel = pagesCache.getPresentationModel(tabModel.hash);
if (presentationModel != null) {
((AsyncImageGetter)presentationModel.imageGetter).setObjects(
imagesDownloadExecutor, imagesDownloadTask, listView, Async.UI_HANDLER, staticSettings);
((VolatileSpanClickListener)presentationModel.spanClickListener).setListener(BoardFragment.this);
presentationModel.setFloatingModels(floatingModels);
if (presentationModel == null) return;
if (presentationModel.isNotReady()) presentationModel.updateViewModels(true, this, null);
toListView(forceUpdate);
return;
}
}
if (localFile != null) {
SerializablePage page;
try {
page = MainApplication.getInstance().serializer.loadPage(localFile.openStream(DownloadingService.MAIN_OBJECT_FILE));
} catch (Exception e) {
Logger.e(TAG, "cannot deserialize local page from json", e);
page = null;
}
if (page != null) {
createPresentationModel(page, false, false);
return;
}
}
Async.runOnUiThread(new Runnable() {
@Override
public void run() {
switchToErrorView(resources.getString(R.string.error_open_local));
}
});
return;
}
//нужно ли пытаться получить из кэша/десериализовать
boolean tryGetFromCache = !listLoaded && (!forceUpdate || isThreadPage);
if (tryGetFromCache) {
//попробовать получить сразу PresentationModel из LRU-кэша PagesCache в памяти
presentationModel = pagesCache.getPresentationModel(tabModel.hash);
if (presentationModel != null) {
((AsyncImageGetter)presentationModel.imageGetter).setObjects(
imagesDownloadExecutor, imagesDownloadTask, listView, Async.UI_HANDLER, staticSettings);
((VolatileSpanClickListener)presentationModel.spanClickListener).setListener(BoardFragment.this);
presentationModel.setFloatingModels(floatingModels);
if (presentationModel == null) return;
if (presentationModel.isNotReady()) presentationModel.updateViewModels(isThreadPage, this, null);
toListView(forceUpdate);
} else {
SerializablePage pageFromFileCache = pagesCache.getSerializablePage(tabModel.hash);
if (pageFromFileCache != null) {
createPresentationModel(pageFromFileCache, forceUpdate, false);
} else {
loadFromChan();
}
}
} else if (forceUpdate) {
loadFromChan();
}
}
/** после загрузки с чана отправляет на ListView */
private void loadFromChan() {
final SerializablePage pageFromChan;
final boolean fromScratch;
if (presentationModel != null && presentationModel.source != null) {
pageFromChan = presentationModel.source;
fromScratch = false;
} else {
pageFromChan = new SerializablePage();
pageFromChan.pageModel = tabModel.pageModel;
fromScratch = true;
}
final int itemsCountBefore =
pageFromChan.posts != null ? pageFromChan.posts.length :
(pageFromChan.threads != null ? pageFromChan.threads.length : 0);
pageLoader = new PageLoaderFromChan(pageFromChan, new PageLoaderFromChan.PageLoaderCallback() {
@Override
public void onSuccess() {
updatingNow = false;
if (isCancelled()) return;
BackgroundThumbDownloader.download(pageFromChan, imagesDownloadTask);
MainApplication.getInstance().subscriptions.checkOwnPost(pageFromChan, itemsCountBefore);
if (isCancelled()) return;
if (fromScratch) {
createPresentationModel(pageFromChan, false, true);
} else {
presentationModel.updateViewModels(isThreadPage, PageGetter.this, new PresentationModel.RebuildCallback() {
@Override
public void onRebuild() {
try {
View v = listView.getChildAt(0);
nullAdapterSavedPosition = listView.getPositionForView(v);
nullAdapterSavedTop = v.getTop();
nullAdapterSavedNumber = adapter.getItem(nullAdapterSavedPosition).sourceModel.number;
} catch (Exception e) {
Logger.e(TAG, e);
}
nullAdapterIsSet = true;
nullAdapter();
}
});
presentationModel = new PresentationModel(presentationModel); //обновить immutable-значение postsCount
pagesCache.putPresentationModel(tabModel.hash, presentationModel);
if (isCancelled()) return;
if (startItem != null) {
for (int i=0; i<presentationModel.presentationList.size(); ++i) {
if (presentationModel.presentationList.get(i).sourceModel.number.equals(startItem)) {
startItemPosition = i;
break;
}
}
}
startItem = null; //уже загрузили с чана а не из кэша, так что дальше искать якорь на данный пост смысла нет
if (isCancelled()) return;
int checkSubscriptions = subscriptions.checkSubscriptions(pageFromChan, itemsCountBefore);
final String newSubscription = checkSubscriptions >= 0 ? pageFromChan.posts[checkSubscriptions].number : null;
if (isCancelled()) return;
Async.runOnUiThread(new Runnable() {
@Override
public void run() {
if (presentationModel == null || presentationModel.isNotReady() || adapter == null)
Toast.makeText(activity, R.string.error_unknown, Toast.LENGTH_LONG).show();
if (adapter == null) return;
if (nullAdapterIsSet) {
listView.setAdapter(adapter);
listView.requestFocus();
hackListViewSetPosition(listView, nullAdapterSavedPosition, nullAdapterSavedTop);
nullAdapterIsSet = false;
}
adapter.notifyDataSetChanged();
if (isThreadPage && adapter.getCount() != itemsCountBefore) resetSearchCache();
setPullableNoRefreshing();
if (startItemPosition != -1) {
hackListViewSetPosition(listView, startItemPosition, startItemTop);
startItemPosition = -1;
}
String notification;
boolean toastToNewPosts = false;
if (isThreadPage) {
int newPostsCount = adapter.getCount() - itemsCountBefore;
if (newPostsCount <= 0) {
notification = resources.getString(R.string.postslist_no_new_posts);
} else {
notification = resources.getQuantityString(
R.plurals.postslist_new_posts_quantity, newPostsCount, newPostsCount);
toastToNewPosts = true;
if (silent && activity.isPaused()) {
TabsTrackerService.setUnread();
if (newSubscription != null) {
TabsTrackerService.addSubscriptionNotification(tabModel.webUrl, newSubscription, tabModel.title);
}
}
}
} else {
notification = resources.getString(R.string.postslist_list_updated);
}
if (!silent) {
if (toastToNewPosts) {
ClickableToast.showText(activity, notification, new ClickableToast.OnClickListener() {
@Override
public void onClick() {
listView.setSelection(itemsCountBefore);
}
});
} else {
Toast.makeText(activity, notification, Toast.LENGTH_LONG).show();
}
}
}
});
}
}
@Override
public void onError(final String message) {
updatingNow = false;
if (isCancelled()) return;
Async.runOnUiThread(new Runnable() {
@Override
public void run() {
switchToErrorView(message, silent);
}
});
}
@Override
public void onInteractiveException(final InteractiveException e) {
if (isCancelled()) return;
if (silent && activity.isPaused()) {
Async.runOnUiThread(new Runnable() {
@Override
public void run() {
setPullableNoRefreshing();
}
});
return;
}
e.handle(activity, PageGetter.this, new InteractiveException.Callback() {
@Override public void onSuccess() { updatingNow = false; update(true, false, false); }
@Override public void onError(String message) { updatingNow = false; switchToErrorView(message); }
});
}
}, chan, this);
if (isCancelled()) return;
updatingNow = true;
pageLoader.run();
}
/**
* Создаёт (с нуля) {@link PresentationModel} и отправляет на listView
* @param serializablePage
* @param needUpdateAfter требуется ли обновить страницу на чане после создания и показа {@link PresentationModel}
* @param putToFileCache положить соответствующую сериализованную модель SerializablePage в файловый кэш
*/
private void createPresentationModel(SerializablePage serializablePage, boolean needUpdateAfter, boolean putToFileCache) {
presentationModel = new PresentationModel(
serializablePage,
settings.isLocalTime(),
settings.isReduceNames(),
spanClickListener,
imageGetter,
activity.getTheme(),
pageType == TYPE_THREADSLIST ? null : floatingModels);
presentationModel.updateViewModels(isThreadPage, PageGetter.this, null);
pagesCache.putPresentationModel(tabModel.hash, presentationModel, putToFileCache);
if (isCancelled()) return;
activity.sendBroadcast(new Intent(BROADCAST_PAGE_LOADED));
toListView(needUpdateAfter);
}
private volatile boolean nullAdapterFlag;
/** обнулить адаптер listView (пока производятся манипуляции с внутренним list), из не-UI потока */
private void nullAdapter() {
nullAdapterFlag = true;
Async.runOnUiThread(new Runnable() {
public void run() {
listView.setAdapter(null);
nullAdapterFlag = false;
}
});
while (nullAdapterFlag) Thread.yield();
}
/** @param needUpdateAfter - требуется ли обновить страницу на чане после показа */
private void toListView(final boolean needUpdateAfter) {
if (presentationModel == null || presentationModel.presentationList == null) return;
adapter = new PostsListAdapter(BoardFragment.this);
if (presentationModel == null) return;
if (pageType == TYPE_POSTSLIST && tabModel.firstUnreadPosition == 0) {
resetFirstUnreadPosition();
}
String oldTabTitle = tabModel.title != null ? tabModel.title : "";
if (presentationModel == null) return;
if (isThreadPage && presentationModel.presentationList.size() > 0) {
String tabTitle;
String subject = presentationModel.presentationList.get(0).sourceModel.subject;
if (subject != null && subject.length() != 0) {
tabTitle = subject;
} else {
tabTitle = presentationModel.presentationList.get(0).spannedComment.toString().replace('\n', ' ');
if (tabTitle.length() > MAX_TITLE_LENGHT) {
tabTitle = tabTitle.substring(0, MAX_TITLE_LENGHT);
}
}
tabModel.title = resources.getString(R.string.tabs_title_threadpage_loaded, tabModel.pageModel.boardName, tabTitle);
} else if (tabModel.pageModel.type == UrlPageModel.TYPE_BOARDPAGE
&& tabModel.pageModel.boardPage == presentationModel.source.boardModel.firstPage) {
tabModel.title = resources.getString(R.string.tabs_title_boardpage_first, tabModel.pageModel.boardName);
}
final boolean tabTitleChanged = !oldTabTitle.equals(tabModel.title);
if (tabTitleChanged) updateHistoryFavorites();
if (startItem != null) {
for (int i=0; i<presentationModel.presentationList.size(); ++i) {
if (presentationModel.presentationList.get(i).sourceModel.number.equals(startItem)) {
startItemPosition = i;
startItem = null;
break;
}
}
}
listLoaded = true;
Async.runOnUiThread(new Runnable() {
/** установить SwipeDismissListViewTouchListener, если требуется (соответствует версия ОС, открыт список тредов, включена настройка);
* возвращает созданный OnScrollListener */
private ListView.OnScrollListener setSwipeDismissListener() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 && pageType == TYPE_THREADSLIST &&
settings.swipeToHideThread()) {
final SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(listView,
new SwipeDismissListViewTouchListener.DismissCallbacks() {
@Override
public void onDismiss(ListView listView, int[] reverseSortedPositions) {
for (int i : reverseSortedPositions) {
adapter.getItem(i).hidden = true;
database.addHidden(tabModel.pageModel.chanName, tabModel.pageModel.boardName,
adapter.getItem(i).sourceModel.number, null);
adapter.notifyDataSetChanged();
}
}
@Override
public boolean canDismiss(int position) {
return !adapter.getItem(position).hidden;
}
});
listView.setOnTouchListener(touchListener);
return touchListener.makeScrollListener();
}
return null;
}
@Override
public void run() {
if (presentationModel == null || presentationModel.isNotReady())
Toast.makeText(activity, R.string.error_unknown, Toast.LENGTH_LONG).show();
listView.setAdapter(adapter);
listView.requestFocus();
final ListView.OnScrollListener swipeDismissOnScrollListener = setSwipeDismissListener();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ECLAIR_MR1) {
//busy состояние адаптера, не загружать картинки из интернета, во время скроллинга
listView.setOnScrollListener(new ListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (swipeDismissOnScrollListener != null) swipeDismissOnScrollListener.onScrollStateChanged(view, scrollState);
if (scrollState == ListView.OnScrollListener.SCROLL_STATE_IDLE) {
adapter.setBusy(false);
} else {
adapter.setBusy(true);
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//скрытие actionbar
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ||
!staticSettings.hideActionBar || view.getChildCount() <= 0) return;
int firstVisibleTop = view.getChildAt(0).getTop();
int topDelta = firstVisibleTop - lastFirstVisibleTop;
if (firstVisibleItem == lastFirstVisibleItem && Math.abs(topDelta) < maxTopDelta) {
if ((currentTopDelta < 0) == (topDelta < 0)) currentTopDelta += topDelta; else currentTopDelta = topDelta;
} else if (firstVisibleItem != lastFirstVisibleItem && adapter.isBusy) {
currentTopDelta = Integer.signum(lastFirstVisibleItem - firstVisibleItem) * (maxTopDelta + 1);
} else {
currentTopDelta = 0;
}
boolean top = firstVisibleItem == 0 && firstVisibleTop == 0;
long currentTime = System.currentTimeMillis();
if (top || currentTime - lastActionTime > 1000) {
if (currentTopDelta < -maxTopDelta) {
if (CompatibilityImpl.hideActionBar(activity)) {
lastActionTime = currentTime;
currentTopDelta = 0;
}
} else if (top || currentTopDelta > maxTopDelta) {
if (CompatibilityImpl.showActionBar(activity)) {
lastActionTime = currentTime;
currentTopDelta = 0;
}
}
}
lastFirstVisibleItem = firstVisibleItem;
lastFirstVisibleTop = firstVisibleTop;
}
private int lastFirstVisibleItem = Integer.MAX_VALUE;
private int lastFirstVisibleTop = Integer.MAX_VALUE;
private int currentTopDelta = 0;
private int maxTopDelta = (int) (resources.getDisplayMetrics().density * 24 + 0.5f);
private long lastActionTime = System.currentTimeMillis();
});
pullableLayout.setOnEdgeReachedListener(new SwipeRefreshLayout.OnEdgeReachedListener() {
@Override
public void onEdgeReached() {
adapter.setBusy(false);
}
});
}
switchToListView();
updateMenu();
if (isThreadPage) {
activity.setTitle(tabModel.title);
} else if (pageType == TYPE_THREADSLIST) {
if (presentationModel != null) activity.setTitle(presentationModel.source.boardModel.boardDescription);
}
if (activity.tabsAdapter != null && tabTitleChanged) {
activity.tabsAdapter.notifyDataSetChanged();
}
if (startItemPosition != -1) {
hackListViewSetPosition(listView, startItemPosition, startItemTop);
startItemPosition = -1;
}
if (needUpdateAfter) {
AppearanceUtils.callWhenLoaded(pullableLayout, new Runnable() {
@Override
public void run() {
update(true, true, silent);
}
});
}
}
});
}
@Override
public void cancel() {
super.cancel();
updatingNow = false;
}
}
private static void hackListViewSetPosition(final ListView listView, final int position, final int top) {
try {
listView.setSelectionFromTop(position, top);
AppearanceUtils.callWhenLoaded(listView, new Runnable() {
@Override
public void run() {
try {
int setPosition = listView.getFirstVisiblePosition();
int setTop = listView.getChildAt(0).getTop();
int incTop = listView.getChildCount() < 2 ? 0 : Math.max(0, -listView.getChildAt(1).getTop());
if (setPosition != position || setTop != top || incTop > 0) {
listView.setSelectionFromTop(position, top + incTop);
}
} catch(Exception e) {
Logger.e(TAG, e);
}
}
});
} catch(Exception e) {
Logger.e(TAG, e);
}
}
private static class PostsListAdapter extends ArrayAdapter<PresentationItemModel> {
private static final int ITEM_VIEW_TYPE_NORMAL = 0;
private static final int ITEM_VIEW_TYPE_HIDDEN = 1;
private final WeakReference<BoardFragment> fragmentRef;
private volatile boolean isBusy = false;
private final LayoutInflater inflater;
private final SparseBooleanArray expanded = new SparseBooleanArray();
private final int thumbnailsInRowCount;
private int currentCount;
private int[] hackListViewPosition = null; //смещение скроллинга, если в последних есть сокращённый длинный пост ("Показать весь текст")
private BoardFragment fragment() {
return fragmentRef.get();
}
private static class WeakOnCreateContextMenuListener implements View.OnCreateContextMenuListener {
private final WeakReference<BoardFragment> fragmentRef;
public WeakOnCreateContextMenuListener(BoardFragment fragment) {
this.fragmentRef = new WeakReference<>(fragment);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
BoardFragment fragment = fragmentRef.get();
if (fragment == null || fragment.presentationModel == null) {
Fragment currentFragment = MainApplication.getInstance().tabsSwitcher.currentFragment;
if (currentFragment instanceof BoardFragment) fragment = (BoardFragment) currentFragment;
}
if (fragment != null) {
try {
fragment.onCreateContextMenu(menu, v, menuInfo);
} catch (Exception e) {
Logger.e(TAG, e);
}
}
}
}
private void weakRegisterForContextMenu(View v) {
v.setOnCreateContextMenuListener(new WeakOnCreateContextMenuListener(fragment()));
}
private static class OnUnreadFrameListener implements View.OnClickListener, View.OnLongClickListener {
private WeakReference<BoardFragment> fragmentRef;
public OnUnreadFrameListener(WeakReference<BoardFragment> fragmentRef) {
this.fragmentRef = fragmentRef;
}
@Override
public boolean onLongClick(View v) {
fragmentRef.get().resetFirstUnreadPosition();
return true;
}
@Override
public void onClick(View v) {
fragmentRef.get().resetFirstUnreadPosition();
}
}
private static class OnAttachmentClickListener implements View.OnClickListener {
private WeakReference<BoardFragment> fragmentRef;
public OnAttachmentClickListener(WeakReference<BoardFragment> fragmentRef) {
this.fragmentRef = fragmentRef;
}
@Override
public void onClick(View v) {
BoardFragment fragment = fragmentRef.get();
if (fragment == null || fragment.presentationModel == null) {
Fragment currentFragment = MainApplication.getInstance().tabsSwitcher.currentFragment;
if (currentFragment instanceof BoardFragment) fragment = (BoardFragment) currentFragment;
}
if (fragment != null) fragment.openAttachment((AttachmentModel)v.getTag());
}
}
private OnUnreadFrameListener onUnreadFrameListener;
private OnAttachmentClickListener onAttachmentClickListener;
public PostsListAdapter(BoardFragment fragment) {
super(fragment.activity, 0, fragment.presentationModel.presentationList);
fragmentRef = new WeakReference<BoardFragment>(fragment);
onUnreadFrameListener = new OnUnreadFrameListener(fragmentRef);
onAttachmentClickListener = new OnAttachmentClickListener(fragmentRef);
if (fragment.presentationModel != null) // может обнулиться в BoardFragment.onDestroy() (т.к. метод работает асинхронно)
this.currentCount = fragment.presentationModel.presentationList.size();
this.inflater = LayoutInflater.from(fragment.activity);
this.thumbnailsInRowCount = Math.max(1, fragment.postItemWidth / fragment.thumbnailWidth);
}
static class PostViewTag {
public static final int MAX_BADGE_ICONS = 10;
public static final int MAX_THUMBNAILS = 20;
public static final int MAX_THUMBNAIL_ROWS = 20;
public int position;
public boolean isPopupDialog = false;
public boolean clickableLinksSet = false;
public View unreadFrame;
public boolean unreadFrameIsVisible = false;
public JellyBeanSpanFixTextView headerView;
public TextView stickyClosedThreadView;
public boolean stickyClosedThreadIsVisible = false;
public View deletedPostView;
public boolean deletedPostViewIsVisible = false;
public TextView dateView;
public boolean dateIsVisible = false;
public LinearLayout badgeViewContainer;
public ImageView[] badgeIcons = new ImageView[MAX_BADGE_ICONS];
public int badgeIconsInflatedCount = 0;
public int badgeIconsVisibleCount = 0;
public TextView badgeText;
public boolean badgeIsVisible = false;
public LinearLayout multiThumbnailsViewContainer;
public LinearLayout[] multiThumbnailsRows = new LinearLayout[MAX_THUMBNAIL_ROWS];
public View[] multiThumbnails = new View[MAX_THUMBNAILS];
public int multiThumbnailsInflatedCount = 0;
public int multiThumbnailsVisibleCount = 0;
public boolean multiThumbnailsIsVisible = false;
public View singleThumbnailView;
public boolean singleThumbnailIsVisible = false;
public ClickableLinksTextView commentView;
public boolean commentFloatingPosition = false;
public TextView showFullTextView;
public boolean showFullTextIsVisible = false;
public JellyBeanSpanFixTextView repliesView;
public boolean repliesIsVisible = false;
public TextView postsCountView;
public boolean postsCountIsVisible = false;
}
@Override
public int getCount() {
return currentCount;
}
@Override
public void notifyDataSetChanged() {
try {
currentCount = fragment().presentationModel.presentationList.size();
if (fragment().pageType != TYPE_THREADSLIST && fragment().staticSettings.itemHeight != 0) {
boolean needHack = false;
for (int i=0, len=fragment().listView.getChildCount(); i<len; ++i) {
View v = fragment().listView.getChildAt(i);
if (v.getTag() instanceof PostViewTag && ((PostViewTag) v.getTag()).showFullTextIsVisible) {
needHack = true;
break;
}
}
if (needHack) {
View v = fragment().listView.getChildAt(0);
int position = fragment().listView.getPositionForView(v);
hackListViewPosition = new int[] { position, v.getTop() };
} else {
hackListViewPosition = null;
}
}
} catch (Exception e) {
Logger.e(TAG, e);
hackListViewPosition = null;
}
super.notifyDataSetChanged();
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public int getItemViewType(int position) {
return this.getItem(position).hidden ? ITEM_VIEW_TYPE_HIDDEN : ITEM_VIEW_TYPE_NORMAL;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return getView(position, convertView, parent, null);
}
/**
* Измерить значение ширины вида (view) элемента, выше которого устанавливать не имеет смысла
* @param position позиция элемента в списке
* @return значение ширины
*/
public int measureViewWidth(int position) {
View tmp = getView(position, null, null, Integer.MAX_VALUE);
tmp.findViewById(R.id.post_frame_main).
measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
PostViewTag tag = (PostViewTag)tmp.getTag();
int width = tmp.findViewById(R.id.post_frame_main).getMeasuredWidth() + (tag.commentFloatingPosition ? fragment().thumbnailWidth : 0);
if (!tag.dateIsVisible) return width;
tag.headerView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
tag.dateView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int margin = Math.round(12 * (fragment().resources.getDisplayMetrics().xdpi / DisplayMetrics.DENSITY_DEFAULT));
int forHeader = tag.headerView.getMeasuredWidth() + tag.dateView.getMeasuredWidth() + margin - width;
if (forHeader > 0) width += forHeader;
return width;
}
/**
* Построить вид (view) элемента
* @param position позиция элемента в списке
* @param convertView старый view для вторичного использования (при вызове через адаптер)
* @param parent родительский view, к которому будет прикреплён создающийся (при вызове через адаптер)
* @param popupWidth ширина окна, в котором вид будет отображён (если необходимо получить view для всплывающего диалога).
* Для получения вида по умолчанию (в адаптере listview) необходимо передать null.
* @return
*/
public View getView(int position, View convertView, ViewGroup parent, Integer popupWidth) {
return getView(position, convertView, parent, popupWidth, null, null);
}
/**
* @param position позиция элемента в списке
* @param convertView старый view для вторичного использования (при вызове через адаптер)
* @param parent родительский view, к которому будет прикреплён создающийся (при вызове через адаптер)
* @param popupWidth ширина окна, в котором вид будет отображён (если необходимо получить view для всплывающего диалога).
* Для получения вида по умолчанию (в адаптере listview) необходимо передать null.
* @param custom если != null, строится View (только для всплывающего диалога, popupWidth не должно быть равно null)
* не для элемента на позиции position, а для этой модели. При этом комментарий не переносится в ScrollView (т.к. в диалоге будет ListView)
*/
public View getView(int position, View convertView, ViewGroup parent, Integer popupWidth, PresentationItemModel custom) {
return getView(position, convertView, parent, popupWidth, custom, null);
}
/**
* @param position позиция элемента в списке
* @param convertView старый view для вторичного использования (при вызове через адаптер)
* @param parent родительский view, к которому будет прикреплён создающийся (при вызове через адаптер)
* @param popupWidth ширина окна, в котором вид будет отображён (если необходимо получить view для всплывающего диалога).
* Для получения вида по умолчанию (в адаптере listview) необходимо передать null.
* @param referer номер поста, из которого открывается диалог (здесь будет выделен цветом {@link ThemeUtils.ThemeColors#refererForeground})
*/
public View getView(int position, View convertView, ViewGroup parent, Integer popupWidth, String referer) {
return getView(position, convertView, parent, popupWidth, null, referer);
}
/**
* @param position позиция элемента в списке
* @param convertView старый view для вторичного использования (при вызове через адаптер)
* @param parent родительский view, к которому будет прикреплён создающийся (при вызове через адаптер)
* @param popupWidth ширина окна, в котором вид будет отображён (если необходимо получить view для всплывающего диалога).
* Для получения вида по умолчанию (в адаптере listview) необходимо передать null.
* @param custom если != null, строится View (только для всплывающего диалога, popupWidth не должно быть равно null)
* не для элемента на позиции position, а для этой модели. При этом комментарий не переносится в ScrollView (т.к. в диалоге будет ListView)
* @param referer номер поста, из которого открывается диалог (здесь будет выделен цветом {@link ThemeUtils.ThemeColors#refererForeground})
*/
public View getView(int position, View convertView, ViewGroup parent, Integer popupWidth, PresentationItemModel custom, String referer) {
final PresentationItemModel model = custom == null ? this.getItem(position) : custom;
//(popupWidth == null) <=> (элемент не для всплывающего диалога, а для ListView)
if (popupWidth == null && model.hidden) {
if (fragment().staticSettings.showHiddenItems) {
View view = convertView == null ? inflater.inflate(R.layout.post_item_hidden, parent, false) : convertView;
view.setTag(Integer.valueOf(position));
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
fragment().onItemClick(null, null, (Integer)v.getTag(), 0);
}
});
((TextView) view).setText(fragment().resources.getString(
fragment().pageType == TYPE_THREADSLIST ? R.string.postitem_hidden_thread : R.string.postitem_hidden_post,
model.sourceModel.number,
model.autohideReason != null ? model.autohideReason : model.spannedComment.toString()));
return view;
} else {
return convertView == null ? inflater.inflate(R.layout.post_item_null, parent, false) : convertView;
}
}
final View view = convertView == null ? inflater.inflate(R.layout.post_item_frame, parent, false) : convertView;
final PostViewTag tag;
if (view.getTag() != null) {
tag = (PostViewTag) view.getTag();
} else {
tag = new PostViewTag();
tag.unreadFrame = view.findViewById(R.id.post_frame_unread);
tag.unreadFrame.setOnClickListener(onUnreadFrameListener);
tag.unreadFrame.setOnLongClickListener(onUnreadFrameListener);
tag.headerView = (JellyBeanSpanFixTextView) view.findViewById(R.id.post_header);
tag.stickyClosedThreadView = (TextView) view.findViewById(R.id.post_sticky_closed_thread);
tag.deletedPostView = view.findViewById(R.id.post_deleted_mark);
tag.dateView = (TextView) view.findViewById(R.id.post_date);
tag.badgeViewContainer = (LinearLayout) view.findViewById(R.id.post_badge_container);
tag.badgeText = (TextView) view.findViewById(R.id.post_badge_title);
tag.multiThumbnailsViewContainer = (LinearLayout) view.findViewById(R.id.post_multi_thumbnails_container);
tag.singleThumbnailView = view.findViewById(R.id.post_thumbnail);
tag.commentView = (ClickableLinksTextView) view.findViewById(R.id.post_comment);
tag.showFullTextView = (TextView) view.findViewById(R.id.post_show_full_text);
tag.repliesView = (JellyBeanSpanFixTextView) view.findViewById(R.id.post_replies);
tag.postsCountView = (TextView) view.findViewById(R.id.post_posts_count);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && fragment().pageType == TYPE_POSTSLIST) {
CompatibilityImpl.setCustomSelectionActionModeMenuCallback(tag.commentView,
R.string.context_menu_reply_with_quote,
ThemeUtils.getActionbarIcon(fragment().activity.getTheme(), fragment().resources, R.attr.actionAddPost),
new CompatibilityImpl.CustomSelectionActionModeCallback() {
@Override
public void onClick() {
try {
int start = tag.commentView.getSelectionStart();
int end = tag.commentView.getSelectionEnd();
String quote = tag.commentView.getText().subSequence(start, end).toString();
fragment().openReply(tag.position, true, quote);
} catch (Exception e) {
Logger.e(TAG, e);
}
}
@Override
public void onCreate() {
try {
if (tag.isPopupDialog || tag.position != getCount() - 1) return;
final int margin = (int) (50 * fragment().resources.getDisplayMetrics().density + 0.5f);
ViewGroup.LayoutParams params = tag.commentView.getLayoutParams();
if (params.height != ViewGroup.LayoutParams.WRAP_CONTENT) return;
params.height = tag.commentView.getHeight() + margin;
tag.commentView.setLayoutParams(params);
fragment().scrollDown();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
final int selectionStart = tag.commentView.getSelectionStart();
final int selectionEnd = tag.commentView.getSelectionEnd();
AppearanceUtils.callWhenLoaded(tag.commentView, new Runnable() {
@Override
public void run() {
try {
ViewGroup.LayoutParams params = tag.commentView.getLayoutParams();
if (params.height != ViewGroup.LayoutParams.WRAP_CONTENT) return;
params.height = tag.commentView.getHeight() + margin;
tag.commentView.setLayoutParams(params);
fragment().scrollDown();
AppearanceUtils.callWhenLoaded(tag.commentView, new Runnable() {
@Override
public void run() {
try {
tag.commentView.startSelection();
Selection.setSelection(
(Spannable) tag.commentView.getText(), selectionStart, selectionEnd);
} catch (Exception e) {
Logger.e(TAG, e);
}
}
});
} catch (Exception e) {
Logger.e(TAG, e);
}
}
});
}
} catch (Exception e) {
Logger.e(TAG, e);
}
}
@Override
public void onDestroy() {
try {
ViewGroup.LayoutParams params = tag.commentView.getLayoutParams();
if (params.height == ViewGroup.LayoutParams.WRAP_CONTENT) return;
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
tag.commentView.setLayoutParams(params);
} catch (Exception e) {
Logger.e(TAG, e);
}
}
});
}
view.setTag(tag);
}
tag.position = position;
// заголовок и дата
tag.headerView.setText(model.spannedHeader);
tag.dateView.setText(model.dateString);
if (fragment().staticSettings.isDisplayDate) {
if (!tag.dateIsVisible) {
tag.dateView.setVisibility(View.VISIBLE);
tag.dateIsVisible = true;
}
} else {
if (tag.dateIsVisible) {
tag.dateView.setVisibility(View.GONE);
tag.dateIsVisible = false;
}
}
// заполнение бэйджа
int badgeIconsCount = Math.min(model.sourceModel.icons == null ? 0 : model.sourceModel.icons.length, PostViewTag.MAX_BADGE_ICONS);
if (badgeIconsCount > 0 || (model.badgeTitle != null && model.badgeTitle.length() != 0)) {
tag.badgeText.setText(model.badgeTitle != null && model.badgeTitle.length() != 0 ? model.badgeTitle : "");
for (int i=tag.badgeIconsVisibleCount; i<badgeIconsCount && i<tag.badgeIconsInflatedCount; ++i)
tag.badgeIcons[i].setVisibility(View.VISIBLE);
for (int i=badgeIconsCount; i<tag.badgeIconsVisibleCount; ++i) tag.badgeIcons[i].setVisibility(View.GONE);
for (int i=tag.badgeIconsInflatedCount; i<badgeIconsCount; ++i) {
tag.badgeIcons[i] = (ImageView)inflater.inflate(R.layout.post_badge_icon, tag.badgeViewContainer, false);
tag.badgeViewContainer.addView(tag.badgeIcons[i], tag.badgeIconsInflatedCount);
++tag.badgeIconsInflatedCount;
}
tag.badgeIconsVisibleCount = badgeIconsCount;
for (int i=0; i<badgeIconsCount; ++i)
fillBadge(tag.badgeIcons[i], model.sourceModel.icons[i].source, model.badgeHashes[i], popupWidth != null);
if (!tag.badgeIsVisible) {
tag.badgeViewContainer.setVisibility(View.VISIBLE);
tag.badgeIsVisible = true;
}
} else {
if (tag.badgeIsVisible) {
tag.badgeViewContainer.setVisibility(View.GONE);
tag.badgeIsVisible = false;
}
}
//заполнение аттачментов
int attachmentsCount = Math.min(model.attachmentHashes.length, PostViewTag.MAX_THUMBNAILS);
if (attachmentsCount == 0) {
if (tag.singleThumbnailIsVisible) {
tag.singleThumbnailView.setVisibility(View.GONE);
tag.singleThumbnailIsVisible = false;
}
if (tag.multiThumbnailsIsVisible) {
tag.multiThumbnailsViewContainer.setVisibility(View.GONE);
tag.multiThumbnailsIsVisible = false;
}
} else if (attachmentsCount == 1) {
if (tag.multiThumbnailsIsVisible) {
tag.multiThumbnailsViewContainer.setVisibility(View.GONE);
tag.multiThumbnailsIsVisible = false;
}
if (!tag.singleThumbnailIsVisible) {
tag.singleThumbnailView.setVisibility(View.VISIBLE);
tag.singleThumbnailIsVisible = true;
}
fillThumbnail(tag.singleThumbnailView, model.sourceModel.attachments[0], model.attachmentHashes[0], popupWidth != null);
} else {
if (tag.singleThumbnailIsVisible) {
tag.singleThumbnailView.setVisibility(View.GONE);
tag.singleThumbnailIsVisible = false;
}
if (!tag.multiThumbnailsIsVisible) {
tag.multiThumbnailsViewContainer.setVisibility(View.VISIBLE);
tag.multiThumbnailsIsVisible = true;
}
int currentThumbnailsInRowCount = popupWidth == null ? thumbnailsInRowCount : Math.max(1, popupWidth / fragment().thumbnailWidth);
int layoutsInflated = divcell(tag.multiThumbnailsInflatedCount, currentThumbnailsInRowCount);
int layoutsVisible = divcell(tag.multiThumbnailsVisibleCount, currentThumbnailsInRowCount);
int layoutsRequired = divcell(attachmentsCount, currentThumbnailsInRowCount);
for (int i=layoutsVisible; i<layoutsRequired && i<layoutsInflated; ++i) tag.multiThumbnailsRows[i].setVisibility(View.VISIBLE);
for (int i=layoutsRequired; i<layoutsVisible; ++i) tag.multiThumbnailsRows[i].setVisibility(View.GONE);
for (int i=layoutsInflated; i<layoutsRequired; ++i) {
tag.multiThumbnailsRows[i] = new LinearLayout(fragment().activity);
tag.multiThumbnailsRows[i].setOrientation(LinearLayout.HORIZONTAL);
tag.multiThumbnailsViewContainer.addView(tag.multiThumbnailsRows[i]);
}
for (int i=tag.multiThumbnailsVisibleCount; i<attachmentsCount && i<tag.multiThumbnailsInflatedCount; ++i)
tag.multiThumbnails[i].setVisibility(View.VISIBLE);
for (int i=attachmentsCount; i<tag.multiThumbnailsVisibleCount; ++i) tag.multiThumbnails[i].setVisibility(View.GONE);
for (int i=tag.multiThumbnailsInflatedCount; i<attachmentsCount; ++i) {
int curLayout = i / currentThumbnailsInRowCount;
tag.multiThumbnails[i] = inflater.inflate(R.layout.post_thumbnail, tag.multiThumbnailsRows[curLayout], false);
((ViewGroup.MarginLayoutParams)tag.multiThumbnails[i].getLayoutParams()).setMargins(0, 0, fragment().thumbnailMargin, 0);
tag.multiThumbnailsRows[curLayout].addView(tag.multiThumbnails[i]);
++tag.multiThumbnailsInflatedCount;
}
tag.multiThumbnailsVisibleCount = attachmentsCount;
for (int i=0; i<attachmentsCount; ++i)
fillThumbnail(tag.multiThumbnails[i], model.sourceModel.attachments[i], model.attachmentHashes[i], popupWidth != null);
}
//комментарий
boolean isFloating;
int refererHighlightColor = ThemeUtils.ThemeColors.getInstance(fragment().activity.getTheme()).refererForeground;
if (popupWidth == null) {
tag.commentView.setText(highlightReferer(referer, refererHighlightColor,
fragment().searchHighlightActive && fragment().cachedSearchHighlightedSpanables != null &&
fragment().cachedSearchHighlightedSpanables.get(position) != null ?
fragment().cachedSearchHighlightedSpanables.get(position) : model.spannedComment));
isFloating = model.floating;
} else {
PresentationItemModel.SpannedCommentContainer customSpanned =
model.getSpannedCommentForCustomWidth(popupWidth - fragment().postItemPadding, fragment().floatingModels);
tag.commentView.setText(highlightReferer(referer, refererHighlightColor, customSpanned.spanned));
isFloating = customSpanned.floating;
}
if (attachmentsCount == 1) {
if (isFloating) {
if (!tag.commentFloatingPosition) {
FlowTextHelper.setFloatLayoutPosition(tag.singleThumbnailView, tag.commentView);
tag.commentFloatingPosition = true;
}
} else {
if (tag.commentFloatingPosition) {
FlowTextHelper.setDefaultLayoutPosition(tag.singleThumbnailView, tag.commentView);
tag.commentFloatingPosition = false;
}
}
}
if ((popupWidth != null || fragment().pageType == TYPE_POSTSLIST) && !tag.clickableLinksSet) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
CompatibilityImpl.setTextIsSelectable(tag.commentView);
} else {
tag.commentView.setMovementMethod(FixedLinkMovementMethod.getInstance());
}
tag.headerView.setMovementMethod(FixedLinkMovementMethod.getInstance());
tag.repliesView.setMovementMethod(FixedLinkMovementMethod.getInstance());
tag.clickableLinksSet = true;
}
if (popupWidth == null && fragment().pageType == TYPE_POSTSLIST) {
//подсветка непрочитанных сообщений
if (position >= fragment().firstUnreadPosition) {
if (!tag.unreadFrameIsVisible) {
tag.unreadFrame.setVisibility(View.VISIBLE);
tag.unreadFrameIsVisible = true;
}
} else {
if (tag.unreadFrameIsVisible) {
tag.unreadFrame.setVisibility(View.GONE);
tag.unreadFrameIsVisible = false;
}
}
}
//отметка удалённого поста
if (model.isDeleted) {
if (!tag.deletedPostViewIsVisible) {
tag.deletedPostView.setVisibility(View.VISIBLE);
tag.deletedPostViewIsVisible = true;
}
} else {
if (tag.deletedPostViewIsVisible) {
tag.deletedPostView.setVisibility(View.GONE);
tag.deletedPostViewIsVisible = false;
}
}
//сократить (кнопка "Показать весь текст") длинные посты
if (tag.showFullTextIsVisible) {
tag.showFullTextView.setVisibility(View.GONE);
tag.showFullTextIsVisible = false;
}
if (popupWidth == null) {
if (fragment().pageType != TYPE_THREADSLIST) {
if (fragment().staticSettings.itemHeight != 0 && !expanded.get(tag.position)) {
tag.commentView.setMaxHeight(fragment().staticSettings.itemHeight);
tag.commentView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (fragment() == null) return false;
if (hackListViewPosition != null) {
fragment().listView.setSelectionFromTop(hackListViewPosition[0], hackListViewPosition[1]);
hackListViewPosition = null;
}
tag.commentView.getViewTreeObserver().removeOnPreDrawListener(this);
if (tag.commentView.getHeight() < fragment().staticSettings.itemHeight) {
return true;
}
tag.showFullTextView.setVisibility(View.VISIBLE);
tag.showFullTextIsVisible = true;
tag.showFullTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
expanded.put(tag.position, true);
tag.commentView.setMaxHeight(Integer.MAX_VALUE);
tag.showFullTextView.setVisibility(View.GONE);
tag.showFullTextIsVisible = false;
}
});
return false;
}
});
} else {
tag.commentView.setMaxHeight(Integer.MAX_VALUE);
}
} else {
tag.commentView.setMaxLines(fragment().maxItemLines);
}
}
//ссылки на ответы
Spanned usingReferencesString = fragment().staticSettings.repliesOnlyQuantity ? model.referencesQuantityString : model.referencesString;
if (usingReferencesString != null && usingReferencesString.length() != 0) {
tag.repliesView.setText(highlightReferer(referer, refererHighlightColor, usingReferencesString));
if (!tag.repliesIsVisible) {
tag.repliesView.setVisibility(View.VISIBLE);
tag.repliesIsVisible = true;
}
} else {
if (tag.repliesIsVisible) {
tag.repliesView.setVisibility(View.GONE);
tag.repliesIsVisible = false;
}
}
//информация о треде (для списка тредов), количество постов, надпись о закрытом/прикрепленном треде
if (popupWidth == null && fragment().pageType == TYPE_THREADSLIST) {
if (model.postsCountString != null) {
tag.postsCountView.setText(model.postsCountString);
if (!tag.postsCountIsVisible) {
tag.postsCountView.setVisibility(View.VISIBLE);
tag.postsCountIsVisible = true;
}
} else {
if (tag.postsCountIsVisible) {
tag.postsCountView.setVisibility(View.GONE);
tag.postsCountIsVisible = false;
}
}
if (model.stickyClosedString != null) {
tag.stickyClosedThreadView.setText(model.stickyClosedString);
if (!tag.stickyClosedThreadIsVisible) {
tag.stickyClosedThreadView.setVisibility(View.VISIBLE);
tag.stickyClosedThreadIsVisible = true;
}
} else {
if (tag.stickyClosedThreadIsVisible) {
tag.stickyClosedThreadView.setVisibility(View.GONE);
tag.stickyClosedThreadIsVisible = false;
}
}
}
//для построения диалогового окна
if (popupWidth != null) {
if (fragment().pageType == TYPE_POSTSLIST) {
weakRegisterForContextMenu(view);
weakRegisterForContextMenu(view.findViewById(R.id.post_content_layout));
tag.isPopupDialog = true;
}
if (custom != null) return view;
final ScrollView scrollContent = (ScrollView) view.findViewById(R.id.post_scroll_content);
RelativeLayout contentLayout = (RelativeLayout) view.findViewById(R.id.post_content_layout);
((ViewGroup) contentLayout.getParent()).removeView(contentLayout);
scrollContent.addView(contentLayout);
scrollContent.setVisibility(View.VISIBLE);
final ScrollView scrollReplies = (ScrollView) view.findViewById(R.id.post_scroll_replies);
((ViewGroup) tag.repliesView.getParent()).removeView(tag.repliesView);
scrollReplies.addView(tag.repliesView);
scrollReplies.setVisibility(View.VISIBLE);
tag.repliesView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
tag.repliesView.getViewTreeObserver().removeOnPreDrawListener(this);
int contentHeight = tag.commentView.getHeight();
if (tag.singleThumbnailIsVisible) contentHeight = Math.max(contentHeight, tag.singleThumbnailView.getHeight());
else if (tag.multiThumbnailsIsVisible) contentHeight += tag.multiThumbnailsViewContainer.getHeight();
if (scrollContent.getHeight() == 0 || contentHeight > scrollContent.getHeight()) {
int maxHeight = (scrollContent.getHeight() + scrollReplies.getHeight()) / 2;
if (maxHeight == 0) {
Logger.e(TAG, "error: can't measure replies view height");
} else if (contentHeight != 0 && contentHeight < maxHeight) {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) scrollContent.getLayoutParams();
params.height = contentHeight;
params.weight = 0;
} else if (tag.repliesView.getHeight() > maxHeight) {
scrollReplies.getLayoutParams().height = maxHeight;
scrollReplies.removeAllViews();
scrollReplies.addView(tag.repliesView);
}
scrollContent.scrollTo(0, 0);
}
return true;
}
});
} else {
tag.isPopupDialog = false;
if (fragment().pageType == TYPE_POSTSLIST) {
weakRegisterForContextMenu(view);
}
}
return view;
}
private Spanned highlightReferer(String referer, int color, Spanned spanned) {
if (referer == null || referer.length() == 0) return spanned;
SpannableStringBuilder builder = null;
ClickableURLSpan[] spans = spanned.getSpans(0, spanned.length(), ClickableURLSpan.class);
for (ClickableURLSpan span : spans) {
int spanStart = spanned.getSpanStart(span);
int spanEnd = spanned.getSpanEnd(span);
if (spanned.subSequence(spanStart, spanEnd).toString().contains(referer)) {
if (builder == null) builder = new SpannableStringBuilder(spanned);
builder.setSpan(new ForegroundColorSpan(color), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return builder == null ? spanned : builder;
}
public void setBusy(boolean isBusy) {
if (isBusy == this.isBusy) return;
this.isBusy = isBusy;
if (!isBusy) setNonBusy();
fragment().activity.setDrawerLock(isBusy ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
}
private void setNonBusy() {
if (!fragment().downloadThumbnails()) return;
int count = fragment().listView.getChildCount();
for (int i=0; i<count; ++i) {
View v = fragment().listView.getChildAt(i);
int position = fragment().listView.getPositionForView(v);
PresentationItemModel model = getItem(position);
if (model.hidden || !(v.getTag() instanceof PostViewTag)) continue;
PostViewTag tag = (PostViewTag) v.getTag();
int attachmentsCount = Math.min(model.attachmentHashes.length, PostViewTag.MAX_THUMBNAILS);
if (attachmentsCount == 1) {
Object picTag = tag.singleThumbnailView.findViewById(R.id.post_thumbnail_image).getTag();
if (picTag == null || picTag == Boolean.FALSE) {
fillThumbnail(tag.singleThumbnailView, model.sourceModel.attachments[0], model.attachmentHashes[0], true);
}
} else {
for (int j=0; j<attachmentsCount; ++j) {
Object picTag = tag.multiThumbnails[j].findViewById(R.id.post_thumbnail_image).getTag();
if (picTag == null || picTag == Boolean.FALSE) {
fillThumbnail(tag.multiThumbnails[j], model.sourceModel.attachments[j], model.attachmentHashes[j], true);
}
}
}
int badgeIconsCount = Math.min(model.sourceModel.icons == null ? 0 : model.sourceModel.icons.length, PostViewTag.MAX_BADGE_ICONS);
for (int j=0; j<badgeIconsCount; ++j) {
if (tag.badgeIcons[j].getTag() == null || tag.badgeIcons[j].getTag() == Boolean.FALSE) {
fillBadge(tag.badgeIcons[j], model.sourceModel.icons[j].source, model.badgeHashes[j], true);
}
}
}
}
private void setImageViewSpoiler(ImageView imageView, boolean isSpoiler) {
int alphaValue = isSpoiler ? 8 : 255;
CompatibilityUtils.setImageAlpha(imageView, alphaValue);
}
private void fillThumbnail(View thumbnailView, AttachmentModel attachment, String hash, boolean nonBusy) {
BoardFragment fragment = fragment();
weakRegisterForContextMenu(thumbnailView);
thumbnailView.setOnClickListener(onAttachmentClickListener);
thumbnailView.setTag(attachment);
ImageView thumbnailPic = (ImageView) thumbnailView.findViewById(R.id.post_thumbnail_image);
TextView size = (TextView) thumbnailView.findViewById(R.id.post_thumbnail_attachment_size);
TextView type = (TextView) thumbnailView.findViewById(R.id.post_thumbnail_attachment_type);
setImageViewSpoiler(thumbnailPic, attachment.isSpoiler || fragment.staticSettings.maskPictures);
switch (attachment.type) {
case AttachmentModel.TYPE_IMAGE_GIF:
type.setText(R.string.postitem_gif);
break;
case AttachmentModel.TYPE_VIDEO:
type.setText(R.string.postitem_video);
break;
case AttachmentModel.TYPE_AUDIO:
type.setText(R.string.postitem_audio);
break;
case AttachmentModel.TYPE_OTHER_FILE:
type.setText(R.string.postitem_file);
break;
case AttachmentModel.TYPE_OTHER_NOTFILE:
type.setText(R.string.postitem_link);
break;
}
if (attachment.type == AttachmentModel.TYPE_IMAGE_STATIC || attachment.type == AttachmentModel.TYPE_IMAGE_SVG) {
type.setVisibility(View.GONE);
} else {
type.setVisibility(View.VISIBLE);
}
if (attachment.type == AttachmentModel.TYPE_OTHER_NOTFILE) {
size.setVisibility(View.GONE);
} else {
size.setText(Attachments.getAttachmentSizeString(attachment, fragment.resources));
size.setVisibility(View.VISIBLE);
}
boolean curBusy = isBusy && !nonBusy;
if (attachment.thumbnail != null && attachment.thumbnail.length() != 0) {
CancellableTask imagesDownloadTask = fragment.imagesDownloadTask;
ExecutorService imagesDownloadExecutor = fragment.imagesDownloadExecutor;
if (fragment.presentationModel == null) {
Fragment currentFragment = MainApplication.getInstance().tabsSwitcher.currentFragment;
if (currentFragment instanceof BoardFragment) {
imagesDownloadTask = ((BoardFragment) currentFragment).imagesDownloadTask;
imagesDownloadExecutor = ((BoardFragment) currentFragment).imagesDownloadExecutor;
}
}
thumbnailPic.setTag(Boolean.FALSE);
fragment.bitmapCache.asyncGet(
hash,
attachment.thumbnail,
fragment.resources.getDimensionPixelSize(R.dimen.post_thumbnail_size),
fragment.chan,
fragment.tabModel.type == TabModel.TYPE_LOCAL ? fragment.localFile : null,
imagesDownloadTask,
thumbnailPic,
imagesDownloadExecutor,
Async.UI_HANDLER,
fragment.downloadThumbnails() && !curBusy,
fragment.downloadThumbnails() ? (curBusy ? 0 : R.drawable.thumbnail_error) :
Attachments.getDefaultThumbnailResId(attachment.type));
} else {
thumbnailPic.setTag(Boolean.TRUE);
thumbnailPic.setImageResource(Attachments.getDefaultThumbnailResId(attachment.type));
}
}
/**
*
* @param badgeIcon
* @param url исходный (возможно, относительный) непофикшеный
* @param hash
* @param nonBusy если true, значение поля isBusy игнорируется, загрузка происходит всегда, если только settings.
*/
private void fillBadge(ImageView badgeIcon, String url, String hash, boolean nonBusy) {
BoardFragment fragment = fragment();
CancellableTask imagesDownloadTask = fragment.imagesDownloadTask;
ExecutorService imagesDownloadExecutor = fragment.imagesDownloadExecutor;
if (fragment.presentationModel == null) {
Fragment currentFragment = MainApplication.getInstance().tabsSwitcher.currentFragment;
if (currentFragment instanceof BoardFragment) {
imagesDownloadTask = ((BoardFragment) currentFragment).imagesDownloadTask;
imagesDownloadExecutor = ((BoardFragment) currentFragment).imagesDownloadExecutor;
}
}
badgeIcon.setTag(Boolean.FALSE);
boolean curBusy = isBusy && !nonBusy;
fragment().bitmapCache.asyncGet(
hash,
url,
fragment.resources.getDimensionPixelSize(R.dimen.post_badge_size),
fragment.chan,
fragment.tabModel.type == TabModel.TYPE_LOCAL ? fragment().localFile : null,
imagesDownloadTask,
badgeIcon,
imagesDownloadExecutor,
Async.UI_HANDLER,
fragment.downloadThumbnails() && !curBusy,
0);
}
}
/**
* Целочисленное деление с округлением в большую сторону
*/
private static int divcell(int a, int b) {
int res = a/b;
if (a%b != 0) ++res;
return res;
}
/**
* Загружать ли миниатюры автоматически
*/
private boolean downloadThumbnails() {
switch (staticSettings.downloadThumbnails) {
case ALWAYS: return true;
case WIFI_ONLY: return Wifi.isConnected();
default: return false;
}
}
/**
* Обновить страницу
*/
public void update() {
update(true, true, false);
}
/**
* Обновить страницу, без вывода всплывающего уведомления
*/
public void updateSilent() {
if (!listLoaded) {
Logger.e(TAG, "called updateSilent() but the list is not loaded");
} else if (updatingNow) {
Logger.d(TAG, "already updating now");
} else {
update(true, true, true);
}
}
/**
* Загрузить или обновить страницу
* @param forceUpdate нужно ли обновлять страницу из интернета, если её версия уже есть в кэше
* @param setRefreshingLayout установить обновление pullableLayout, вызывает {@link SwipeRefreshLayout#setRefreshing(boolean)}
* @param silent не выводить уведомление (Toast) после обновления
*/
private void update(boolean forceUpdate, boolean setRefreshingLayout, boolean silent) {
if (currentTask != null) {
currentTask.cancel();
}
if (listLoaded) {
if (setRefreshingLayout) {
pullableLayout.setRefreshing(true);
}
} else {
switchToLoadingView();
}
PageGetter pageGetter = new PageGetter(forceUpdate, silent);
currentTask = pageGetter;
if (listLoaded) {
Async.runAsync(pageGetter);
} else {
new Thread(pageGetter).start();
}
}
/**
* Остановить обновление pullableLayout (убрать крутящийся круг).
* Костыль для решения следующей проблемы: в случае обновления свайпом,
* если после этого обновление проходит слишком быстро, анимация не останавливается,
* когда вызывается просто setRefreshing(false)
*/
private void setPullableNoRefreshing() {
long time = System.currentTimeMillis() - pullableLayoutSetRefreshingTime;
pullableLayoutSetRefreshingTime = 0;
if (time >= PULLABLE_ANIMATION_DELAY) {
pullableLayout.setRefreshing(false);
} else Async.runOnUiThreadDelayed(new Runnable() {
@Override
public void run() {
pullableLayout.setRefreshing(false);
}
}, PULLABLE_ANIMATION_DELAY - time);
}
/**
* Проскролить спиок до заданного элемента (поста)
* @param number номер поста
*/
public void scrollToItem(String number) {
if (listLoaded) {
for (int i=0; i<presentationModel.presentationList.size(); ++i) {
PresentationItemModel model = presentationModel.presentationList.get(i);
if (model.sourceModel.number.equals(number)) {
listView.setSelection(i);
break;
}
}
}
}
/**
* Проскроллить список вверх на 50 dp
*/
public void scrollUp() {
scroll(true);
}
/**
* Проскроллить список вниз на 50 dp
*/
public void scrollDown() {
scroll(false);
}
private void scroll(boolean up) {
if (listLoaded) {
int step = (int) (50 * resources.getDisplayMetrics().density + 0.5f);
View v = listView.getChildAt(0);
int position = listView.getPositionForView(v);
int top = v.getTop();
listView.setSelectionFromTop(position, top + step * (up ? 1 : -1));
}
}
private void setNavigationCatalogBar() {
if (presentationModel == null) return;
if (tabModel.pageModel.type == UrlPageModel.TYPE_BOARDPAGE) {
View.OnClickListener navigationBarOnClickListener = new NavigationBarOnClickListener(this);
for (int id : new int[] {R.id.board_navigation_previous, R.id.board_navigation_next, R.id.board_navigation_page }) {
navigationBarView.findViewById(id).setOnClickListener(navigationBarOnClickListener);
}
((TextView) navigationBarView.findViewById(R.id.board_navigation_page)).setText(String.valueOf(tabModel.pageModel.boardPage));
if (tabModel.pageModel.boardPage == presentationModel.source.boardModel.firstPage) {
navigationBarView.findViewById(R.id.board_navigation_previous).setVisibility(View.INVISIBLE);
}
if (tabModel.pageModel.boardPage == presentationModel.source.boardModel.lastPage) {
navigationBarView.findViewById(R.id.board_navigation_next).setVisibility(View.INVISIBLE);
}
} else if (tabModel.pageModel.type == UrlPageModel.TYPE_CATALOGPAGE) {
String[] catalogTypes = presentationModel.source.boardModel.catalogTypeDescriptions;
if (catalogTypes == null) catalogTypes = new String[] { resources.getString(R.string.catalog_default) };
catalogBarView.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_spinner_dropdown_item, catalogTypes));
catalogBarView.setSelection(tabModel.pageModel.catalogType);
catalogBarView.setOnItemSelectedListener(new CatalogOnSelectedListener(this));
}
}
private static class NavigationBarOnClickListener implements View.OnClickListener {
private final WeakReference<BoardFragment> fragmentRef;
public NavigationBarOnClickListener(BoardFragment fragment) {
fragmentRef = new WeakReference<BoardFragment>(fragment);
}
@Override
public void onClick(View v) {
final UrlPageModel model = new UrlPageModel();
model.type = UrlPageModel.TYPE_BOARDPAGE;
model.chanName = fragmentRef.get().chan.getChanName();
model.boardName = fragmentRef.get().tabModel.pageModel.boardName;
switch (v.getId()) {
case R.id.board_navigation_previous:
model.boardPage = fragmentRef.get().tabModel.pageModel.boardPage - 1;
UrlHandler.open(model, fragmentRef.get().activity);
break;
case R.id.board_navigation_next:
model.boardPage = fragmentRef.get().tabModel.pageModel.boardPage + 1;
UrlHandler.open(model, fragmentRef.get().activity);
break;
case R.id.board_navigation_page:
final EditText inputField = new EditText(fragmentRef.get().activity);
String pageNumberHint = fragmentRef.get().resources.getString(R.string.dialog_switch_page_hint) +
(fragmentRef.get().presentationModel.source.boardModel.lastPage == BoardModel.LAST_PAGE_UNDEFINED ? "" : " (" +
fragmentRef.get().presentationModel.source.boardModel.firstPage + "-" +
fragmentRef.get().presentationModel.source.boardModel.lastPage + ")");
inputField.setHint(pageNumberHint);
inputField.setInputType(fragmentRef.get().presentationModel.source.boardModel.firstPage >= 0 ?
InputType.TYPE_CLASS_NUMBER : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED));
DialogInterface.OnClickListener dialogOnClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
if (inputField.getText().length() == 0) return;
try {
model.boardPage = Integer.parseInt(inputField.getText().toString());
if (model.boardPage < fragmentRef.get().presentationModel.source.boardModel.firstPage ||
model.boardPage > fragmentRef.get().presentationModel.source.boardModel.lastPage)
throw new NumberFormatException();
if (model.boardPage != fragmentRef.get().tabModel.pageModel.boardPage) {
UrlHandler.open(model, fragmentRef.get().activity);
}
} catch (NumberFormatException e) {
Toast.makeText(fragmentRef.get().activity, R.string.dialog_switch_page_incorrect, Toast.LENGTH_LONG).show();
}
}
}
};
new AlertDialog.Builder(fragmentRef.get().activity).
setTitle(R.string.dialog_switch_page_title).
setView(inputField).
setPositiveButton(R.string.dialog_switch_page_go, dialogOnClickListener).
setNegativeButton(android.R.string.cancel, dialogOnClickListener).
show();
break;
}
}
}
private static class CatalogOnSelectedListener implements AdapterView.OnItemSelectedListener {
private final WeakReference<BoardFragment> fragmentRef;
public CatalogOnSelectedListener(BoardFragment fragment) {
fragmentRef = new WeakReference<BoardFragment>(fragment);
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (fragmentRef.get().tabModel.pageModel.catalogType == position) return;
UrlPageModel model = new UrlPageModel();
model.type = UrlPageModel.TYPE_CATALOGPAGE;
model.chanName = fragmentRef.get().chan.getChanName();
model.boardName = fragmentRef.get().tabModel.pageModel.boardName;
model.catalogType = position;
UrlHandler.open(model, fragmentRef.get().activity);
}
@Override
public void onNothingSelected(AdapterView<?> parent) { }
}
private void initSearchBar() {
if (searchBarInitialized) return;
final EditText field = (EditText) searchBarView.findViewById(R.id.board_search_field);
final TextView results = (TextView) searchBarView.findViewById(R.id.board_search_result);
if (pageType == TYPE_POSTSLIST) {
field.setHint(R.string.search_bar_in_thread_hint);
}
final View.OnClickListener searchOnClickListener = new View.OnClickListener() {
private int lastFound = -1;
@Override
public void onClick(View v) {
if (v != null && v.getId() == R.id.board_search_close) {
searchHighlightActive = false;
adapter.notifyDataSetChanged();
searchBarView.setVisibility(View.GONE);
} else if (listView != null && listView.getChildCount() > 0 && adapter != null && cachedSearchResults != null) {
boolean atEnd = listView.getChildAt(listView.getChildCount() - 1).getTop() +
listView.getChildAt(listView.getChildCount() - 1).getHeight() == listView.getHeight();
View topView = listView.getChildAt(0);
if ((v == null || v.getId() == R.id.board_search_previous) &&
topView.getTop() < 0 && listView.getChildCount() > 1) topView = listView.getChildAt(1);
int currentListPosition = listView.getPositionForView(topView);
int newResultIndex = Collections.binarySearch(cachedSearchResults, currentListPosition);
if (newResultIndex >= 0) {
if (v != null) {
if (v.getId() == R.id.board_search_next) ++newResultIndex;
else if (v.getId() == R.id.board_search_previous) --newResultIndex;
}
} else {
newResultIndex = -newResultIndex - 1;
if (v != null && v.getId() == R.id.board_search_previous) --newResultIndex;
}
while (newResultIndex < 0) newResultIndex += cachedSearchResults.size();
newResultIndex %= cachedSearchResults.size();
if (v != null && v.getId() == R.id.board_search_next && lastFound == newResultIndex && atEnd) newResultIndex = 0;
lastFound = newResultIndex;
listView.setSelection(cachedSearchResults.get(newResultIndex));
results.setText((newResultIndex + 1) + "/" + cachedSearchResults.size());
}
}
};
for (int id : new int[] { R.id.board_search_close, R.id.board_search_previous, R.id.board_search_next }) {
searchBarView.findViewById(id).setOnClickListener(searchOnClickListener);
}
field.setOnKeyListener(new View.OnKeyListener() {
private boolean searchUsingChan() {
if (pageType != TYPE_THREADSLIST) return false;
if (presentationModel != null)
if (presentationModel.source != null)
if (presentationModel.source.boardModel != null)
if (!presentationModel.source.boardModel.searchAllowed) return false;
return true;
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
if (searchUsingChan()) {
UrlPageModel model = new UrlPageModel();
model.chanName = chan.getChanName();
model.type = UrlPageModel.TYPE_SEARCHPAGE;
model.boardName = tabModel.pageModel.boardName;
model.searchRequest = field.getText().toString();
UrlHandler.open(model, activity);
} else {
int highlightColor = ThemeUtils.getThemeColor(activity.getTheme(), R.attr.searchHighlightBackground, Color.RED);
String request = field.getText().toString().toLowerCase(Locale.US);
if (cachedSearchRequest == null || !request.equals(cachedSearchRequest)) {
cachedSearchRequest = request;
cachedSearchResults = new ArrayList<Integer>();
cachedSearchHighlightedSpanables = new SparseArray<Spanned>();
List<PresentationItemModel> safePresentationList = presentationModel.getSafePresentationList();
if (safePresentationList != null) {
for (int i=0; i<safePresentationList.size(); ++i) {
PresentationItemModel model = safePresentationList.get(i);
if (model.hidden && !staticSettings.showHiddenItems) continue;
String comment = model.spannedComment.toString().toLowerCase(Locale.US).replace('\n', ' ');
List<Integer> altFoundPositions = null;
if (model.floating) {
int floatingpos = FlowTextHelper.getFloatingPosition(model.spannedComment);
if (floatingpos != -1 && floatingpos < model.spannedComment.length() &&
model.spannedComment.charAt(floatingpos) == '\n') {
String altcomment = comment.substring(0, floatingpos) + comment.substring(
floatingpos + 1, Math.min(model.spannedComment.length(), floatingpos + request.length()));
int start = 0;
int curpos;
while (start < altcomment.length() && (curpos = altcomment.indexOf(request, start)) != -1) {
if (altFoundPositions == null) altFoundPositions = new ArrayList<Integer>();
altFoundPositions.add(curpos);
start = curpos + request.length();
}
}
}
if (comment.contains(request) || altFoundPositions != null) {
cachedSearchResults.add(Integer.valueOf(i));
SpannableStringBuilder spannedHighlited =
new SpannableStringBuilder(safePresentationList.get(i).spannedComment);
int start = 0;
int curpos;
while (start < comment.length() && (curpos = comment.indexOf(request, start)) != -1) {
start = curpos + request.length();
if (altFoundPositions != null && Collections.binarySearch(altFoundPositions, curpos) >= 0) continue;
spannedHighlited.setSpan(new BackgroundColorSpan(highlightColor),
curpos, curpos + request.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (altFoundPositions != null) {
for (Integer pos : altFoundPositions) {
spannedHighlited.setSpan(new BackgroundColorSpan(highlightColor),
pos, pos + request.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
cachedSearchHighlightedSpanables.put(i, spannedHighlited);
}
}
}
}
if (cachedSearchResults.size() == 0) {
Toast.makeText(activity, R.string.notification_not_found, Toast.LENGTH_LONG).show();
} else {
boolean firstTime = !searchHighlightActive;
searchHighlightActive = true;
adapter.notifyDataSetChanged();
searchBarView.findViewById(R.id.board_search_next).setVisibility(View.VISIBLE);
searchBarView.findViewById(R.id.board_search_previous).setVisibility(View.VISIBLE);
searchBarView.findViewById(R.id.board_search_result).setVisibility(View.VISIBLE);
searchOnClickListener.onClick(firstTime ? null : searchBarView.findViewById(R.id.board_search_next));
}
}
try {
InputMethodManager imm = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(field.getWindowToken(), 0);
} catch (Exception e) {
Logger.e(TAG, e);
}
return true;
}
return false;
}
});
field.addTextChangedListener(new OnSearchTextChangedListener(this));
field.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
if (resources.getDimensionPixelSize(R.dimen.panel_height) < field.getMeasuredHeight())
searchBarView.getLayoutParams().height = field.getMeasuredHeight();
searchBarInitialized = true;
}
private static class OnSearchTextChangedListener implements TextWatcher {
private final WeakReference<BoardFragment> fragmentRef;
public OnSearchTextChangedListener(BoardFragment fragment) {
this.fragmentRef = new WeakReference<BoardFragment>(fragment);
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (fragmentRef.get().searchHighlightActive) {
fragmentRef.get().searchHighlightActive = false;
fragmentRef.get().adapter.notifyDataSetChanged();
}
fragmentRef.get().searchBarView.findViewById(R.id.board_search_next).setVisibility(View.GONE);
fragmentRef.get().searchBarView.findViewById(R.id.board_search_previous).setVisibility(View.GONE);
fragmentRef.get().searchBarView.findViewById(R.id.board_search_result).setVisibility(View.GONE);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void afterTextChanged(Editable s) {}
}
private void resetSearchCache() {
searchBarView.findViewById(R.id.board_search_next).setVisibility(View.GONE);
searchBarView.findViewById(R.id.board_search_previous).setVisibility(View.GONE);
searchBarView.findViewById(R.id.board_search_result).setVisibility(View.GONE);
searchHighlightActive = false;
cachedSearchHighlightedSpanables = null;
cachedSearchRequest = null;
cachedSearchResults = null;
}
private void finalizeSearchBar() {
if (!searchBarInitialized) return;
for (int id : new int[] { R.id.board_search_close, R.id.board_search_previous, R.id.board_search_next }) {
searchBarView.findViewById(id).setOnClickListener(null);
}
final EditText field = (EditText) searchBarView.findViewById(R.id.board_search_field);
field.setOnKeyListener(null);
}
private void openPostForm(String hash, BoardModel boardModel, SendPostModel sendPostModel) {
if (PostingService.isNowPosting()) {
Toast.makeText(activity, resources.getString(R.string.posting_now_posting), Toast.LENGTH_LONG).show();
return;
}
Intent addPostIntent = new Intent(activity.getApplicationContext(), PostFormActivity.class);
addPostIntent.putExtra(PostingService.EXTRA_PAGE_HASH, hash);
addPostIntent.putExtra(PostingService.EXTRA_BOARD_MODEL, boardModel);
addPostIntent.putExtra(PostingService.EXTRA_SEND_POST_MODEL, sendPostModel);
startActivity(addPostIntent);
}
private void openReply(int position, boolean withQuote, String quote) {
PresentationItemModel item = adapter.getItem(position);
SendPostModel sendReplyModel = getSendPostModel();
int sendReplyModelPos = sendReplyModel.commentPosition;
if (sendReplyModelPos > sendReplyModel.comment.length()) sendReplyModelPos = -1;
if (sendReplyModelPos < 0) sendReplyModelPos = sendReplyModel.comment.length();
String insertion;
if (withQuote) {
String quotedComment = (quote != null ? quote : item.spannedComment.toString().replaceAll("(^|\n)(>>\\d+(\n|\\s)?)+", "$1")).
replaceAll("(\n+)", "$1>");
insertion = ">>" + item.sourceModel.number + "\n" + (quotedComment.length() > 0 ? ">" + quotedComment + "\n" : "");
} else {
insertion = ">>" + item.sourceModel.number + "\n";
}
sendReplyModel.comment = sendReplyModel.comment.substring(0, sendReplyModelPos) +
insertion + sendReplyModel.comment.substring(sendReplyModelPos);
sendReplyModel.commentPosition = sendReplyModelPos + insertion.length();
openPostForm(tabModel.hash, presentationModel.source.boardModel, sendReplyModel);
}
private SendPostModel getSendPostModel() {
SendPostModel draft = MainApplication.getInstance().draftsCache.get(tabModel.hash);
if (draft == null) {
draft = new SendPostModel();
draft.chanName = tabModel.pageModel.chanName;
draft.boardName = tabModel.pageModel.boardName;
draft.threadNumber = pageType == TYPE_POSTSLIST ? tabModel.pageModel.threadNumber : null;
draft.comment = "";
BoardModel boardModel = presentationModel.source.boardModel;
if (boardModel.allowNames) draft.name = settings.getDefaultName();
if (boardModel.allowEmails) draft.email = settings.getDefaultEmail();
if (boardModel.allowDeletePosts || boardModel.allowDeleteFiles) draft.password = chan.getDefaultPassword();
if (boardModel.allowRandomHash) draft.randomHash = settings.isRandomHash();
}
return draft;
}
private SendPostModel getSendPostModel(UrlPageModel pageModel) {
String hash = ChanModels.hashUrlPageModel(pageModel);
SendPostModel draft = MainApplication.getInstance().draftsCache.get(hash);
if (draft == null) {
draft = new SendPostModel();
draft.chanName = pageModel.chanName;
draft.boardName = pageModel.boardName;
draft.threadNumber = pageModel.threadNumber;
BoardModel boardModel = presentationModel.source.boardModel;
if (boardModel.allowNames) draft.name = settings.getDefaultName();
if (boardModel.allowEmails) draft.email = settings.getDefaultEmail();
if (boardModel.allowDeletePosts || boardModel.allowDeleteFiles) draft.password = chan.getDefaultPassword();
if (boardModel.allowRandomHash) draft.randomHash = settings.isRandomHash();
}
return draft;
}
private Point getSpanCoordinates(View widget, ClickableURLSpan span) {
TextView parentTextView = (TextView) widget;
Rect parentTextViewRect = new Rect();
// Initialize values for the computing of clickedText position
SpannableString completeText = (SpannableString)(parentTextView).getText();
Layout textViewLayout = parentTextView.getLayout();
int startOffsetOfClickedText = completeText.getSpanStart(span);
int endOffsetOfClickedText = completeText.getSpanEnd(span);
double startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(startOffsetOfClickedText);
double endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(endOffsetOfClickedText);
// Get the rectangle of the clicked text
int currentLineStartOffset = textViewLayout.getLineForOffset(startOffsetOfClickedText);
int currentLineEndOffset = textViewLayout.getLineForOffset(endOffsetOfClickedText);
boolean keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset;
textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect);
// Update the rectangle position to his real position on screen
int[] parentTextViewLocation = {0,0};
parentTextView.getLocationOnScreen(parentTextViewLocation);
double parentTextViewTopAndBottomOffset = (
parentTextViewLocation[1] -
parentTextView.getScrollY() +
parentTextView.getCompoundPaddingTop()
);
Rect windowRect = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(windowRect);
parentTextViewTopAndBottomOffset -= windowRect.top;
parentTextViewRect.top += parentTextViewTopAndBottomOffset;
parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;
parentTextViewRect.left += (
parentTextViewLocation[0] +
startXCoordinatesOfClickedText +
parentTextView.getCompoundPaddingLeft() -
parentTextView.getScrollX()
);
parentTextViewRect.right = (int) (
parentTextViewRect.left +
endXCoordinatesOfClickedText -
startXCoordinatesOfClickedText
);
int x = (parentTextViewRect.left + parentTextViewRect.right) / 2;
int y = (parentTextViewRect.top + parentTextViewRect.bottom) / 2;
if (keywordIsInMultiLine) {
x = parentTextViewRect.left;
}
return new Point(x, y);
}
/**
* Открыть всплывающий диалог с постом
* @param itemPosition позиция элемента (поста) в адаптере listView
* @param isTablet true, если планшетный режим (задается положение окна относительно ссылки)
* @param coordinates координаты нажатой ссылки
*/
private void showPostPopupDialog(final int itemPosition, final boolean isTablet, final Point coordinates, final String refererPost) {
final int bgShadowResource = ThemeUtils.getThemeResId(activity.getTheme(), R.attr.dialogBackgroundShadow);
final int bgColor = ThemeUtils.getThemeColor(activity.getTheme(), R.attr.activityRootBackground, Color.BLACK);
final int measuredWidth = isTablet ? adapter.measureViewWidth(itemPosition) : -1; //измерять требуется только для планшета
final View tmpV = new View(activity);
final Dialog tmpDlg = new Dialog(activity);
tmpDlg.getWindow().setBackgroundDrawableResource(bgShadowResource);
tmpDlg.requestWindowFeature(Window.FEATURE_NO_TITLE);
tmpDlg.setCanceledOnTouchOutside(true);
tmpDlg.setContentView(tmpV);
final Rect activityWindowRect;
final int dlgWindowWidth;
final int dlgWindowHeight;
if (isTablet) {
activityWindowRect = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(activityWindowRect);
dlgWindowWidth = Math.max(coordinates.x, activityWindowRect.width() - coordinates.x);
dlgWindowHeight = Math.max(coordinates.y, activityWindowRect.height() - coordinates.y);
tmpDlg.getWindow().setLayout(dlgWindowWidth, dlgWindowHeight);
} else {
activityWindowRect = null;
dlgWindowWidth = -1;
dlgWindowHeight = -1;
}
tmpDlg.show();
Runnable next = new Runnable() {
@SuppressLint("RtlHardcoded")
@Override
public void run() {
int dlgWidth = tmpV.getWidth();
int dlgHeight = tmpV.getHeight();
tmpDlg.hide();
tmpDlg.cancel();
int newWidth = isTablet ? Math.min(measuredWidth, dlgWidth) : dlgWidth;
View view = adapter.getView(itemPosition, null, null, newWidth, refererPost);
view.setBackgroundColor(bgColor);
//Logger.d(TAG, "measured: "+view.findViewById(R.id.post_frame_main).getMeasuredWidth()+
// "x"+view.findViewById(R.id.post_frame_main).getMeasuredHeight());
Dialog dialog = new Dialog(activity);
dialog.getWindow().setBackgroundDrawableResource(bgShadowResource);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setCanceledOnTouchOutside(true);
dialog.setContentView(view);
if (isTablet) {
view.findViewById(R.id.post_frame_main).measure(
MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int newWindowWidth = dlgWindowWidth - dlgWidth + newWidth;
int newWindowHeight = dlgWindowHeight - dlgHeight +
Math.min(view.findViewById(R.id.post_frame_main).getMeasuredHeight(), dlgHeight);
dialog.getWindow().setLayout(newWindowWidth, newWindowHeight);
WindowManager.LayoutParams params = dialog.getWindow().getAttributes();
if (coordinates.x > activityWindowRect.width() - coordinates.x &&
coordinates.x + newWindowWidth > activityWindowRect.width()) {
params.x = activityWindowRect.width() - coordinates.x;
params.gravity = Gravity.RIGHT;
} else {
params.x = coordinates.x;
params.gravity = Gravity.LEFT;
}
if (coordinates.y > activityWindowRect.height() - coordinates.y &&
coordinates.y + newWindowHeight > activityWindowRect.height()) {
params.y = activityWindowRect.height() - coordinates.y;
params.gravity |= Gravity.BOTTOM;
} else {
params.y = coordinates.y;
params.gravity |= Gravity.TOP;
}
dialog.getWindow().setAttributes(params);
//затемнение в планшетном режиме не нужно
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
CompatibilityImpl.setDimAmount(dialog.getWindow(), 0.1f);
} else {
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
}
}
dialog.show();
dialogs.add(dialog);
}
};
if (tmpV.getWidth() != 0) {
next.run();
} else {
AppearanceUtils.callWhenLoaded(tmpDlg.getWindow().getDecorView(), next);
}
}
private void showThreadPreviewDialog(final int position) {
final List<PresentationItemModel> items = new ArrayList<>();
final int bgShadowResource = ThemeUtils.getThemeResId(activity.getTheme(), R.attr.dialogBackgroundShadow);
final int bgColor = ThemeUtils.getThemeColor(activity.getTheme(), R.attr.activityRootBackground, Color.BLACK);
final View tmpV = new View(activity);
final Dialog tmpDlg = new Dialog(activity);
tmpDlg.getWindow().setBackgroundDrawableResource(bgShadowResource);
tmpDlg.requestWindowFeature(Window.FEATURE_NO_TITLE);
tmpDlg.setCanceledOnTouchOutside(true);
tmpDlg.setContentView(tmpV);
tmpDlg.show();
Runnable next = new Runnable() {
@Override
public void run() {
final int dlgWidth = tmpV.getWidth();
tmpDlg.hide();
tmpDlg.cancel();
final Dialog dialog = new Dialog(activity);
if (presentationModel.source != null &&
presentationModel.source.threads != null &&
presentationModel.source.threads.length > position &&
presentationModel.source.threads[position].posts != null &&
presentationModel.source.threads[position].posts.length > 0) {
final String threadNumber = presentationModel.source.threads[position].posts[0].number;
ClickableURLSpan.URLSpanClickListener spanClickListener = new ClickableURLSpan.URLSpanClickListener() {
@Override
public void onClick(View v, ClickableURLSpan span, String url, String referer) {
if (url.startsWith("#")) {
try {
UrlPageModel threadPageModel = new UrlPageModel();
threadPageModel.chanName = chan.getChanName();
threadPageModel.type = UrlPageModel.TYPE_THREADPAGE;
threadPageModel.boardName = tabModel.pageModel.boardName;
threadPageModel.threadNumber = threadNumber;
url = chan.buildUrl(threadPageModel) + url;
dialog.dismiss();
UrlHandler.open(chan.fixRelativeUrl(url), activity);
} catch (Exception e) {
Logger.e(TAG, e);
}
} else {
dialog.dismiss();
UrlHandler.open(chan.fixRelativeUrl(url), activity);
}
}
};
AndroidDateFormat.initPattern();
String datePattern = AndroidDateFormat.getPattern();
DateFormat dateFormat = datePattern == null ?
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) : new SimpleDateFormat(datePattern, Locale.US);
dateFormat.setTimeZone(settings.isLocalTime() ?
TimeZone.getDefault() : TimeZone.getTimeZone(presentationModel.source.boardModel.timeZoneId));
int postsCount = presentationModel.source.threads[position].postsCount;
boolean showIndex = presentationModel.source.threads[position].posts.length <= postsCount;
int curPostIndex = postsCount - presentationModel.source.threads[position].posts.length + 1;
boolean openSpoilers = settings.openSpoilers();
for (int i=0; i<presentationModel.source.threads[position].posts.length; ++i) {
PresentationItemModel model = new PresentationItemModel(
presentationModel.source.threads[position].posts[i],
chan.getChanName(),
presentationModel.source.pageModel.boardName,
presentationModel.source.pageModel.threadNumber,
dateFormat,
spanClickListener,
imageGetter,
ThemeUtils.ThemeColors.getInstance(activity.getTheme()),
openSpoilers,
floatingModels,
null);
model.buildSpannedHeader(showIndex ? (i == 0 ? 1 : ++curPostIndex) : -1,
presentationModel.source.boardModel.bumpLimit, presentationModel.source.boardModel.defaultUserName, null, false);
items.add(model);
}
} else {
items.add(presentationModel.presentationList.get(position));
}
ListView dlgList = new ListView(activity);
dlgList.setAdapter(new ArrayAdapter<PresentationItemModel>(activity, 0, items) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = adapter.getView(position, convertView, parent, dlgWidth, getItem(position));
view.setBackgroundColor(bgColor);
return view;
}
});
dialog.getWindow().setBackgroundDrawableResource(bgShadowResource);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setCanceledOnTouchOutside(true);
dialog.setContentView(dlgList);
dialog.show();
dialogs.add(dialog);
}
};
if (tmpV.getWidth() != 0) {
next.run();
} else {
AppearanceUtils.callWhenLoaded(tmpDlg.getWindow().getDecorView(), next);
}
}
private void openReferencesList(final String from) {
final List<Integer> positions = new ArrayList<>();
int position = -1;
for (int i=0; i<presentationModel.presentationList.size(); ++i) {
if (presentationModel.presentationList.get(i).sourceModel.number.equals(from)) {
position = i;
break;
}
}
if (position != -1) {
Spanned referencesString = presentationModel.presentationList.get(position).referencesString;
if (referencesString == null) {
Logger.e(TAG, "null referencesString");
return;
}
ClickableURLSpan[] spans = referencesString.getSpans(0, referencesString.length(), ClickableURLSpan.class);
for (ClickableURLSpan span : spans) {
String url = span.getURL();
try {
//url уже в нормальном виде, т.к. строится в PresentationItemModel (модулем чана)
UrlPageModel model = UrlHandler.getPageModel(url);
for (; position<presentationModel.presentationList.size(); ++position) {
if (presentationModel.presentationList.get(position).sourceModel.number.equals(model.postNumber)) {
break;
}
}
if (position<presentationModel.presentationList.size()) positions.add(position);
} catch (Exception e) {
Logger.e(TAG, e);
}
}
}
if (positions.size() == 0) {
Logger.e(TAG, "no references");
return;
}
final int bgShadowResource = ThemeUtils.getThemeResId(activity.getTheme(), R.attr.dialogBackgroundShadow);
final int bgColor = ThemeUtils.getThemeColor(activity.getTheme(), R.attr.activityRootBackground, Color.BLACK);
final View tmpV = new View(activity);
final Dialog tmpDlg = new Dialog(activity);
tmpDlg.getWindow().setBackgroundDrawableResource(bgShadowResource);
tmpDlg.requestWindowFeature(Window.FEATURE_NO_TITLE);
tmpDlg.setCanceledOnTouchOutside(true);
tmpDlg.setContentView(tmpV);
tmpDlg.show();
Runnable next = new Runnable() {
@Override
public void run() {
final int dlgWidth = tmpV.getWidth();
tmpDlg.hide();
tmpDlg.cancel();
ListView dlgList = new ListView(activity);
dlgList.setAdapter(new ArrayAdapter<Integer>(activity, 0, positions) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
try {
int adapterPositon = getItem(position);
View view = adapter.getView(adapterPositon, convertView, parent, dlgWidth, adapter.getItem(adapterPositon), from);
view.setBackgroundColor(bgColor);
return view;
} catch (Exception e) {
Logger.e(TAG, e);
Toast.makeText(activity, R.string.error_unknown, Toast.LENGTH_LONG).show();
return new View(activity);
}
}
});
Dialog dialog = new Dialog(activity);
dialog.getWindow().setBackgroundDrawableResource(bgShadowResource);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setCanceledOnTouchOutside(true);
dialog.setContentView(dlgList);
dialog.show();
dialogs.add(dialog);
}
};
if (tmpV.getWidth() != 0) {
next.run();
} else {
AppearanceUtils.callWhenLoaded(tmpDlg.getWindow().getDecorView(), next);
}
}
private void downloadFile(AttachmentModel attachment) {
downloadFile(attachment, false);
}
public static String getCustomSubdir(UrlPageModel pageModel) {
if (pageModel == null || pageModel.boardName == null || pageModel.threadNumber == null || pageModel.type != UrlPageModel.TYPE_THREADPAGE)
return null;
return pageModel.boardName + "-" + pageModel.threadNumber + "_originals";
}
private boolean downloadFile(AttachmentModel attachment, boolean fromGridGallery) {
if (!CompatibilityUtils.hasAccessStorage(activity)) return true;
if (attachment.type == AttachmentModel.TYPE_OTHER_NOTFILE) return true;
String subdir = (fromGridGallery && tabModel.pageModel.type == UrlPageModel.TYPE_THREADPAGE) ? getCustomSubdir(tabModel.pageModel) : null;
DownloadingService.DownloadingQueueItem item = (subdir != null) ?
new DownloadingService.DownloadingQueueItem(attachment, subdir, presentationModel.source.boardModel) :
new DownloadingService.DownloadingQueueItem(attachment, presentationModel.source.boardModel);
String fileName = Attachments.getAttachmentLocalFileName(attachment, presentationModel.source.boardModel);
String itemName = Attachments.getAttachmentLocalShortName(attachment, presentationModel.source.boardModel);
if (DownloadingService.isInQueue(item)) {
if (!fromGridGallery)
Toast.makeText(activity, resources.getString(R.string.notification_download_already_in_queue, itemName), Toast.LENGTH_LONG).show();
return false;
} else {
File dir = new File(settings.getDownloadDirectory(), tabModel.pageModel.chanName);
if (subdir != null) dir = new File(dir, subdir);
if (new File(dir, fileName).exists()) {
if (!fromGridGallery)
Toast.makeText(activity, resources.getString(R.string.notification_download_already_exists, fileName), Toast.LENGTH_LONG).show();
return false;
} else {
Intent downloadIntent = new Intent(activity, DownloadingService.class);
downloadIntent.putExtra(DownloadingService.EXTRA_DOWNLOADING_ITEM, item);
activity.startService(downloadIntent);
return true;
}
}
}
@SuppressLint("InflateParams")
private void saveThisPage() {
if (!CompatibilityUtils.hasAccessStorage(activity)) return;
DownloadingService.DownloadingQueueItem check = new DownloadingService.DownloadingQueueItem(
tabModel.pageModel, presentationModel.source.boardModel, DownloadingService.MODE_DOWNLOAD_ALL);
String itemName = resources.getString(R.string.downloading_thread_format, tabModel.pageModel.boardName, tabModel.pageModel.threadNumber);
if (DownloadingService.isInQueue(check)) {
Toast.makeText(activity, resources.getString(R.string.notification_download_already_in_queue, itemName), Toast.LENGTH_LONG).show();
} else {
Context dialogContext = Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ?
new ContextThemeWrapper(activity, R.style.Theme_Neutron) : activity;
View saveThreadDialogView = LayoutInflater.from(dialogContext).inflate(R.layout.dialog_save_thread, null);
final CheckBox saveThumbsChkbox = (CheckBox) saveThreadDialogView.findViewById(R.id.dialog_save_thread_thumbs);
final CheckBox saveAllChkbox = (CheckBox) saveThreadDialogView.findViewById(R.id.dialog_save_thread_all);
switch (settings.getDownloadThreadMode()) {
case DownloadingService.MODE_DOWNLOAD_ALL:
saveThumbsChkbox.setChecked(true);
saveAllChkbox.setChecked(true);
break;
case DownloadingService.MODE_DOWNLOAD_THUMBS:
saveThumbsChkbox.setChecked(true);
saveAllChkbox.setChecked(false);
break;
default:
saveThumbsChkbox.setChecked(false);
saveAllChkbox.setChecked(false);
saveAllChkbox.setEnabled(false);
break;
}
saveThumbsChkbox.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (saveThumbsChkbox.isChecked()) {
saveAllChkbox.setEnabled(true);
} else {
saveAllChkbox.setEnabled(false);
saveAllChkbox.setChecked(false);
}
}
});
DialogInterface.OnClickListener save = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int mode = DownloadingService.MODE_ONLY_CACHE;
if (saveThumbsChkbox.isChecked()) {
mode = DownloadingService.MODE_DOWNLOAD_THUMBS;
}
if (saveAllChkbox.isChecked()) {
mode = DownloadingService.MODE_DOWNLOAD_ALL;
}
settings.saveDownloadThreadMode(mode);
Intent savePageIntent = new Intent(activity, DownloadingService.class);
savePageIntent.putExtra(DownloadingService.EXTRA_DOWNLOADING_ITEM,
new DownloadingService.DownloadingQueueItem(tabModel.pageModel, presentationModel.source.boardModel, mode));
activity.startService(savePageIntent);
}
};
AlertDialog saveThreadDialog = new AlertDialog.Builder(dialogContext).setView(saveThreadDialogView).
setTitle(R.string.dialog_save_thread_title).
setPositiveButton(R.string.dialog_save_thread_save, save).
setNegativeButton(android.R.string.cancel, null).create();
saveThreadDialog.setCanceledOnTouchOutside(false);
saveThreadDialog.show();
}
}
@SuppressLint("InlinedApi")
private void openGridGallery() {
final int tnSize = resources.getDimensionPixelSize(R.dimen.post_thumbnail_size);
class GridGalleryAdapter extends ArrayAdapter<Triple<AttachmentModel, String, String>>
implements View.OnClickListener, AbsListView.OnScrollListener {
private final GridView view;
private boolean selectingMode = false;
private boolean[] isSelected = null;
private volatile boolean isBusy = false;
public GridGalleryAdapter(GridView view, List<Triple<AttachmentModel, String, String>> list) {
super(activity, 0, list);
this.view = view;
this.isSelected = new boolean[list.size()];
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
if (isBusy) setNonBusy();
isBusy = false;
} else isBusy = true;
}
private void setNonBusy() {
if (!downloadThumbnails()) return;
for (int i=0; i<view.getChildCount(); ++i) {
View v = view.getChildAt(i);
Object tnTag = v.findViewById(R.id.post_thumbnail_image).getTag();
if (tnTag == null || tnTag == Boolean.FALSE) fill(view.getPositionForView(v), v, false);
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = new FrameLayout(activity);
convertView.setLayoutParams(new AbsListView.LayoutParams(tnSize, tnSize));
ImageView tnImage = new ImageView(activity);
tnImage.setLayoutParams(new FrameLayout.LayoutParams(tnSize, tnSize, Gravity.CENTER));
tnImage.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
tnImage.setId(R.id.post_thumbnail_image);
((FrameLayout) convertView).addView(tnImage);
}
convertView.setTag(getItem(position).getLeft());
safeRegisterForContextMenu(convertView);
convertView.setOnClickListener(this);
fill(position, convertView, isBusy);
if (isSelected[position]) {
/*ImageView overlay = new ImageView(activity);
overlay.setImageResource(android.R.drawable.checkbox_on_background);*/
FrameLayout overlay = new FrameLayout(activity);
overlay.setBackgroundColor(Color.argb(128, 0, 255, 0));
if (((FrameLayout) convertView).getChildCount() < 2) ((FrameLayout) convertView).addView(overlay);
} else {
if (((FrameLayout) convertView).getChildCount() > 1) ((FrameLayout) convertView).removeViewAt(1);
}
return convertView;
}
private void safeRegisterForContextMenu(View view) {
try {
view.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
if (presentationModel == null) {
Fragment currentFragment = MainApplication.getInstance().tabsSwitcher.currentFragment;
if (currentFragment instanceof BoardFragment) {
currentFragment.onCreateContextMenu(menu, v, menuInfo);
}
} else {
BoardFragment.this.onCreateContextMenu(menu, v, menuInfo);
}
}
});
} catch (Exception e) {
Logger.e(TAG, e);
}
}
@Override
public void onClick(View v) {
if (selectingMode) {
int position = view.getPositionForView(v);
isSelected[position] = !isSelected[position];
notifyDataSetChanged();
} else {
BoardFragment fragment = BoardFragment.this;
if (presentationModel == null) {
Fragment currentFragment = MainApplication.getInstance().tabsSwitcher.currentFragment;
if (currentFragment instanceof BoardFragment) fragment = (BoardFragment) currentFragment;
}
fragment.openAttachment((AttachmentModel) v.getTag());
}
}
private void fill(int position, View view, boolean isBusy) {
AttachmentModel attachment = getItem(position).getLeft();
String attachmentHash = getItem(position).getMiddle();
ImageView tnImage = (ImageView) view.findViewById(R.id.post_thumbnail_image);
if (attachment.thumbnail == null || attachment.thumbnail.length() == 0) {
tnImage.setTag(Boolean.TRUE);
tnImage.setImageResource(Attachments.getDefaultThumbnailResId(attachment.type));
return;
}
tnImage.setTag(Boolean.FALSE);
CancellableTask imagesDownloadTask = BoardFragment.this.imagesDownloadTask;
ExecutorService imagesDownloadExecutor = BoardFragment.this.imagesDownloadExecutor;
if (presentationModel == null) {
Fragment currentFragment = MainApplication.getInstance().tabsSwitcher.currentFragment;
if (currentFragment instanceof BoardFragment) {
imagesDownloadTask = ((BoardFragment) currentFragment).imagesDownloadTask;
imagesDownloadExecutor = ((BoardFragment) currentFragment).imagesDownloadExecutor;
}
}
bitmapCache.asyncGet(
attachmentHash,
attachment.thumbnail,
tnSize,
chan,
localFile,
imagesDownloadTask,
tnImage,
imagesDownloadExecutor,
Async.UI_HANDLER,
downloadThumbnails() && !isBusy,
downloadThumbnails() ? (isBusy ? 0 : R.drawable.thumbnail_error) :
Attachments.getDefaultThumbnailResId(attachment.type));
}
public void setSelectingMode(boolean selectingMode) {
this.selectingMode = selectingMode;
if (!selectingMode) {
Arrays.fill(isSelected, false);
notifyDataSetChanged();
}
}
public void selectAll() {
if (selectingMode) {
Arrays.fill(isSelected, true);
notifyDataSetChanged();
}
}
public void downloadSelected(final Runnable onFinish) {
final Dialog progressDialog = ProgressDialog.show(activity,
resources.getString(R.string.grid_gallery_dlg_title), resources.getString(R.string.grid_gallery_dlg_message), true, false);
Async.runAsync(new Runnable() {
@Override
public void run() {
BoardFragment fragment = BoardFragment.this;
if (fragment.presentationModel == null) {
Fragment currentFragment = MainApplication.getInstance().tabsSwitcher.currentFragment;
if (currentFragment instanceof BoardFragment) fragment = (BoardFragment) currentFragment;
}
boolean flag = false;
for (int i=0; i<isSelected.length; ++i)
if (isSelected[i])
if (!fragment.downloadFile(getItem(i).getLeft(), true))
flag = true;
final boolean toast = flag;
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (toast) Toast.makeText(activity, R.string.notification_download_exists_or_in_queue, Toast.LENGTH_LONG).show();
progressDialog.dismiss();
onFinish.run();
}
});
}
});
}
}
try {
List<Triple<AttachmentModel, String, String>> list = presentationModel.getAttachments();
if (list == null) {
Toast.makeText(activity, R.string.notifacation_updating_now, Toast.LENGTH_LONG).show();
return;
}
GridView grid = new GridView(activity);
final GridGalleryAdapter gridAdapter = new GridGalleryAdapter(grid, list);
grid.setNumColumns(GridView.AUTO_FIT);
grid.setColumnWidth(tnSize);
int spacing = (int) (resources.getDisplayMetrics().density * 5 + 0.5f);
grid.setVerticalSpacing(spacing);
grid.setHorizontalSpacing(spacing);
grid.setPadding(spacing, spacing, spacing, spacing);
grid.setAdapter(gridAdapter);
grid.setOnScrollListener(gridAdapter);
grid.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f));
final Button btnToSelecting = new Button(activity);
btnToSelecting.setText(R.string.grid_gallery_select);
CompatibilityUtils.setTextAppearance(btnToSelecting, android.R.style.TextAppearance_Small);
btnToSelecting.setSingleLine();
btnToSelecting.setVisibility(View.VISIBLE);
btnToSelecting.setLayoutParams(
new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
final LinearLayout layoutSelectingButtons = new LinearLayout(activity);
layoutSelectingButtons.setOrientation(LinearLayout.HORIZONTAL);
layoutSelectingButtons.setWeightSum(10f);
Button btnDownload = new Button(activity);
btnDownload.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 3.25f));
btnDownload.setText(R.string.grid_gallery_download);
CompatibilityUtils.setTextAppearance(btnDownload, android.R.style.TextAppearance_Small);
btnDownload.setSingleLine();
Button btnSelectAll = new Button(activity);
btnSelectAll.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 3.75f));
btnSelectAll.setText(android.R.string.selectAll);
CompatibilityUtils.setTextAppearance(btnSelectAll, android.R.style.TextAppearance_Small);
btnSelectAll.setSingleLine();
Button btnCancel = new Button(activity);
btnCancel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 3f));
btnCancel.setText(android.R.string.cancel);
CompatibilityUtils.setTextAppearance(btnCancel, android.R.style.TextAppearance_Small);
btnCancel.setSingleLine();
layoutSelectingButtons.addView(btnDownload);
layoutSelectingButtons.addView(btnSelectAll);
layoutSelectingButtons.addView(btnCancel);
layoutSelectingButtons.setVisibility(View.GONE);
layoutSelectingButtons.setLayoutParams(
new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
btnToSelecting.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btnToSelecting.setVisibility(View.GONE);
layoutSelectingButtons.setVisibility(View.VISIBLE);
gridAdapter.setSelectingMode(true);
}
});
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btnToSelecting.setVisibility(View.VISIBLE);
layoutSelectingButtons.setVisibility(View.GONE);
gridAdapter.setSelectingMode(false);
}
});
btnSelectAll.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
gridAdapter.selectAll();
}
});
btnDownload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
gridAdapter.downloadSelected(new Runnable() {
@Override
public void run() {
btnToSelecting.setVisibility(View.VISIBLE);
layoutSelectingButtons.setVisibility(View.GONE);
gridAdapter.setSelectingMode(false);
}
});
}
});
LinearLayout dlgLayout = new LinearLayout(activity);
dlgLayout.setOrientation(LinearLayout.VERTICAL);
dlgLayout.addView(btnToSelecting);
dlgLayout.addView(layoutSelectingButtons);
dlgLayout.addView(grid);
Dialog gridDialog = new Dialog(activity);
gridDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
gridDialog.setContentView(dlgLayout);
gridDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
gridDialog.show();
} catch (OutOfMemoryError oom) {
MainApplication.freeMemory();
Logger.e(TAG, oom);
Toast.makeText(activity, R.string.error_out_of_memory, Toast.LENGTH_LONG).show();
}
}
private void openAttachment(AttachmentModel attachment) {
if (attachment.type == AttachmentModel.TYPE_OTHER_NOTFILE) {
UrlHandler.open(chan.fixRelativeUrl(attachment.path), activity);
return;
}
if (presentationModel == null || presentationModel.source == null || presentationModel.source.boardModel == null) return;
Intent galleryIntent = new Intent(activity.getApplicationContext(), GalleryActivity.class);
galleryIntent.putExtra(GalleryActivity.EXTRA_SETTINGS, GallerySettings.fromSettings(settings));
galleryIntent.putExtra(GalleryActivity.EXTRA_ATTACHMENT, attachment);
galleryIntent.putExtra(GalleryActivity.EXTRA_BOARDMODEL, presentationModel.source.boardModel);
galleryIntent.putExtra(GalleryActivity.EXTRA_PAGEHASH, tabModel.hash);
if (tabModel.type == TabModel.TYPE_LOCAL) {
galleryIntent.putExtra(GalleryActivity.EXTRA_LOCALFILENAME, tabModel.localFilePath);
}
startActivity(galleryIntent);
}
@SuppressLint("InflateParams")
private void runDelete(final DeletePostModel deletePostModel, final boolean hasFiles) {
Context dialogContext = Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ?
new ContextThemeWrapper(activity, R.style.Theme_Neutron) : activity;
View dlgLayout = LayoutInflater.from(dialogContext).inflate(R.layout.dialog_delete, null);
final EditText inputField = (EditText) dlgLayout.findViewById(R.id.dialog_delete_password_field);
final CheckBox onlyFiles = (CheckBox) dlgLayout.findViewById(R.id.dialog_delete_only_files);
inputField.setText(chan.getDefaultPassword());
if (!presentationModel.source.boardModel.allowDeletePosts && !presentationModel.source.boardModel.allowDeleteFiles) {
Logger.e(TAG, "board model doesn't support deleting");
return;
} else if (!presentationModel.source.boardModel.allowDeletePosts) {
onlyFiles.setEnabled(false);
onlyFiles.setChecked(true);
} else if (presentationModel.source.boardModel.allowDeleteFiles && hasFiles) {
onlyFiles.setEnabled(true);
} else {
onlyFiles.setEnabled(false);
}
DialogInterface.OnClickListener dlgOnClick = new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
if (currentTask != null) currentTask.cancel();
if (pullableLayout.isRefreshing()) setPullableNoRefreshing();
deletePostModel.onlyFiles = onlyFiles.isChecked();
deletePostModel.password = inputField.getText().toString();
final ProgressDialog progressDlg = new ProgressDialog(activity);
final CancellableTask deleteTask = new CancellableTask.BaseCancellableTask();
progressDlg.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
deleteTask.cancel();
}
});
progressDlg.setCanceledOnTouchOutside(false);
progressDlg.setMessage(resources.getString(R.string.dialog_delete_progress));
progressDlg.show();
Async.runAsync(new Runnable() {
@Override
public void run() {
String error = null;
String targetUrl = null;
if (deleteTask.isCancelled()) return;
try {
targetUrl = chan.deletePost(deletePostModel, null, deleteTask);
} catch (Exception e) {
if (e instanceof InteractiveException) {
if (deleteTask.isCancelled()) return;
((InteractiveException) e).handle(activity, deleteTask, new InteractiveException.Callback() {
@Override
public void onSuccess() {
if (!deleteTask.isCancelled()) {
progressDlg.dismiss();
onClick(dialog, which);
}
}
@Override
public void onError(String message) {
if (!deleteTask.isCancelled()) {
progressDlg.dismiss();
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
runDelete(deletePostModel, hasFiles);
}
}
});
return;
}
Logger.e(TAG, "cannot delete post", e);
error = e.getMessage() == null ? "" : e.getMessage();
}
if (deleteTask.isCancelled()) return;
final boolean success = error == null;
final String result = success ? targetUrl : error;
Async.runOnUiThread(new Runnable() {
@Override
public void run() {
if (deleteTask.isCancelled()) return;
progressDlg.dismiss();
if (success) {
if (result == null) {
update();
} else {
UrlHandler.open(result, activity);
}
} else {
Toast.makeText(activity, TextUtils.isEmpty(result) ? resources.getString(R.string.error_unknown) : result,
Toast.LENGTH_LONG).show();
}
}
});
}
});
}
};
new AlertDialog.Builder(activity).
setTitle(R.string.dialog_delete_password).
setView(dlgLayout).
setPositiveButton(R.string.dialog_delete_button, dlgOnClick).
setNegativeButton(android.R.string.cancel, null).
create().
show();
}
private void runReport(final DeletePostModel reportPostModel) {
final EditText inputField = new EditText(activity);
inputField.setSingleLine();
if (presentationModel.source.boardModel.allowReport != BoardModel.REPORT_WITH_COMMENT) {
inputField.setEnabled(false);
inputField.setKeyListener(null);
} else {
inputField.setText(reportPostModel.reportReason == null ? "" : reportPostModel.reportReason);
}
DialogInterface.OnClickListener dlgOnClick = new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
if (currentTask != null) currentTask.cancel();
if (pullableLayout.isRefreshing()) setPullableNoRefreshing();
reportPostModel.reportReason = inputField.getText().toString();
final ProgressDialog progressDlg = new ProgressDialog(activity);
final CancellableTask reportTask = new CancellableTask.BaseCancellableTask();
progressDlg.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
reportTask.cancel();
}
});
progressDlg.setCanceledOnTouchOutside(false);
progressDlg.setMessage(resources.getString(R.string.dialog_report_progress));
progressDlg.show();
Async.runAsync(new Runnable() {
@Override
public void run() {
String error = null;
String targetUrl = null;
if (reportTask.isCancelled()) return;
try {
targetUrl = chan.reportPost(reportPostModel, null, reportTask);
} catch (Exception e) {
if (e instanceof InteractiveException) {
if (reportTask.isCancelled()) return;
((InteractiveException) e).handle(activity, reportTask, new InteractiveException.Callback() {
@Override
public void onSuccess() {
if (!reportTask.isCancelled()) {
progressDlg.dismiss();
onClick(dialog, which);
}
}
@Override
public void onError(String message) {
if (!reportTask.isCancelled()) {
progressDlg.dismiss();
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
runReport(reportPostModel);
}
}
});
return;
}
Logger.e(TAG, "cannot report post", e);
error = e.getMessage() == null ? "" : e.getMessage();
}
if (reportTask.isCancelled()) return;
final boolean success = error == null;
final String result = success ? targetUrl : error;
Async.runOnUiThread(new Runnable() {
@Override
public void run() {
if (reportTask.isCancelled()) return;
progressDlg.dismiss();
if (success) {
if (result == null) {
update();
} else {
UrlHandler.open(result, activity);
}
} else {
Toast.makeText(activity, TextUtils.isEmpty(result) ? resources.getString(R.string.error_unknown) : result,
Toast.LENGTH_LONG).show();
}
}
});
}
});
}
};
new AlertDialog.Builder(activity).
setTitle(R.string.dialog_report_reason).
setView(inputField).
setPositiveButton(R.string.dialog_report_button, dlgOnClick).
setNegativeButton(android.R.string.cancel, null).
create().
show();
}
private void openFromChan() {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
UrlHandler.open(presentationModel.source.pageModel, activity);
}
}
};
new AlertDialog.Builder(activity).setMessage(R.string.dialog_open_chan_text).
setPositiveButton(android.R.string.yes, dialogClickListener).setNegativeButton(android.R.string.no, dialogClickListener).create().show();
}
private static class OpenedDialogs {
private List<WeakReference<Dialog>> refsList = new ArrayList<>();
private ReferenceQueue<Dialog> queue = new ReferenceQueue<>();
private void reduce() {
Reference<? extends Dialog> r;
while ((r = queue.poll()) != null) {
int i = refsList.indexOf(r);
if (i != -1) refsList.remove(i);
}
}
private synchronized void add(Dialog dialog) {
reduce();
refsList.add(new WeakReference<>(dialog));
}
public void onDestroyFragment(long tabId) {
Fragment currentFragment = MainApplication.getInstance().tabsSwitcher.currentFragment;
if (currentFragment instanceof BoardFragment && currentFragment.getArguments().getLong("TabModelId") == tabId) return;
reduce();
for (int i=0; i<refsList.size(); ++i) {
Dialog dialog = refsList.get(i).get();
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
}
}
}
}