diff --git a/src/main/java/org/quantumbadger/redreader/common/ImagePreviewUtils.java b/src/main/java/org/quantumbadger/redreader/common/ImagePreviewUtils.java new file mode 100644 index 000000000..104dc19c8 --- /dev/null +++ b/src/main/java/org/quantumbadger/redreader/common/ImagePreviewUtils.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * This file is part of RedReader. + * + * RedReader 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. + * + * RedReader 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 RedReader. If not, see . + ******************************************************************************/ + +package org.quantumbadger.redreader.common; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; +import android.util.Log; +import android.view.View; +import android.view.LayoutInflater; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import org.quantumbadger.redreader.R; +import org.quantumbadger.redreader.activities.BaseActivity; +import org.quantumbadger.redreader.account.RedditAccountManager; +import org.quantumbadger.redreader.cache.CacheManager; +import org.quantumbadger.redreader.cache.CacheRequest; +import org.quantumbadger.redreader.cache.CacheRequestCallbacks; +import org.quantumbadger.redreader.cache.downloadstrategy.DownloadStrategyIfNotCached; +import org.quantumbadger.redreader.common.datastream.SeekableInputStream; +import org.quantumbadger.redreader.common.time.TimestampUTC; +import org.quantumbadger.redreader.reddit.prepared.RedditParsedPost; +import org.quantumbadger.redreader.reddit.prepared.RedditPreparedPost; +import org.quantumbadger.redreader.views.RRAnimationShrinkHeight; +import org.quantumbadger.redreader.views.liststatus.ErrorView; + +public class ImagePreviewUtils { + + private static final String TAG = "ImagePreviewUtils"; + private static final String PROMPT_PREF_KEY = "inline_image_prompt_accepted"; + private static final AtomicInteger sInlinePreviewsShownThisSession = new AtomicInteger(0); + + public interface ImagePreviewListener { + void setImageBitmap(Bitmap bitmap); + void setLoadingSpinnerVisible(boolean visible); + void setOuterViewVisible(boolean visible); + void setPlayOverlayVisible(boolean visible); + void setPreviewDimensions(String ratio); + LinearLayout getFooterView(); + BaseActivity getActivity(); + boolean isUsageIdValid(int usageId); + void addErrorView(ErrorView errorView); + void setErrorViewLayout(View errorView); + } + + public static void showPrefPrompt(final ImagePreviewListener listener) { + final BaseActivity activity = listener.getActivity(); + final SharedPrefsWrapper sharedPrefs = General.getSharedPrefs(activity); + + LayoutInflater.from(activity).inflate( + R.layout.inline_images_question_view, + listener.getFooterView(), + true); + + final FrameLayout promptView = listener.getFooterView() + .findViewById(R.id.inline_images_prompt_root); + final Button keepShowing = listener.getFooterView() + .findViewById(R.id.inline_preview_prompt_keep_showing_button); + final Button turnOff = listener.getFooterView() + .findViewById(R.id.inline_preview_prompt_turn_off_button); + + keepShowing.setOnClickListener(v -> { + new RRAnimationShrinkHeight(promptView).start(); + sharedPrefs.edit() + .putBoolean(PROMPT_PREF_KEY, true) + .apply(); + }); + + turnOff.setOnClickListener(v -> { + final String prefPreview = activity.getApplicationContext() + .getString(R.string.pref_images_inline_image_previews_key); + sharedPrefs.edit() + .putBoolean(PROMPT_PREF_KEY, true) + .putString(prefPreview, "never") + .apply(); + }); + } + + public static void downloadInlinePreview( + @NonNull final BaseActivity activity, + @NonNull final RedditPreparedPost post, + @NonNull final ImagePreviewListener listener, + final int usageId) { + + final Rect windowVisibleDisplayFrame = DisplayUtils.getWindowVisibleDisplayFrame(activity); + + final int screenWidth = Math.min(1080, Math.max(720, windowVisibleDisplayFrame.width())); + final int screenHeight = Math.min(2000, Math.max(400, windowVisibleDisplayFrame.height())); + + final RedditParsedPost.ImagePreviewDetails preview = post.src.getPreview(screenWidth, 0); + + if(preview == null || preview.width < 10 || preview.height < 10) { + listener.setOuterViewVisible(false); + listener.setLoadingSpinnerVisible(false); + return; + } + + final int boundedImageHeight = Math.min( + (screenHeight * 2) / 3, + (int)(((long)preview.height * screenWidth) / preview.width)); + + listener.setPreviewDimensions(screenWidth + ":" + boundedImageHeight); + listener.setOuterViewVisible(true); + listener.setLoadingSpinnerVisible(true); + + CacheManager.getInstance(activity).makeRequest(new CacheRequest( + preview.url, + RedditAccountManager.getAnon(), + null, + new Priority(Constants.Priority.INLINE_IMAGE_PREVIEW), + DownloadStrategyIfNotCached.INSTANCE, + Constants.FileType.INLINE_IMAGE_PREVIEW, + CacheRequest.DownloadQueueType.IMMEDIATE, + activity, + new CacheRequestCallbacks() { + @Override + public void onDataStreamComplete( + @NonNull final GenericFactory stream, + final TimestampUTC timestamp, + @NonNull final UUID session, + final boolean fromCache, + @Nullable final String mimetype) { + + if(!listener.isUsageIdValid(usageId)) { + return; + } + + try(InputStream is = stream.create()) { + final Bitmap data = BitmapFactory.decodeStream(is); + + if(data == null) { + throw new IOException("Failed to decode bitmap"); + } + + // Avoid a crash on badly behaving Android ROMs (where the ImageView + // crashes if an image is too big) + // Should never happen as we limit the preview size to 3000x3000 + if(data.getByteCount() > 50 * 1024 * 1024) { + throw new RuntimeException("Image was too large: " + + data.getByteCount() + + ", preview URL was " + + preview.url + + " and post was " + + post.src.getIdAndType()); + } + + final boolean alreadyAcceptedPrompt = General.getSharedPrefs(activity) + .getBoolean(PROMPT_PREF_KEY, false); + + final int totalPreviewsShown + = sInlinePreviewsShownThisSession.incrementAndGet(); + + final boolean isVideoPreview = post.isVideoPreview(); + + AndroidCommon.runOnUiThread(() -> { + listener.setImageBitmap(data); + listener.setLoadingSpinnerVisible(false); + + if(isVideoPreview) { + listener.setPlayOverlayVisible(true); + } + + // Show every 8 previews, starting at the second one + if(totalPreviewsShown % 8 == 2 && !alreadyAcceptedPrompt) { + showPrefPrompt(listener); + } + }); + + } catch(final Throwable t) { + onFailure(General.getGeneralErrorForFailure( + activity, + CacheRequest.RequestFailureType.CONNECTION, + t, + null, + preview.url, + Optional.empty())); + } + } + + @Override + public void onFailure(@NonNull final RRError error) { + Log.e(TAG, "Failed to download image preview: " + error, error.t); + + if(!listener.isUsageIdValid(usageId)) { + return; + } + + AndroidCommon.runOnUiThread(() -> { + listener.setLoadingSpinnerVisible(false); + listener.setOuterViewVisible(false); + + final ErrorView errorView = new ErrorView( + activity, + error); + + listener.addErrorView(errorView); + listener.setErrorViewLayout(errorView); + }); + } + } + )); + } +} diff --git a/src/main/java/org/quantumbadger/redreader/common/PrefsUtility.java b/src/main/java/org/quantumbadger/redreader/common/PrefsUtility.java index 1262439fb..9a2a96aa2 100644 --- a/src/main/java/org/quantumbadger/redreader/common/PrefsUtility.java +++ b/src/main/java/org/quantumbadger/redreader/common/PrefsUtility.java @@ -59,6 +59,10 @@ public final class PrefsUtility { private static SharedPrefsWrapper sharedPrefs; private static Resources mRes; + public static String getLayoutMode() { + return sharedPrefs.getString("pref_layout_mode", "thumbnails"); + } + private static String getPrefKey(@StringRes final int prefKey) { return mRes.getString(prefKey); } diff --git a/src/main/java/org/quantumbadger/redreader/reddit/prepared/RedditParsedPost.kt b/src/main/java/org/quantumbadger/redreader/reddit/prepared/RedditParsedPost.kt index bd4f51a08..8979ce331 100644 --- a/src/main/java/org/quantumbadger/redreader/reddit/prepared/RedditParsedPost.kt +++ b/src/main/java/org/quantumbadger/redreader/reddit/prepared/RedditParsedPost.kt @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with RedReader. If not, see . ******************************************************************************/ + package org.quantumbadger.redreader.reddit.prepared import androidx.appcompat.app.AppCompatActivity @@ -141,9 +142,25 @@ class RedditParsedPost( @JvmField val height: Int ) - fun getPreview(minWidth: Int, minHeight: Int) = src.preview?.images?.get(0)?.run { - getPreviewInternal(this, minWidth, minHeight) - } + + val isGallery = src.gallery_data?.items?.firstOrNull()?.ok()?.media_id != null + + fun getPreview(minWidth: Int, minHeight: Int): ImagePreviewDetails? { + if (isGallery) { + val firstItem = src.gallery_data?.items?.firstOrNull()?.ok()?.media_id ?: return null + val metadata = src.media_metadata?.get(firstItem)?.ok() ?: return null + + return ImagePreviewDetails( + UriString(metadata.s.u?.decoded ?: return null), + metadata.s.x.toInt(), + metadata.s.y.toInt() + ) + } + + return src.preview?.images?.get(0)?.run { + getPreviewInternal(this, minWidth, minHeight) + } + } fun getPreviewMP4(minWidth: Int, minHeight: Int) = src.preview?.images?.get(0)?.variants?.mp4?.apply { diff --git a/src/main/java/org/quantumbadger/redreader/reddit/prepared/RedditPreparedPost.java b/src/main/java/org/quantumbadger/redreader/reddit/prepared/RedditPreparedPost.java index ae0c90f1a..8008c6370 100644 --- a/src/main/java/org/quantumbadger/redreader/reddit/prepared/RedditPreparedPost.java +++ b/src/main/java/org/quantumbadger/redreader/reddit/prepared/RedditPreparedPost.java @@ -72,6 +72,7 @@ public final class RedditPreparedPost implements RedditChangeDataManager.Listene public final boolean canModerate; public final boolean hasThumbnail; public final boolean mIsProbablyAnImage; + public final boolean isGallery; private final boolean mShowInlinePreviews; @@ -109,6 +110,7 @@ public RedditPreparedPost( isArchived = post.isArchived(); isLocked = post.isLocked(); canModerate = post.getCanModerate(); + isGallery = post.isGallery(); mIsProbablyAnImage = LinkHandler.isProbablyAnImage(post.getUrl()); @@ -127,18 +129,27 @@ public RedditPreparedPost( } public boolean shouldShowInlinePreview() { + final String layoutMode = PrefsUtility.getLayoutMode(); + final boolean alwaysPreviewMode = layoutMode.equals("always_preview") + || layoutMode.equals("card"); + return mShowInlinePreviews && (src.isPreviewEnabled() || "gfycat.com".equals(src.getDomain()) || "i.imgur.com".equals(src.getDomain()) || "streamable.com".equals(src.getDomain()) || "i.redd.it".equals(src.getDomain()) - || "v.redd.it".equals(src.getDomain())); + || "v.redd.it".equals(src.getDomain()) + || (isGallery && alwaysPreviewMode)); } public boolean isVideoPreview() { return src.isVideoPreview(); } + public boolean isGalleryPreview() { + return isGallery; + } + public void performAction(final BaseActivity activity, final RedditPostActions.Action action) { RedditPostActions.INSTANCE.onActionMenuItemSelected(this, activity, action); } diff --git a/src/main/java/org/quantumbadger/redreader/views/RedditPostHeaderView.java b/src/main/java/org/quantumbadger/redreader/views/RedditPostHeaderView.java index 8b1452c95..c575cc40d 100644 --- a/src/main/java/org/quantumbadger/redreader/views/RedditPostHeaderView.java +++ b/src/main/java/org/quantumbadger/redreader/views/RedditPostHeaderView.java @@ -26,6 +26,13 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.TooltipCompat; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.annotation.NonNull; +import android.graphics.Bitmap; + import org.quantumbadger.redreader.R; import org.quantumbadger.redreader.account.RedditAccount; import org.quantumbadger.redreader.account.RedditAccountManager; @@ -37,6 +44,8 @@ import org.quantumbadger.redreader.reddit.api.RedditPostActions; import org.quantumbadger.redreader.reddit.prepared.RedditChangeDataManager; import org.quantumbadger.redreader.reddit.prepared.RedditPreparedPost; +import org.quantumbadger.redreader.common.ImagePreviewUtils; +import org.quantumbadger.redreader.views.liststatus.ErrorView; public class RedditPostHeaderView extends LinearLayout { @@ -45,24 +54,55 @@ public class RedditPostHeaderView extends LinearLayout { @Nullable private final Runnable mChangeListenerAddTask; @Nullable private final Runnable mChangeListenerRemoveTask; + @NonNull private final FrameLayout mImagePreviewHolder; + @NonNull private final ImageView mImagePreviewImageView; + @NonNull private final ConstraintLayout mImagePreviewPlayOverlay; + @NonNull private final LinearLayout mImagePreviewOuter; + @NonNull private final LoadingSpinnerView mImagePreviewLoadingSpinner; + @NonNull private final BaseActivity mActivity; + @NonNull private final LinearLayout mPostErrors; + + static private int mUsageId = 0; + public RedditPostHeaderView( final BaseActivity activity, final RedditPreparedPost post) { super(activity); - final float dpScale = activity.getResources().getDisplayMetrics().density; + mActivity = activity; + + final String layoutMode = PrefsUtility.getLayoutMode(); + + boolean alwaysPreviewMode = false; + + switch(layoutMode) { + case "always_preview": + alwaysPreviewMode = true; + break; + case "card": + alwaysPreviewMode = true; + break; + default: + alwaysPreviewMode = false; + break; + } + + final boolean showInlinePreview = alwaysPreviewMode || + post.shouldShowInlinePreview(); + + final float dpScale = mActivity.getResources().getDisplayMetrics().density; setOrientation(LinearLayout.VERTICAL); - final LinearLayout greyHeader = new LinearLayout(activity); + final LinearLayout greyHeader = new LinearLayout(mActivity); RedditPostActions.INSTANCE.setupAccessibilityActions( new AccessibilityActionManager( greyHeader, - activity.getResources()), + mActivity.getResources()), post, - activity, + mActivity, true); greyHeader.setOrientation(LinearLayout.VERTICAL); @@ -74,27 +114,46 @@ public RedditPostHeaderView( final float titleFontScale = PrefsUtility.appearance_fontscale_post_header_titles(); - final TextView title = new TextView(activity); + final TextView title = new TextView(mActivity); title.setTextSize(19.0f * titleFontScale); title.setTypeface(Fonts.getRobotoLightOrAlternative()); title.setText(post.src.getTitle()); - title.setContentDescription(post.buildAccessibilityTitle(activity, true)); + title.setContentDescription(post.buildAccessibilityTitle(mActivity, true)); title.setTextColor(Color.WHITE); greyHeader.addView(title); + final View previewView = inflate(mActivity, R.layout.post_header_preview, null); + + mPostErrors = previewView.findViewById(R.id.reddit_post_errors); + + mImagePreviewOuter = previewView.findViewById(R.id.reddit_post_image_preview_outer); + mImagePreviewHolder = previewView.findViewById(R.id.reddit_post_image_preview_holder); + mImagePreviewImageView = previewView.findViewById(R.id.reddit_post_image_preview_imageview); + mImagePreviewPlayOverlay = previewView.findViewById( + R.id.reddit_post_image_preview_play_overlay); + + mImagePreviewLoadingSpinner = new LoadingSpinnerView(mActivity); + mImagePreviewHolder.addView(mImagePreviewLoadingSpinner); + + greyHeader.addView(previewView, greyHeader.indexOfChild(title) + 1); + + if(showInlinePreview) { + ImagePreviewUtils.downloadInlinePreview(activity, post, previewListener, mUsageId); + } + final float subtitleFontScale = PrefsUtility.appearance_fontscale_post_header_subtitles(); - subtitle = new TextView(activity); + subtitle = new TextView(mActivity); subtitle.setTextSize(13.0f * subtitleFontScale); - subtitle.setText(post.buildSubtitle(activity, true)); - subtitle.setContentDescription(post.buildAccessibilitySubtitle(activity, true)); + subtitle.setText(post.buildSubtitle(mActivity, true)); + subtitle.setContentDescription(post.buildAccessibilitySubtitle(mActivity, true)); subtitle.setTextColor(Color.rgb(200, 200, 200)); greyHeader.addView(subtitle); { - final TypedArray appearance = activity.obtainStyledAttributes(new int[] { + final TypedArray appearance = mActivity.obtainStyledAttributes(new int[] { R.attr.rrPostListHeaderBackgroundCol}); greyHeader.setBackgroundColor(appearance.getColor(0, General.COLOR_INVALID)); @@ -105,7 +164,7 @@ public RedditPostHeaderView( greyHeader.setOnClickListener(v -> { if(!post.isSelf()) { LinkHandler.onLinkClicked( - activity, + mActivity, post.src.getUrl(), false, post.src.getSrc()); @@ -113,14 +172,14 @@ public RedditPostHeaderView( }); greyHeader.setOnLongClickListener(v -> { - RedditPostActions.INSTANCE.showActionMenu(activity, post); + RedditPostActions.INSTANCE.showActionMenu(mActivity, post); return true; }); addView(greyHeader); final RedditAccount currentUser = - RedditAccountManager.getInstance(activity).getDefaultAccount(); + RedditAccountManager.getInstance(mActivity).getDefaultAccount(); if(!currentUser.isAnonymous()) { @@ -133,7 +192,7 @@ public RedditPostHeaderView( if(!PrefsUtility.pref_appearance_hide_headertoolbar_commentlist()) { final LinearLayout buttons = - inflate(activity, R.layout.post_header_toolbar, this) + inflate(mActivity, R.layout.post_header_toolbar, this) .findViewById(R.id.post_toolbar_layout); for(int i = 0; i < buttons.getChildCount(); i++) { @@ -157,32 +216,32 @@ public RedditPostHeaderView( buttons.findViewById(R.id.post_toolbar_botton_more); buttonAddUpvote.setOnClickListener(v -> post.performAction( - activity, + mActivity, RedditPostActions.Action.UPVOTE)); buttonRemoveUpvote.setOnClickListener(v -> post.performAction( - activity, + mActivity, RedditPostActions.Action.UNVOTE)); buttonAddDownvote.setOnClickListener(v -> post.performAction( - activity, + mActivity, RedditPostActions.Action.DOWNVOTE)); buttonRemoveDownvote.setOnClickListener(v -> post.performAction( - activity, + mActivity, RedditPostActions.Action.UNVOTE)); buttonReply.setOnClickListener(v -> post.performAction( - activity, + mActivity, RedditPostActions.Action.REPLY)); buttonShare.setOnClickListener(v -> post.performAction( - activity, + mActivity, RedditPostActions.Action.SHARE)); buttonMore.setOnClickListener(v -> post.performAction( - activity, + mActivity, RedditPostActions.Action.ACTION_MENU)); changeListener = thingIdAndType -> { - subtitle.setText(post.buildSubtitle(activity, true)); + subtitle.setText(post.buildSubtitle(mActivity, true)); subtitle.setContentDescription( - post.buildAccessibilitySubtitle(activity, true)); + post.buildAccessibilitySubtitle(mActivity, true)); final boolean isUpvoted = changeDataManager.isUpvoted( post.src.getIdAndType()); @@ -212,9 +271,9 @@ public RedditPostHeaderView( } else { changeListener = thingIdAndType -> { - subtitle.setText(post.buildSubtitle(activity, true)); + subtitle.setText(post.buildSubtitle(mActivity, true)); subtitle.setContentDescription( - post.buildAccessibilitySubtitle(activity, true)); + post.buildAccessibilitySubtitle(mActivity, true)); }; } @@ -233,6 +292,62 @@ public RedditPostHeaderView( } } + private final ImagePreviewUtils.ImagePreviewListener previewListener = + new ImagePreviewUtils.ImagePreviewListener() { + @Override + public void setImageBitmap(final Bitmap bitmap) { + mImagePreviewImageView.setImageBitmap(bitmap); + } + + @Override + public void setLoadingSpinnerVisible(final boolean visible) { + mImagePreviewLoadingSpinner.setVisibility(visible ? VISIBLE : GONE); + } + + @Override + public void setOuterViewVisible(final boolean visible) { + mImagePreviewOuter.setVisibility(visible ? VISIBLE : GONE); + } + + @Override + public void setPlayOverlayVisible(final boolean visible) { + mImagePreviewPlayOverlay.setVisibility(visible ? VISIBLE : GONE); + } + + @Override + public void setPreviewDimensions(final String ratio) { + final ConstraintLayout.LayoutParams params = + (ConstraintLayout.LayoutParams)mImagePreviewHolder.getLayoutParams(); + params.dimensionRatio = ratio; + mImagePreviewHolder.setLayoutParams(params); + } + + @Override + public LinearLayout getFooterView() { + return null; + } + + @Override + public BaseActivity getActivity() { + return mActivity; + } + + @Override + public void addErrorView(final ErrorView errorView) { + mPostErrors.addView(errorView); + } + + @Override + public void setErrorViewLayout(final View errorView) { + General.setLayoutMatchWidthWrapHeight(errorView); + } + + @Override + public boolean isUsageIdValid(final int usageId) { + return usageId == mUsageId; + } + }; + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); diff --git a/src/main/java/org/quantumbadger/redreader/views/RedditPostView.java b/src/main/java/org/quantumbadger/redreader/views/RedditPostView.java index 6bce3e40d..8fd625749 100644 --- a/src/main/java/org/quantumbadger/redreader/views/RedditPostView.java +++ b/src/main/java/org/quantumbadger/redreader/views/RedditPostView.java @@ -20,68 +20,39 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Rect; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.constraintlayout.widget.ConstraintLayout; import org.quantumbadger.redreader.R; -import org.quantumbadger.redreader.account.RedditAccountManager; import org.quantumbadger.redreader.activities.BaseActivity; -import org.quantumbadger.redreader.cache.CacheManager; -import org.quantumbadger.redreader.cache.CacheRequest; -import org.quantumbadger.redreader.cache.CacheRequestCallbacks; -import org.quantumbadger.redreader.cache.downloadstrategy.DownloadStrategyIfNotCached; import org.quantumbadger.redreader.common.AndroidCommon; -import org.quantumbadger.redreader.common.Constants; -import org.quantumbadger.redreader.common.DisplayUtils; import org.quantumbadger.redreader.common.General; -import org.quantumbadger.redreader.common.GenericFactory; -import org.quantumbadger.redreader.common.Optional; import org.quantumbadger.redreader.common.PrefsUtility; -import org.quantumbadger.redreader.common.Priority; -import org.quantumbadger.redreader.common.RRError; -import org.quantumbadger.redreader.common.SharedPrefsWrapper; -import org.quantumbadger.redreader.common.datastream.SeekableInputStream; -import org.quantumbadger.redreader.common.time.TimestampUTC; import org.quantumbadger.redreader.fragments.PostListingFragment; import org.quantumbadger.redreader.reddit.api.RedditPostActions; -import org.quantumbadger.redreader.reddit.prepared.RedditParsedPost; import org.quantumbadger.redreader.reddit.prepared.RedditPreparedPost; +import org.quantumbadger.redreader.common.ImagePreviewUtils; import org.quantumbadger.redreader.views.liststatus.ErrorView; -import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; public final class RedditPostView extends FlingableItemView implements RedditPreparedPost.ThumbnailLoadedCallback { - private static final String TAG = "RedditPostView"; - - private static final String PROMPT_PREF_KEY = "inline_image_prompt_accepted"; - - private static final AtomicInteger sInlinePreviewsShownThisSession = new AtomicInteger(0); - private final AccessibilityActionManager mAccessibilityActionManager; private RedditPreparedPost mPost = null; @@ -209,8 +180,15 @@ public void handleMessage(@NonNull final Message msg) { final float titleFontScale = PrefsUtility.appearance_fontscale_posts(); final float subtitleFontScale = PrefsUtility.appearance_fontscale_post_subtitles(); - final View rootView = - LayoutInflater.from(context).inflate(R.layout.reddit_post, this, true); + final String layoutMode = PrefsUtility.getLayoutMode(); + + int layout = R.layout.reddit_post; + + if (layoutMode.equals("card")) { + layout = R.layout.reddit_post_card; + } + + final View rootView = LayoutInflater.from(context).inflate(layout, this, true); mOuterView = Objects.requireNonNull(rootView.findViewById(R.id.reddit_post_layout_outer)); mInnerView = Objects.requireNonNull(rootView.findViewById(R.id.reddit_post_layout_inner)); @@ -346,9 +324,26 @@ public void handleMessage(@NonNull final Message msg) { @UiThread public void reset(@NonNull final RedditPreparedPost newPost) { + final String layoutMode = PrefsUtility.getLayoutMode(); - if(newPost != mPost) { + boolean alwaysPreviewMode = false; + + switch(layoutMode) { + case "always_preview": + alwaysPreviewMode = true; + break; + case "card": + alwaysPreviewMode = true; + break; + default: + alwaysPreviewMode = false; + break; + } + + final boolean showInlinePreview = alwaysPreviewMode || + newPost.shouldShowInlinePreview(); + if(newPost != mPost) { mThumbnailView.setImageBitmap(null); mImagePreviewImageView.setImageBitmap(null); mImagePreviewPlayOverlay.setVisibility(GONE); @@ -364,21 +359,15 @@ public void reset(@NonNull final RedditPreparedPost newPost) { mCommentsText.setText(String.valueOf(newPost.src.getSrc().getNum_comments())); } - final boolean showInlinePreview = newPost.shouldShowInlinePreview(); - final boolean showThumbnail = !showInlinePreview && newPost.hasThumbnail; - if(showInlinePreview) { - downloadInlinePreview(newPost, mUsageId); - - } else { + if(!showInlinePreview) { mImagePreviewLoadingSpinner.setVisibility(GONE); mImagePreviewOuter.setVisibility(GONE); setBottomMargin(false); } if(showThumbnail) { - final Bitmap thumbnail = newPost.getThumbnail(this, mUsageId); mThumbnailView.setImageBitmap(thumbnail); @@ -386,12 +375,11 @@ public void reset(@NonNull final RedditPreparedPost newPost) { mThumbnailView.setMinimumWidth(mThumbnailSizePrefPixels); General.setLayoutWidthHeight( - mThumbnailView, - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.MATCH_PARENT); + mThumbnailView, + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.MATCH_PARENT); mInnerView.setMinimumHeight(mThumbnailSizePrefPixels); - } else { mThumbnailView.setMinimumWidth(0); mThumbnailView.setVisibility(GONE); @@ -407,6 +395,10 @@ public void reset(@NonNull final RedditPreparedPost newPost) { mPost = newPost; + if(showInlinePreview) { + ImagePreviewUtils.downloadInlinePreview(mActivity, mPost, previewListener, mUsageId); + } + updateAppearance(); } @@ -492,179 +484,59 @@ private void setBottomMargin(final boolean enabled) { mOuterView.setLayoutParams(layoutParams); } - private void downloadInlinePreview( - @NonNull final RedditPreparedPost post, - final int usageId) { - - final Rect windowVisibleDisplayFrame - = DisplayUtils.getWindowVisibleDisplayFrame(mActivity); - - final int screenWidth = Math.min(1080, Math.max(720, windowVisibleDisplayFrame.width())); - final int screenHeight = Math.min(2000, Math.max(400, windowVisibleDisplayFrame.height())); - - final RedditParsedPost.ImagePreviewDetails preview - = post.src.getPreview(screenWidth, 0); - - if(preview == null || preview.width < 10 || preview.height < 10) { - mImagePreviewOuter.setVisibility(GONE); - mImagePreviewLoadingSpinner.setVisibility(GONE); - setBottomMargin(false); - return; + private final ImagePreviewUtils.ImagePreviewListener previewListener = + new ImagePreviewUtils.ImagePreviewListener() { + @Override + public void setImageBitmap(final Bitmap bitmap) { + mImagePreviewImageView.setImageBitmap(bitmap); } - final int boundedImageHeight = Math.min( - (screenHeight * 2) / 3, - (int)(((long)preview.height * screenWidth) / preview.width)); - - final ConstraintLayout.LayoutParams imagePreviewLayoutParams - = (ConstraintLayout.LayoutParams)mImagePreviewHolder.getLayoutParams(); - - imagePreviewLayoutParams.dimensionRatio = screenWidth + ":" + boundedImageHeight; - mImagePreviewHolder.setLayoutParams(imagePreviewLayoutParams); - - mImagePreviewOuter.setVisibility(VISIBLE); - mImagePreviewLoadingSpinner.setVisibility(VISIBLE); - setBottomMargin(true); - - CacheManager.getInstance(mActivity).makeRequest(new CacheRequest( - preview.url, - RedditAccountManager.getAnon(), - null, - new Priority(Constants.Priority.INLINE_IMAGE_PREVIEW), - DownloadStrategyIfNotCached.INSTANCE, - Constants.FileType.INLINE_IMAGE_PREVIEW, - CacheRequest.DownloadQueueType.IMMEDIATE, - mActivity, - new CacheRequestCallbacks() { - @Override - public void onDataStreamComplete( - @NonNull final GenericFactory stream, - final TimestampUTC timestamp, - @NonNull final UUID session, - final boolean fromCache, - @Nullable final String mimetype) { - - if(usageId != mUsageId) { - return; - } - - try(InputStream is = stream.create()) { - - final Bitmap data = BitmapFactory.decodeStream(is); - - if(data == null) { - throw new IOException("Failed to decode bitmap"); - } - - // Avoid a crash on badly behaving Android ROMs (where the ImageView - // crashes if an image is too big) - // Should never happen as we limit the preview size to 3000x3000 - if(data.getByteCount() > 50 * 1024 * 1024) { - throw new RuntimeException("Image was too large: " - + data.getByteCount() - + ", preview URL was " - + preview.url - + " and post was " - + post.src.getIdAndType()); - } - - final boolean alreadyAcceptedPrompt = General.getSharedPrefs(mActivity) - .getBoolean(PROMPT_PREF_KEY, false); - - final int totalPreviewsShown - = sInlinePreviewsShownThisSession.incrementAndGet(); - - final boolean isVideoPreview = post.isVideoPreview(); - - AndroidCommon.runOnUiThread(() -> { - mImagePreviewImageView.setImageBitmap(data); - mImagePreviewLoadingSpinner.setVisibility(GONE); - - if(isVideoPreview) { - mImagePreviewPlayOverlay.setVisibility(VISIBLE); - } - - // Show every 8 previews, starting at the second one - if(totalPreviewsShown % 8 == 2 && !alreadyAcceptedPrompt) { - showPrefPrompt(); - } - }); - - } catch(final Throwable t) { - onFailure(General.getGeneralErrorForFailure( - mActivity, - CacheRequest.RequestFailureType.CONNECTION, - t, - null, - preview.url, - Optional.empty())); - } - } - - @Override - public void onFailure(@NonNull final RRError error) { - - Log.e(TAG, "Failed to download image preview: " + error, error.t); - - if(usageId != mUsageId) { - return; - } - - AndroidCommon.runOnUiThread(() -> { - - mImagePreviewLoadingSpinner.setVisibility(GONE); - mImagePreviewOuter.setVisibility(GONE); - - final ErrorView errorView = new ErrorView( - mActivity, - error); - - mPostErrors.addView(errorView); - General.setLayoutMatchWidthWrapHeight(errorView); - }); - } - } - )); - } - - private void showPrefPrompt() { - - final SharedPrefsWrapper sharedPrefs - = General.getSharedPrefs(mActivity); - - LayoutInflater.from(mActivity).inflate( - R.layout.inline_images_question_view, - mFooter, - true); - - final FrameLayout promptView - = mFooter.findViewById(R.id.inline_images_prompt_root); + @Override + public void setLoadingSpinnerVisible(final boolean visible) { + mImagePreviewLoadingSpinner.setVisibility(visible ? VISIBLE : GONE); + } - final Button keepShowing - = mFooter.findViewById(R.id.inline_preview_prompt_keep_showing_button); + @Override + public void setOuterViewVisible(final boolean visible) { + mImagePreviewOuter.setVisibility(visible ? VISIBLE : GONE); + } - final Button turnOff - = mFooter.findViewById(R.id.inline_preview_prompt_turn_off_button); + @Override + public void setPlayOverlayVisible(final boolean visible) { + mImagePreviewPlayOverlay.setVisibility(visible ? VISIBLE : GONE); + } - keepShowing.setOnClickListener(v -> { + @Override + public void setPreviewDimensions(final String ratio) { + final ConstraintLayout.LayoutParams params = + (ConstraintLayout.LayoutParams)mImagePreviewHolder.getLayoutParams(); + params.dimensionRatio = ratio; + mImagePreviewHolder.setLayoutParams(params); + } - new RRAnimationShrinkHeight(promptView).start(); + @Override + public LinearLayout getFooterView() { + return mFooter; + } - sharedPrefs.edit() - .putBoolean(PROMPT_PREF_KEY, true) - .apply(); - }); + @Override + public BaseActivity getActivity() { + return mActivity; + } - turnOff.setOnClickListener(v -> { + @Override + public void addErrorView(final ErrorView errorView) { + mPostErrors.addView(errorView); + } - final String prefPreview = mActivity.getApplicationContext() - .getString( - R.string.pref_images_inline_image_previews_key); + @Override + public void setErrorViewLayout(final View errorView) { + General.setLayoutMatchWidthWrapHeight(errorView); + } - sharedPrefs.edit() - .putBoolean(PROMPT_PREF_KEY, true) - .putString(prefPreview, "never") - .apply(); - }); - } + @Override + public boolean isUsageIdValid(final int usageId) { + return usageId == mUsageId; + } + }; } diff --git a/src/main/res/layout/post_header_preview.xml b/src/main/res/layout/post_header_preview.xml new file mode 100644 index 000000000..e4d462e7a --- /dev/null +++ b/src/main/res/layout/post_header_preview.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/layout/reddit_post_card.xml b/src/main/res/layout/reddit_post_card.xml new file mode 100644 index 000000000..96429f5d4 --- /dev/null +++ b/src/main/res/layout/reddit_post_card.xml @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index f07f2a665..00683000c 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -576,4 +576,4 @@ تضمين العنوان/الوصف عند النشر تعليقات ل%s التعليق من قبل %s في ريديت - \ No newline at end of file + diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 2c029dca3..2117e7785 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -669,6 +669,14 @@ Skrýt přečtené články Self-text příspěvku zkopírován do schránky + + + Rozvržení + Režim rozvržení + Miniatury + Vždy Náhled + Karta + Oznámení Smazat praporky v mezipaměti (přečteno, upvoted, atd.) po @@ -962,4 +970,4 @@ Kontroverzní s hlasy. Chyba při odesílání komentáře Kontroverzní: Tento rok - \ No newline at end of file + diff --git a/src/main/res/values-eo/strings.xml b/src/main/res/values-eo/strings.xml index e13a2f263..97c1431e6 100644 --- a/src/main/res/values-eo/strings.xml +++ b/src/main/res/values-eo/strings.xml @@ -625,6 +625,14 @@ Tio ĉi estas elŝut-ligilo. Ĉu vi volas malfermi ĝin per ekstera foliumilo? Frapeto sur memafiŝo + + + Aranĝo + Aranĝa reĝimo + Bildetojn + Ĉiam Antaŭrigardu + Karto + Montri /r/popular en ĉefmenuo Aktivigi filmet-ludadajn stirilojn @@ -944,4 +952,4 @@ Ĉi tio estas enarkivigita kaj ne plu alrespondebla. Indikilo de debatateco (†) Dum spektado de aŭdvidaĵo - \ No newline at end of file + diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 4154a22c2..a26529b6a 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -618,6 +618,14 @@ Comentario padre siguiente Comentario padre anterior Mostrar botones de navegación sobre los comentarios + + + Diseño + Modo de diseño + Miniaturas + Previsualizar Siempre + Tarjeta + Descargar archivo Esto es un enlace de descarga. ¿Deseas abrir esto en un navegador externo? @@ -942,4 +950,4 @@ Comentarios: Esta semana Indicador de controversia (†) Buscar en %s - \ No newline at end of file + diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index c04c1422c..e92fcbe50 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -550,6 +550,11 @@ Ausazko NSFW Subreddita Erakutsi tresna-barra behean Mututu bideoak modu lehenetsian + + + Diseinua + Diseinu modua + Irudiak/Bideoa Sarrerako Reddit mezuak Argitalpena testu propioa arbelera kopiatuta diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 1f207f2d9..5a915cf3e 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -554,6 +554,14 @@ Käytä sisäänrakennettua jakonäkymää Valitse sovellus jakamista varten Saapuvat Reddit-viestit + + + Asettelu + Asettelu-tila + Pikkukuvat + Esikatsele Aina + Kortti + Kuvat/Video Mykistä videot oletuksena Näytä työkalupalkki alhaalla @@ -786,4 +794,4 @@ Syöttämäsi CAPTCHA on väärä. Yritä uudelleen. Väärä CAPTCHA Redditin mukaan nykyisellä käyttäjällä ei ole lupaa tehdä sitä. - \ No newline at end of file + diff --git a/src/main/res/values-in/strings.xml b/src/main/res/values-in/strings.xml index 7860f706f..4c8ec9430 100644 --- a/src/main/res/values-in/strings.xml +++ b/src/main/res/values-in/strings.xml @@ -667,6 +667,14 @@ Tampilkan indikator pemuatan media jika tersedia Tampilkan bilah alat di bawah Matikan suara video secara bawaan + + + Tata Letak + Mode Tata Letak + Gambar mini + Selalu Pratinjau + Kartu + Gambar/Video Pesan Masuk Reddit Pos teks self disalin ke papan klip @@ -949,4 +957,4 @@ Baru: Bulang Ini Terhangat: Sepanjang Waktu Saat melihat media - \ No newline at end of file + diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 643d60dbf..5b9097c4d 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -552,6 +552,14 @@ Mostra un indicatore di caricamento del contenuto, se disponibile Mostra barra degli strumenti in basso Muta i video in modo predefinito + + + Layout + Modalità layout + Miniature + Anteprima Sempre + Carta + Immagini/Video Messaggi di Reddit in arrivo Testo del post copiato negli appunti diff --git a/src/main/res/values-lt/strings.xml b/src/main/res/values-lt/strings.xml index 457ae30d2..09750b7c4 100644 --- a/src/main/res/values-lt/strings.xml +++ b/src/main/res/values-lt/strings.xml @@ -519,6 +519,14 @@ Nėra įdiegtos tinkamos programos, kuria būtų galima tai bendrinti. Naudoti integruotą bendrinimo dialogo langą Įeinančios Reddit žinutės + + + Išdėstymas + Išdėstymo režimas + Miniatiūros + Visada Peržiūrėti + Kortelė + Vaizdai/Vaizdo įrašai Nutildyti vaizdo įrašus pagal automatiškai Rodyti įrankių juostą apačioje @@ -836,4 +844,4 @@ Aktualu: per visą laiką Autorius: %s, administratorius. Kol kas jokių įrašų. - \ No newline at end of file + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 594c12441..c4ee7cd82 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -652,6 +652,14 @@ Ukryj przeczytane posty Skróty Losowy subreddit NSFW + + + Układ + Tryb układu + Miniatury + Zawsze Podgląd + Karta + Obrazy/Wideo Post skopiowany do schowka Wybierz aplikację do udostępnienia @@ -879,4 +887,4 @@ Po zakończeniu logowania, powinieneś zostać przekierowany z powrotem do RedReadera. Jeśli logowanie nie powiedzie się, możesz spróbować użyć innej przeglądarki korzystając z poniższego pola wyboru. Aby się zalogować, zostaniesz przeniesiony do Reddita w przeglądarce. Podany obraz nie został odnaleziony. Mógł zostać usunięty. - \ No newline at end of file + diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 59edf6a9a..21ae49f66 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -585,6 +585,14 @@ Копировать собственный текст Текст сообщения скопирован в буфер обмена Входящие сообщения Reddit + + + Макет + Режим макета + Миниатюры + Всегда Превью + Карточка + Изображения/Видео Видео без звука по умолчанию Показывать панель инструментов внизу diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index c24d3463e..bfc2ce500 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -542,6 +542,14 @@ Застосунків, через які можливе поширення, не встановлено. Оберіть застосунок для поширення Текст допису скопійовано до буфера обміну + + + Макет + Режим макета + Мініатюри + Завжди Переглядати + картка + Зображення/Відео Показувати панель інструментів знизу Показувати індикатор співвідношення сторін @@ -837,4 +845,4 @@ Суперечливий індикатор (†) Пошук у %s Пошук «%1$s» у %2$s - \ No newline at end of file + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 24f59ee2b..49fc5a392 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -552,6 +552,14 @@ 在可用时显示加载中媒体的外观 在底部显示工具栏 默认将视频静音 + + + 布局 + 布局模式 + 缩略图 + 始终预览 + 卡片 + 图片/视频 传入的Reddit消息 帖子self-text已复制到剪贴板 @@ -834,4 +842,4 @@ 查看媒体文件时 从不 帖子已经存档,不能再回复。 - \ No newline at end of file + diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml index 9d3502316..500afbd55 100644 --- a/src/main/res/values/arrays.xml +++ b/src/main/res/values/arrays.xml @@ -1312,6 +1312,19 @@ np_reddit + + + @string/pref_layout_mode_thumbnails + @string/pref_layout_mode_alwayspreview + @string/pref_layout_mode_card + + + + thumbnails + always_preview + card + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 4d1f752c7..9240addf8 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1088,6 +1088,16 @@ pref_behaviour_video_mute_default Mute videos by default + + pref_layout_mode + Layout + Layout Mode + Thumbnails + Always Preview + Card + + Post Image + Images/Video Incoming Reddit Messages diff --git a/src/main/res/xml/prefs_images_video.xml b/src/main/res/xml/prefs_images_video.xml index 7126cba0d..94c686a5b 100644 --- a/src/main/res/xml/prefs_images_video.xml +++ b/src/main/res/xml/prefs_images_video.xml @@ -19,6 +19,19 @@ + + + + + +