diff options
Diffstat (limited to 'org.fox.ttrss/src')
29 files changed, 1269 insertions, 1494 deletions
diff --git a/org.fox.ttrss/src/main/AndroidManifest.xml b/org.fox.ttrss/src/main/AndroidManifest.xml index 3388d515..f2a209f9 100755 --- a/org.fox.ttrss/src/main/AndroidManifest.xml +++ b/org.fox.ttrss/src/main/AndroidManifest.xml @@ -21,9 +21,6 @@ android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" > - <meta-data android:name="org.fox.ttrss.glide.OkHttpProgressGlideModule" - android:value="GlideModule" /> - <meta-data android:name="android.max_aspect" android:value="2.1" /> <activity diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiCommon.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiCommon.java index 17d0c75a..e472a6b0 100644 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiCommon.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/ApiCommon.java @@ -1,6 +1,5 @@ package org.fox.ttrss; -import static org.fox.ttrss.glide.OkHttpProgressGlideModule.createInterceptor; import android.content.Context; import android.content.SharedPreferences; @@ -18,8 +17,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import org.fox.ttrss.glide.OkHttpProgressGlideModule; - import java.io.IOException; import java.util.HashMap; import java.util.Locale; @@ -27,11 +24,18 @@ import java.util.concurrent.TimeUnit; import okhttp3.Credentials; import okhttp3.HttpUrl; +import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; public class ApiCommon { public static final String TAG = "ApiCommon"; @@ -146,7 +150,7 @@ public class ApiCommon { Request request = requestBuilder.build(); - OkHttpProgressGlideModule.ResponseProgressListener listener = new OkHttpProgressGlideModule.ResponseProgressListener() { + ResponseProgressListener listener = new ResponseProgressListener() { @Override public void update(HttpUrl url, long bytesRead, long contentLength) { // Log.d(TAG, "[progress] " + url + " " + bytesRead + " of " + contentLength); @@ -266,6 +270,67 @@ public class ApiCommon { return null; } + private interface ResponseProgressListener { + void update(HttpUrl url, long bytesRead, long contentLength); + } + + private static Interceptor createInterceptor(final ResponseProgressListener listener) { + return chain -> { + Request request = chain.request(); + Response response = chain.proceed(request); + return response.newBuilder() + .body(new ProgressResponseBody(request.url(), response.body(), listener)) + .build(); + }; + } + + private static class ProgressResponseBody extends ResponseBody { + private final HttpUrl url; + private final ResponseBody responseBody; + private final ResponseProgressListener progressListener; + private BufferedSource bufferedSource; + + public ProgressResponseBody(HttpUrl url, ResponseBody responseBody, + ResponseProgressListener progressListener) { + + this.url = url; + this.responseBody = responseBody; + this.progressListener = progressListener; + } + + @Override public MediaType contentType() { + return responseBody.contentType(); + } + + @Override public long contentLength() { + return responseBody.contentLength(); + } + + @Override public BufferedSource source() { + if (bufferedSource == null) { + bufferedSource = Okio.buffer(source(responseBody.source())); + } + return bufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + long totalBytesRead = 0L; + @Override public long read(Buffer sink, long byteCount) throws IOException { + long bytesRead = super.read(sink, byteCount); + long fullLength = responseBody.contentLength(); + if (bytesRead == -1) { // this source is exhausted + totalBytesRead = fullLength; + } else { + totalBytesRead += bytesRead; + } + progressListener.update(url, totalBytesRead, fullLength); + return bytesRead; + } + }; + } + } + private static String getUserAgent(Context context) { try { PackageInfo packageInfo = context.getPackageManager(). diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleModel.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleModel.java index 2b596828..b1f99208 100644 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleModel.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/ArticleModel.java @@ -70,6 +70,12 @@ public class ArticleModel extends AndroidViewModel implements ApiCommon.ApiCalle return m_articles; } + public void updateById(@NonNull Article article) { + int position = m_articles.getValue().getPositionById(article.id); + + if (position != -1) + update(position, article); + } public void update(int position, Article article) { m_articles.getValue().set(position, article); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/CommonActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/CommonActivity.java index 97e356c4..12c74d2a 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/CommonActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/CommonActivity.java @@ -24,9 +24,12 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.View; +import android.view.WindowManager; import android.widget.CheckBox; import androidx.activity.EdgeToEdge; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.browser.customtabs.CustomTabsCallback; @@ -40,8 +43,8 @@ import androidx.preference.PreferenceManager; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.request.animation.GlideAnimation; import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.transition.Transition; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; @@ -234,6 +237,9 @@ public class CommonActivity extends AppCompatActivity implements SharedPreferenc m_prefs.registerOnSharedPreferenceChangeListener(this); + if (m_prefs.getBoolean("window_secure_mode", false)) + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + setupWidgetUpdates(this); if (savedInstanceState == null) { @@ -310,9 +316,9 @@ public class CommonActivity extends AppCompatActivity implements SharedPreferenc setAppTheme(sharedPreferences); } - String[] filter = new String[] { "enable_cats", "headline_mode", "widget_update_interval", + String[] filter = new String[] { "enable_cats", "widget_update_interval", "headlines_swipe_to_dismiss", "headlines_mark_read_scroll", "headlines_request_size", - "force_phone_layout", "open_on_startup"}; + "force_phone_layout", "open_on_startup", "window_secure_mode" }; m_needRestart = Arrays.asList(filter).contains(key); } @@ -348,13 +354,13 @@ public class CommonActivity extends AppCompatActivity implements SharedPreferenc protected void shareImageFromUri(String url) { Glide.with(this) - .load(url) .asBitmap() + .load(url) .skipMemoryCache(false) .diskCacheStrategy(DiskCacheStrategy.ALL) .into(new SimpleTarget<Bitmap>() { @Override - public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) { + public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { Log.d(TAG, "image resource ready: " + resource); if (resource != null) { diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/DetailActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/DetailActivity.java index 10b06022..59ff4e66 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/DetailActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/DetailActivity.java @@ -90,12 +90,12 @@ public class DetailActivity extends OnlineActivity implements HeadlinesEventList return true; } else if (itemId == R.id.toggle_unread) { - article.unread = !article.unread; - saveArticleUnread(article); + Article articleClone = new Article(article); - if (hf != null) { - hf.notifyItemChanged(Application.getArticles().indexOf(article)); - } + articleClone.unread = !articleClone.unread; + saveArticleUnread(articleClone); + + Application.getArticlesModel().updateById(articleClone); } } @@ -272,9 +272,13 @@ public class DetailActivity extends OnlineActivity implements HeadlinesEventList @Override public void onArticleSelected(Article article, boolean open) { - if (article.unread) { - article.unread = false; - saveArticleUnread(article); + Article articleClone = new Article(article); + + if (articleClone.unread) { + articleClone.unread = false; + saveArticleUnread(articleClone); + + Application.getArticlesModel().updateById(articleClone); } if (!getSupportActionBar().isShowing()) getSupportActionBar().show(); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryActivity.java index c527730b..0a19702e 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryActivity.java @@ -12,62 +12,65 @@ import android.view.Window; import android.widget.PopupMenu; import android.widget.ProgressBar; +import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsControllerCompat; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; -import androidx.viewpager.widget.ViewPager; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.PagerSnapHelper; +import androidx.viewpager2.widget.ViewPager2; -import com.ToxicBakery.viewpager.transforms.DepthPageTransformer; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.fox.ttrss.types.GalleryEntry; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; +import org.fox.ttrss.util.DiffFragmentStateAdapter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; -import me.relex.circleindicator.CircleIndicator; +import me.relex.circleindicator.CircleIndicator2; +import me.relex.circleindicator.CircleIndicator3; public class GalleryActivity extends CommonActivity { private final String TAG = this.getClass().getSimpleName(); - protected ArrayList<GalleryEntry> m_items = new ArrayList<>(); protected String m_title; private ArticleImagesPagerAdapter m_adapter; public String m_content; - private ViewPager m_pager; // TODO replace with viewpager2 + private ViewPager2 m_pager; // TODO replace with viewpager2 private ProgressBar m_checkProgress; + private boolean m_firstWasSelected; - private static class ArticleImagesPagerAdapter extends FragmentStatePagerAdapter { - private final List<GalleryEntry> m_items; + private static class GalleryEntryDiffItemCallback extends DiffUtil.ItemCallback<GalleryEntry> { - public ArticleImagesPagerAdapter(FragmentManager fm, List<GalleryEntry> items) { - super(fm); - m_items = items; + @Override + public boolean areItemsTheSame(@NonNull GalleryEntry oldItem, @NonNull GalleryEntry newItem) { + return oldItem.url.equals(newItem.url); } @Override - public int getCount() { - return m_items.size(); + public boolean areContentsTheSame(@NonNull GalleryEntry oldItem, @NonNull GalleryEntry newItem) { + return oldItem.url.equals(newItem.url) && oldItem.type.equals(newItem.type); } + } - @Override - public Fragment getItem(int position) { + private static class ArticleImagesPagerAdapter extends DiffFragmentStateAdapter<GalleryEntry> { + protected ArticleImagesPagerAdapter(FragmentActivity fragmentActivity, DiffUtil.ItemCallback<GalleryEntry> diffCallback) { + super(fragmentActivity, diffCallback); + } - //Log.d(TAG, "getItem: " + position + " " + m_urls.get(position)); + @Override + public Fragment createFragment(int position) { - GalleryEntry item = m_items.get(position); + GalleryEntry item = getItem(position); switch (item.type) { case TYPE_IMAGE: { @@ -87,161 +90,16 @@ public class GalleryActivity extends CommonActivity { } } - private static class MediaProgressResult { - GalleryEntry item; - int position; - int count; - - public MediaProgressResult(GalleryEntry item, int position, int count) { - this.item = item; - this.position = position; - this.count = count; - } - } - - private class MediaCheckTask extends AsyncTask<List<GalleryEntry>, MediaProgressResult, List<GalleryEntry>> { - - private final List<GalleryEntry> m_checkedItems = new ArrayList<>(); - - @Override - protected List<GalleryEntry> doInBackground(List<GalleryEntry>... params) { - - ArrayList<GalleryEntry> items = new ArrayList<>(params[0]); - int position = 0; - - for (GalleryEntry item : items) { - if (!isCancelled()) { - ++position; - - Log.d(TAG, "checking: " + item.url + " " + item.coverUrl); - - if (item.type == GalleryEntry.GalleryEntryType.TYPE_IMAGE) { - try { - Bitmap bmp = Glide.with(GalleryActivity.this) - .load(item.url) - .asBitmap() - .skipMemoryCache(false) - .diskCacheStrategy(DiskCacheStrategy.ALL) - //.dontTransform() - .into(HeadlinesFragment.FLAVOR_IMG_MIN_SIZE, HeadlinesFragment.FLAVOR_IMG_MIN_SIZE) - .get(); - - if (bmp.getWidth() >= HeadlinesFragment.FLAVOR_IMG_MIN_SIZE && bmp.getHeight() >= HeadlinesFragment.FLAVOR_IMG_MIN_SIZE) { - m_checkedItems.add(item); - publishProgress(new MediaProgressResult(item, position, items.size())); - } - - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } catch (OutOfMemoryError e) { - e.printStackTrace(); - } - - } else { - m_checkedItems.add(item); - publishProgress(new MediaProgressResult(item, position, items.size())); - } - } - } - - return m_checkedItems; - } - } - - boolean collectGalleryContents(String imgSrcFirst, Document doc, List<GalleryEntry> uncheckedItems ) { - Elements elems = doc.select("img,video"); - - boolean firstFound = false; - - for (Element elem : elems) { - - GalleryEntry item = new GalleryEntry(); - - if ("video".equalsIgnoreCase(elem.tagName())) { - String cover = elem.attr("poster"); - - Element source = elem.select("source").first(); - - if (source != null) { - String src = source.attr("src"); - - if (!src.isEmpty()) { - //Log.d(TAG, "vid/src=" + src); - - if (src.startsWith("//")) { - src = "https:" + src; - } - - if (imgSrcFirst.equals(src)) - firstFound = true; - - try { - Uri checkUri = Uri.parse(src); - - if (!"data".equalsIgnoreCase(checkUri.getScheme())) { - item.url = src; - item.coverUrl = cover; - item.type = GalleryEntry.GalleryEntryType.TYPE_VIDEO; - } - - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - } else { - String src = elem.attr("src"); - - if (!src.isEmpty()) { - if (src.startsWith("//")) { - src = "https:" + src; - } - - if (imgSrcFirst.equals(src)) - firstFound = true; - - Log.d(TAG, "img/fir=" + imgSrcFirst + ";"); - Log.d(TAG, "img/src=" + src + "; ff=" + firstFound); - - try { - Uri checkUri = Uri.parse(src); - - if (!"data".equalsIgnoreCase(checkUri.getScheme())) { - item.url = src; - item.type = GalleryEntry.GalleryEntryType.TYPE_IMAGE; - } - - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - if ((firstFound || imgSrcFirst.isEmpty()) && item.url != null) { - if (m_items.isEmpty()) - m_items.add(item); - else - uncheckedItems.add(item); - } - } - - return firstFound; - } - public void onSaveInstanceState(Bundle out) { super.onSaveInstanceState(out); - out.putParcelableArrayList("m_items", m_items); out.putString("m_title", m_title); out.putString("m_content", m_content); } @Override public void onCreate(Bundle savedInstanceState) { - ActivityCompat.postponeEnterTransition(this); + // ActivityCompat.postponeEnterTransition(this); // we use that before parent onCreate so let's init locally m_prefs = PreferenceManager @@ -263,102 +121,120 @@ public class GalleryActivity extends CommonActivity { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().hide(); - ArrayList<GalleryEntry> uncheckedItems = new ArrayList<>(); + setTitle(m_title); + + m_adapter = new ArticleImagesPagerAdapter(this, new GalleryEntryDiffItemCallback()); + + m_pager = findViewById(R.id.gallery_pager); + m_pager.setAdapter(m_adapter); + + m_checkProgress = findViewById(R.id.gallery_check_progress); if (savedInstanceState == null) { m_title = getIntent().getStringExtra("title"); m_content = getIntent().getStringExtra("content"); + } else { + // ArrayList<GalleryEntry> list = savedInstanceState.getParcelableArrayList("m_items"); + m_title = savedInstanceState.getString("m_title"); + m_content = savedInstanceState.getString("m_content"); + } - String imgSrcFirst = getIntent().getStringExtra("firstSrc"); + GalleryModel model = new ViewModelProvider(this).get(GalleryModel.class); - Document doc = Jsoup.parse(m_content); + // this should be dealt with first so that transition completes properly + String firstSrc = getIntent().getStringExtra("firstSrc"); - // if we were unable to find first image, try again for all media content so that - // gallery doesn't lock up because of a pending shared transition - if (!collectGalleryContents(imgSrcFirst, doc, uncheckedItems)) - if (!collectGalleryContents("", doc, uncheckedItems)) - m_items.add(new GalleryEntry(imgSrcFirst, GalleryEntry.GalleryEntryType.TYPE_IMAGE, null)); - } else { - ArrayList<GalleryEntry> list = savedInstanceState.getParcelableArrayList("m_items"); + /* but what about videos? if (firstSrc != null) { + List<GalleryEntry> initialItems = new ArrayList<GalleryEntry>(); - m_items.clear(); - m_items.addAll(list); + initialItems.add(0, new GalleryEntry(firstSrc, GalleryEntry.GalleryEntryType.TYPE_IMAGE, null)); - m_title = savedInstanceState.getString("m_title"); - m_content = savedInstanceState.getString("m_content"); - } + m_adapter.submitList(initialItems); - findViewById(R.id.gallery_overflow).setOnClickListener(v -> { - PopupMenu popup = new PopupMenu(GalleryActivity.this, v); - MenuInflater inflater = popup.getMenuInflater(); - inflater.inflate(R.menu.content_gallery_entry, popup.getMenu()); + model.update(initialItems); + } */ - final GalleryEntry entry = m_items.get(m_pager.getCurrentItem()); + model.collectItems(m_content, firstSrc); - popup.getMenu().findItem(R.id.article_img_share) - .setVisible(entry.type == GalleryEntry.GalleryEntryType.TYPE_IMAGE); + model.getItemsToCheck().observe(this, itemsToCheck -> { + Log.d(TAG, "observed items to check=" + itemsToCheck); - popup.setOnMenuItemClickListener(item -> onImageMenuItemSelected(item, entry)); + m_checkProgress.setMax(itemsToCheck); + m_checkProgress.setProgress(0); + }); - popup.show(); + model.getIsChecking().observe(this, isChecking -> { + Log.d(TAG, "observed isChecking=" + isChecking); + m_checkProgress.setVisibility(isChecking ? View.VISIBLE : View.GONE); }); - setTitle(m_title); + model.getCheckProgress().observe(this, progress -> { + Log.d(TAG, "observed item check progress=" + progress); + + m_checkProgress.setProgress(progress); + }); - m_adapter = new ArticleImagesPagerAdapter(getSupportFragmentManager(), m_items); + model.getItems().observe(this, galleryEntries -> { + Log.d(TAG, "observed gallery entries=" + galleryEntries + " firstSrc=" + firstSrc); - m_pager = findViewById(R.id.gallery_pager); - m_pager.setAdapter(m_adapter); - m_pager.setPageTransformer(true, new DepthPageTransformer()); + m_adapter.submitList(galleryEntries, () -> { + if (!m_firstWasSelected) { + for (GalleryEntry entry : galleryEntries) { + if (entry.url.equals(firstSrc)) { + int position = galleryEntries.indexOf(entry); + + Log.d(TAG, "selecting first src=" + firstSrc + " pos=" + position); + m_pager.setCurrentItem(position); + + m_firstWasSelected = true; + break; + } + } + } + }); + }); - CircleIndicator indicator = findViewById(R.id.gallery_pager_indicator); + CircleIndicator3 indicator = findViewById(R.id.gallery_pager_indicator); indicator.setViewPager(m_pager); - m_adapter.registerDataSetObserver(indicator.getDataSetObserver()); - m_checkProgress = findViewById(R.id.gallery_check_progress); + m_adapter.registerAdapterDataObserver(indicator.getAdapterDataObserver()); - Log.d(TAG, "items to check:" + uncheckedItems.size()); - MediaCheckTask mct = new MediaCheckTask() { - @Override - protected void onProgressUpdate(MediaProgressResult... result) { - //m_items.add(result[0].item); - m_adapter.notifyDataSetChanged(); + findViewById(R.id.gallery_overflow).setOnClickListener(v -> { + try { + GalleryEntry entry = m_adapter.getCurrentList().get(m_pager.getCurrentItem()); - if (result[0].position < result[0].count) { - m_checkProgress.setVisibility(View.VISIBLE); - m_checkProgress.setMax(result[0].count); - m_checkProgress.setProgress(result[0].position); - } else { - m_checkProgress.setVisibility(View.GONE); - } + PopupMenu popup = new PopupMenu(GalleryActivity.this, v); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.content_gallery_entry, popup.getMenu()); - } + popup.getMenu().findItem(R.id.article_img_share) + .setVisible(entry.type == GalleryEntry.GalleryEntryType.TYPE_IMAGE); - @Override - protected void onPostExecute(List<GalleryEntry> result) { - m_items.addAll(result); - m_adapter.notifyDataSetChanged(); - } - }; + popup.setOnMenuItemClickListener(item -> onImageMenuItemSelected(item, entry)); - mct.execute(uncheckedItems); + popup.show(); + } catch (IndexOutOfBoundsException e) { + e.printStackTrace(); + } + }); } @Override public boolean onContextItemSelected(MenuItem item) { int position = m_pager.getCurrentItem(); - GalleryEntry entry = m_items.get(position); - - //String url = m_items.get(position).url; - + try { + GalleryEntry entry = m_adapter.getCurrentList().get(position); + if (onImageMenuItemSelected(item, entry)) + return true; - if (onImageMenuItemSelected(item, entry)) - return true; + } catch (IndexOutOfBoundsException e) { + e.printStackTrace(); + } return super.onContextItemSelected(item); } diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryImageFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryImageFragment.java index 8f9d5783..db141427 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryImageFragment.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryImageFragment.java @@ -1,5 +1,6 @@ package org.fox.ttrss; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -8,15 +9,17 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ProgressBar; +import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.view.ViewCompat; import com.bogdwellers.pinchtozoom.ImageMatrixTouchHandler; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.target.GlideDrawableImageViewTarget; +import com.bumptech.glide.request.target.DrawableImageViewTarget; import com.bumptech.glide.request.target.Target; public class GalleryImageFragment extends GalleryBaseFragment { @@ -56,33 +59,34 @@ public class GalleryImageFragment extends GalleryBaseFragment { final ProgressBar progressBar = view.findViewById(R.id.flavor_image_progress); final View errorMessage = view.findViewById(R.id.flavor_image_error); - final GlideDrawableImageViewTarget glideImage = new GlideDrawableImageViewTarget(imgView); + // final GlideDrawableImageViewTarget glideImage = new GlideDrawableImageViewTarget(imgView); - Glide.with(getContext()) + Glide.with(this) .load(m_url) - //.dontAnimate() .diskCacheStrategy(DiskCacheStrategy.ALL) .skipMemoryCache(false) - .listener(new RequestListener<String, GlideDrawable>() { + .listener(new RequestListener<Drawable>() { @Override - public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) { + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) { progressBar.setVisibility(View.GONE); errorMessage.setVisibility(View.VISIBLE); - ActivityCompat.startPostponedEnterTransition(m_activity); + // ActivityCompat.startPostponedEnterTransition(m_activity); + return false; } @Override - public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) { + public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { progressBar.setVisibility(View.GONE); errorMessage.setVisibility(View.GONE); - ActivityCompat.startPostponedEnterTransition(m_activity); + // ActivityCompat.startPostponedEnterTransition(m_activity); + return false; } }) - .into(glideImage); + .into(new DrawableImageViewTarget(imgView)); return view; } diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryModel.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryModel.java new file mode 100644 index 00000000..750f85cd --- /dev/null +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryModel.java @@ -0,0 +1,178 @@ +package org.fox.ttrss; + +import android.app.Application; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; + +import org.fox.ttrss.types.GalleryEntry; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class GalleryModel extends AndroidViewModel { + private final String TAG = this.getClass().getSimpleName(); + + private MutableLiveData<List<GalleryEntry>> m_items = new MutableLiveData<>(new ArrayList<>()); + private MutableLiveData<Integer> m_checkProgress = new MutableLiveData<>(Integer.valueOf(0)); + private MutableLiveData<Integer> m_itemsToCheck = new MutableLiveData<>(Integer.valueOf(0)); + private MutableLiveData<Boolean> m_isChecking = new MutableLiveData<>(Boolean.valueOf(false)); + + public GalleryModel(@NonNull Application application) { + super(application); + } + + public LiveData<List<GalleryEntry>> getItems() { + return m_items; + } + + private ExecutorService m_executor = Executors.newSingleThreadExecutor(); + private Handler m_mainHandler = new Handler(Looper.getMainLooper()); + + public LiveData<Integer> getItemsToCheck() { + return m_itemsToCheck; + } + + public LiveData<Integer> getCheckProgress() { + return m_checkProgress; + } + + private boolean isDataUri(String src) { + try { + Uri uri = Uri.parse(src); + + return "data".equalsIgnoreCase(uri.getScheme()); + + } catch (Exception e) { + e.printStackTrace(); + } + + return false; + } + + public void update(List<GalleryEntry> items) { + m_items.postValue(items); + } + + public LiveData<Boolean> getIsChecking() { + return m_isChecking; + } + + public void collectItems(String articleText, String srcFirst) { + m_executor.execute(() -> { + + Document doc = Jsoup.parse(articleText); + + List<GalleryEntry> checkList = new ArrayList<>(); + + Log.d(TAG, "looking for srcFirst=" + srcFirst); + + Elements elems = doc.select("img,video"); + + m_itemsToCheck.postValue(elems.size()); + + int currentItem = 0; + boolean firstFound = false; + + m_isChecking.postValue(true); + + for (Element elem : elems) { + ++currentItem; + + if ("video".equalsIgnoreCase(elem.tagName())) { + Element source = elem.select("source").first(); + String poster = elem.attr("abs:poster"); + + if (source != null) { + String src = source.attr("abs:src"); + + Log.d(TAG, "checking vid src=" + src + " poster=" + poster); + + if (src != null && src.equals(srcFirst)) { + Log.d(TAG, "first item found, vid=" + src); + + firstFound = true; + + GalleryEntry item = new GalleryEntry(src, GalleryEntry.GalleryEntryType.TYPE_VIDEO, poster); + + checkList.add(item); + + m_items.postValue(checkList); + } else { + if (!isDataUri(src)) { + checkList.add(new GalleryEntry(src, GalleryEntry.GalleryEntryType.TYPE_VIDEO, poster)); + m_items.postValue(checkList); + } + } + } + } else { + String src = elem.attr("abs:src"); + + Log.d(TAG, "checking img src=" + src); + + if (src != null && src.equals(srcFirst)) { + Log.d(TAG, "first item found, img=" + src); + + firstFound = true; + + GalleryEntry item = new GalleryEntry(src, GalleryEntry.GalleryEntryType.TYPE_IMAGE, null); + + checkList.add(item); + + m_items.postValue(checkList); + } else { + if (!isDataUri(src)) { + Log.d(TAG, "checking image with glide: " + src); + + try { + Bitmap bmp = Glide.with(getApplication().getApplicationContext()) + .asBitmap() + .load(src) + .skipMemoryCache(false) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .into(HeadlinesFragment.FLAVOR_IMG_MIN_SIZE, HeadlinesFragment.FLAVOR_IMG_MIN_SIZE) + .get(); + + if (bmp != null && bmp.getWidth() >= HeadlinesFragment.FLAVOR_IMG_MIN_SIZE && bmp.getHeight() >= HeadlinesFragment.FLAVOR_IMG_MIN_SIZE) { + Log.d(TAG, "image matches gallery criteria, adding..."); + + checkList.add(new GalleryEntry(src, GalleryEntry.GalleryEntryType.TYPE_IMAGE, null)); + m_items.postValue(checkList); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + m_checkProgress.postValue(currentItem); + } + + // if we didn't find it in the document, let's add insert to the list anyway so shared transition + // would hopefully work + if (!firstFound) { + checkList.add(0, new GalleryEntry(srcFirst, GalleryEntry.GalleryEntryType.TYPE_IMAGE, null)); + m_items.postValue(checkList); + } + + m_isChecking.postValue(false); + }); + } +} diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryVideoFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryVideoFragment.java index a96e59b5..a171fb5b 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryVideoFragment.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/GalleryVideoFragment.java @@ -26,7 +26,6 @@ public class GalleryVideoFragment extends GalleryBaseFragment { String m_url; String m_coverUrl; MediaPlayer m_mediaPlayer; - private boolean m_userVisibleHint = false; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -57,87 +56,35 @@ public class GalleryVideoFragment extends GalleryBaseFragment { registerForContextMenu(imgView); - /*final GlideDrawableImageViewTarget glideImage = new GlideDrawableImageViewTarget(imgView); + // ActivityCompat.startPostponedEnterTransition(m_activity); - Glide.with(this) - .load(m_coverUrl) - //.dontAnimate() - .diskCacheStrategy(DiskCacheStrategy.ALL) - .skipMemoryCache(false) - .listener(new RequestListener<String, GlideDrawable>() { - @Override - public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) { - ActivityCompat.startPostponedEnterTransition(m_activity); - - initializeVideoPlayer(view); - return false; - } - - @Override - public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) { - ActivityCompat.startPostponedEnterTransition(m_activity); - - initializeVideoPlayer(view); - return false; - } - }) - .into(glideImage); */ + view.findViewById(R.id.flavor_image_progress).setVisibility(View.VISIBLE); - ActivityCompat.startPostponedEnterTransition(m_activity); initializeVideoPlayer(view); return view; } - @Override - public void setUserVisibleHint(boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - m_userVisibleHint = isVisibleToUser; - - Log.d(TAG, "setUserVisibleHint: " + isVisibleToUser); - - if (getView() == null) return; - - try { - - if (isVisibleToUser) { - if (m_mediaPlayer != null && !m_mediaPlayer.isPlaying()) { - m_mediaPlayer.start(); - } - - } else { - if (m_mediaPlayer != null && m_mediaPlayer.isPlaying()) { - m_mediaPlayer.pause(); - } - } - } catch (IllegalStateException e) { - e.printStackTrace(); - } - - } - private void initializeVideoPlayer(final View view) { - - //Log.d(TAG, "initializeVideoPlayer: " + m_activity + " " + view); - - - final MediaController m_mediaController = new MediaController(m_activity); + final MediaController mediaController = new MediaController(m_activity); final TextureView textureView = view.findViewById(R.id.flavor_video); registerForContextMenu(textureView); textureView.setOnClickListener(v -> { try { - if (!m_mediaController.isShowing()) - m_mediaController.show(5000); + + if (mediaController.isShowing()) + mediaController.hide(); else - m_mediaController.hide(); + mediaController.show(); + } catch (Exception e) { e.printStackTrace(); } }); - m_mediaController.setAnchorView(textureView); + mediaController.setAnchorView(textureView); textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override @@ -146,7 +93,7 @@ public class GalleryVideoFragment extends GalleryBaseFragment { m_mediaPlayer = new MediaPlayer(); - m_mediaController.setMediaPlayer(new MediaController.MediaPlayerControl() { + mediaController.setMediaPlayer(new MediaController.MediaPlayerControl() { @Override public void start() { m_mediaPlayer.start(); @@ -213,16 +160,13 @@ public class GalleryVideoFragment extends GalleryBaseFragment { } m_mediaPlayer.setOnPreparedListener(mp -> { - getView().findViewById(R.id.flavor_image).setVisibility(View.GONE); - getView().findViewById(R.id.flavor_image_progress).setVisibility(View.GONE); + view.findViewById(R.id.flavor_image_progress).setVisibility(View.GONE); try { resizeSurface(textureView); mp.setLooping(true); - if (m_userVisibleHint) { - mp.start(); - } + mp.start(); } catch (IllegalStateException e) { e.printStackTrace(); } diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java index 8c24a14a..e20fbcae 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragment.java @@ -6,9 +6,8 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; -import android.graphics.Paint; import android.graphics.Point; -import android.graphics.SurfaceTexture; +import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.media.MediaPlayer; import android.net.ConnectivityManager; @@ -22,6 +21,7 @@ import android.transition.Fade; import android.transition.Transition; import android.util.DisplayMetrics; import android.util.Log; +import android.util.Size; import android.util.TypedValue; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; @@ -29,7 +29,6 @@ import android.view.Display; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.Surface; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; @@ -38,17 +37,17 @@ import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.CheckBox; import android.widget.EditText; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ListView; import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.TextView; -import androidx.core.app.ActivityCompat; -import androidx.core.app.ActivityOptionsCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; +import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.ItemTouchHelper; @@ -60,12 +59,17 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.amulyakhare.textdrawable.TextDrawable; import com.amulyakhare.textdrawable.util.ColorGenerator; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.target.GlideDrawableImageViewTarget; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.DrawableImageViewTarget; +import com.bumptech.glide.request.target.SimpleTarget; import com.bumptech.glide.request.target.Target; import com.google.android.material.button.MaterialButton; +import com.google.android.material.card.MaterialCardView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; @@ -81,12 +85,10 @@ import org.jsoup.nodes.Element; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; -import java.util.HashMap; +import java.util.List; import java.util.TimeZone; import java.util.stream.Collectors; -import jp.wasabeef.glide.transformations.CropCircleTransformation; - public class HeadlinesFragment extends androidx.fragment.app.Fragment { private boolean m_isLazyLoading; @@ -99,11 +101,13 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { public enum ArticlesSelection { ALL, NONE, UNREAD } public static final int FLAVOR_IMG_MIN_SIZE = 128; - public static final int THUMB_IMG_MIN_SIZE = 32; private final String TAG = this.getClass().getSimpleName(); private Feed m_feed; + + /** TODO this should be stored in model, either as an observable or a field - article.active or something */ + @Deprecated private int m_activeArticleId; private String m_searchQuery = ""; @@ -117,12 +121,11 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { private boolean m_compactLayoutMode = false; private RecyclerView m_list; private LinearLayoutManager m_layoutManager; + private HeadlinesFragmentModel m_headlinesFragmentModel; private MediaPlayer m_mediaPlayer; private TextureView m_activeTexture; - protected static HashMap<Integer, Integer> m_flavorHeightsCache = new HashMap<>(); - public ArticleList getSelectedArticles() { return Application.getArticles() .stream() @@ -130,11 +133,6 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { } public void initialize(Feed feed) { - - // clear loaded headlines before switching feed - if (feed != m_feed) - Application.getArticlesModel().update(new ArticleList()); - m_feed = feed; } @@ -156,9 +154,13 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { m_activity.editArticleNote(article); return true; } else if (itemId == R.id.headlines_article_unread) { - article.unread = !article.unread; - m_activity.saveArticleUnread(article); - m_adapter.notifyItemChanged(position); + Article articleClone = new Article(article); + articleClone.unread = !articleClone.unread; + + m_activity.saveArticleUnread(articleClone); + + Application.getArticlesModel().updateById(articleClone); + return true; } else if (itemId == R.id.headlines_article_link_copy) { m_activity.copyToClipboard(article.link); @@ -167,11 +169,13 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { m_activity.openUri(Uri.parse(article.link)); if (article.unread) { - article.unread = false; - m_activity.saveArticleUnread(article); + Article articleClone = new Article(article); + articleClone.unread = !articleClone.unread; - m_adapter.notifyItemChanged(position); - } + m_activity.saveArticleUnread(articleClone); + + Application.getArticlesModel().updateById(articleClone); + } return true; } else if (itemId == R.id.headlines_share_article) { m_activity.shareArticle(article); @@ -195,20 +199,21 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { } private void catchupAbove(Article article) { + ArticleList tmp = new ArticleList(); ArticleList articles = Application.getArticles(); + for (Article a : articles) { if (article.equalsById(a)) break; if (a.unread) { - a.unread = false; - tmp.add(a); + Article articleClone = new Article(a); - int position = articles.getPositionById(a.id); + articleClone.unread = false; + tmp.add(articleClone); - if (position != -1) - m_adapter.notifyItemChanged(position); + Application.getArticlesModel().updateById(articleClone); } } @@ -294,6 +299,8 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.d(TAG, "onCreateView"); + m_headlinesFragmentModel = new ViewModelProvider(this).get(HeadlinesFragmentModel.class); + String headlineMode = m_prefs.getString("headline_mode", "HL_DEFAULT"); if ("HL_COMPACT".equals(headlineMode) || "HL_COMPACT_NOIMAGES".equals(headlineMode)) @@ -418,12 +425,11 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { m_activity.setArticlesUnread(m_readArticles, Article.UPDATE_SET_FALSE); for (Article a : m_readArticles) { - a.unread = false; + Article articleClone = new Article(a); - int position = Application.getArticles().getPositionById(a.id); + articleClone.unread = false; - if (position != -1) - m_adapter.notifyItemChanged(position); + Application.getArticlesModel().updateById(articleClone); } m_readArticles.clear(); @@ -585,7 +591,7 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { return; if (!append) - m_activeArticleId = -1; + setActiveArticleId(-1); model.setSearchQuery(getSearchQuery()); model.startLoading(append, m_feed, m_activity.getResizeWidth()); @@ -615,8 +621,6 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { public View flavorImageOverflow; public TextureView flavorVideoView; public MaterialButton attachmentsView; - public ProgressTarget<String, GlideDrawable> flavorProgressTarget; - int articleId; public TextView linkHost; public ArticleViewHolder(View v) { @@ -624,16 +628,6 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { view = v; - view.getViewTreeObserver().addOnPreDrawListener(() -> { - View flavorImage = view.findViewById(R.id.flavor_image); - - if (flavorImage != null) { - HeadlinesFragment.m_flavorHeightsCache.put(articleId, flavorImage.getMeasuredHeight()); - } - - return true; - }); - titleView = v.findViewById(R.id.title); feedTitleView = v.findViewById(R.id.feed_title); @@ -647,8 +641,8 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { dateView = v.findViewById(R.id.date); selectionBoxView = v.findViewById(R.id.selected); menuButtonView = v.findViewById(R.id.article_menu_button); - flavorImageHolder = v.findViewById(R.id.flavorImageHolder); - flavorImageLoadingBar = v.findViewById(R.id.flavorImageLoadingBar); + flavorImageHolder = v.findViewById(R.id.flavor_image_holder); + flavorImageLoadingBar = v.findViewById(R.id.flavor_image_progressbar); textImage = v.findViewById(R.id.text_image); textChecked = v.findViewById(R.id.text_checked); headlineHeader = v.findViewById(R.id.headline_header); @@ -656,23 +650,15 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { flavorVideoView = v.findViewById(R.id.flavor_video); attachmentsView = v.findViewById(R.id.attachments); linkHost = v.findViewById(R.id.link_host); - - if (flavorImageView != null && flavorImageLoadingBar != null) { - flavorProgressTarget = new FlavorProgressTarget<>(new GlideDrawableImageViewTarget(flavorImageView), flavorImageLoadingBar); - } - - } - - public void clearAnimation() { - view.clearAnimation(); } } private static class FlavorProgressTarget<Z> extends ProgressTarget<String, Z> { - private final ProgressBar progress; - public FlavorProgressTarget(Target<Z> target, ProgressBar progress) { + private final ArticleViewHolder holder; + public FlavorProgressTarget(Target<Z> target, String model, ArticleViewHolder holder) { super(target); - this.progress = progress; + setModel(model); + this.holder = holder; } @Override public float getGranualityPercentage() { @@ -680,43 +666,57 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { } @Override protected void onConnecting() { - progress.setIndeterminate(true); - progress.setVisibility(View.VISIBLE); + holder.flavorImageHolder.setVisibility(View.VISIBLE); + + holder.flavorImageLoadingBar.setIndeterminate(true); + holder.flavorImageLoadingBar.setVisibility(View.VISIBLE); } @Override protected void onDownloading(long bytesRead, long expectedLength) { - progress.setIndeterminate(false); - progress.setProgress((int)(100 * bytesRead / expectedLength)); + holder.flavorImageHolder.setVisibility(View.VISIBLE); + + holder.flavorImageLoadingBar.setIndeterminate(false); + holder.flavorImageLoadingBar.setProgress((int)(100 * bytesRead / expectedLength)); } @Override protected void onDownloaded() { - progress.setIndeterminate(true); + holder.flavorImageHolder.setVisibility(View.VISIBLE); + + holder.flavorImageLoadingBar.setIndeterminate(true); } @Override protected void onDelivered() { - progress.setVisibility(View.INVISIBLE); + holder.flavorImageHolder.setVisibility(View.VISIBLE); + + holder.flavorImageLoadingBar.setVisibility(View.INVISIBLE); } } private class ArticleListAdapter extends ListAdapter<Article, ArticleViewHolder> { public static final int VIEW_NORMAL = 0; - public static final int VIEW_UNREAD = 1; - public static final int VIEW_ACTIVE = 2; - public static final int VIEW_ACTIVE_UNREAD = 3; - public static final int VIEW_AMR_FOOTER = 4; - - public static final int VIEW_COUNT = VIEW_AMR_FOOTER + 1; - - private final Integer[] origTitleColors = new Integer[VIEW_COUNT]; + public static final int VIEW_AMR_FOOTER = 1; private final ColorGenerator m_colorGenerator = ColorGenerator.DEFAULT; private final TextDrawable.IBuilder m_drawableBuilder = TextDrawable.builder().round(); - - boolean flavorImageEnabled; + private final ColorStateList m_cslTertiary; + private final ColorStateList m_cslPrimary; + private final int m_colorSurfaceContainerLowest; + private final int m_colorSurface; + private final int m_colorPrimary; + private final int m_colorTertiary; + private final int m_colorSecondary; + private final int m_colorOnSurface; + private final int m_colorTertiaryContainer; + private final int m_colorOnTertiaryContainer; + + boolean m_flavorImageEnabled; + private final int m_screenWidth; private final int m_screenHeight; - private int m_lastAddedPosition; + + private final int m_headlineSmallFontSize; + private final int m_headlineFontSize; private final ConnectivityManager m_cmgr; private boolean canShowFlavorImage() { - if (flavorImageEnabled) { + if (m_flavorImageEnabled) { if (m_prefs.getBoolean("headline_images_wifi_only", false)) { // why do i have to get this service every time instead of using a member variable :( NetworkInfo wifi = m_cmgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI); @@ -732,6 +732,12 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { return false; } + private int colorFromAttr(int attr) { + TypedValue tv = new TypedValue(); + m_activity.getTheme().resolveAttribute(attr, tv, true); + return ContextCompat.getColor(m_activity, tv.resourceId); + } + public ArticleListAdapter() { super(new ArticleDiffItemCallback()); @@ -739,406 +745,428 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { Point size = new Point(); display.getSize(size); m_screenHeight = size.y; + m_screenWidth = size.x; String headlineMode = m_prefs.getString("headline_mode", "HL_DEFAULT"); - flavorImageEnabled = "HL_DEFAULT".equals(headlineMode) || "HL_COMPACT".equals(headlineMode); - - m_cmgr = (ConnectivityManager) m_activity.getSystemService(Context.CONNECTIVITY_SERVICE); - } + m_flavorImageEnabled = "HL_DEFAULT".equals(headlineMode) || "HL_COMPACT".equals(headlineMode); - @Override - public ArticleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + m_colorPrimary = colorFromAttr(R.attr.colorPrimary); + m_colorSecondary = colorFromAttr(R.attr.colorSecondary); + m_colorTertiary = colorFromAttr(R.attr.colorTertiary); - int layoutId = m_compactLayoutMode ? R.layout.headlines_row_compact : R.layout.headlines_row; + m_cslTertiary = ColorStateList.valueOf(m_colorTertiary); + m_cslPrimary = ColorStateList.valueOf(m_colorPrimary); - switch (viewType) { - case VIEW_AMR_FOOTER: - layoutId = R.layout.headlines_footer; - break; - case VIEW_UNREAD: - layoutId = m_compactLayoutMode ? R.layout.headlines_row_compact_unread : R.layout.headlines_row_unread; - break; - case VIEW_ACTIVE: - layoutId = m_compactLayoutMode ? R.layout.headlines_row_compact_active : R.layout.headlines_row; - break; - case VIEW_ACTIVE_UNREAD: - layoutId = m_compactLayoutMode ? R.layout.headlines_row_compact_active_unread : R.layout.headlines_row_unread; - break; - } + m_colorSurfaceContainerLowest = colorFromAttr(R.attr.colorSurfaceContainerLowest); + m_colorSurface = colorFromAttr(R.attr.colorSurface); + m_colorOnSurface = colorFromAttr(R.attr.colorOnSurface); - View v = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); + m_colorTertiaryContainer = colorFromAttr(R.attr.colorTertiaryContainer); + m_colorOnTertiaryContainer = colorFromAttr(R.attr.colorOnTertiaryContainer); - //registerForContextMenu(v); + m_headlineFontSize = m_prefs.getInt("headlines_font_size_sp_int", 13); + m_headlineSmallFontSize = Math.max(10, Math.min(18, m_headlineFontSize - 2)); - return new ArticleViewHolder(v); + m_cmgr = (ConnectivityManager) m_activity.getSystemService(Context.CONNECTIVITY_SERVICE); } + @NonNull @Override - public void onBindViewHolder(final ArticleViewHolder holder, int position) { - int headlineFontSize = m_prefs.getInt("headlines_font_size_sp_int", 13); - int headlineSmallFontSize = Math.max(10, Math.min(18, headlineFontSize - 2)); + public ArticleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - Article article = getItem(position); + int layoutId = m_compactLayoutMode ? R.layout.headlines_row_compact : R.layout.headlines_row; - holder.articleId = article.id; + if (viewType == VIEW_AMR_FOOTER) { + layoutId = R.layout.headlines_footer; + } - if (article.id == Article.TYPE_AMR_FOOTER && m_prefs.getBoolean("headlines_mark_read_scroll", false)) { - WindowManager wm = (WindowManager) m_activity.getSystemService(Context.WINDOW_SERVICE); - Display display = wm.getDefaultDisplay(); - int screenHeight = (int)(display.getHeight() * 1.5); + View v = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); - holder.view.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT, screenHeight)); - } + ArticleViewHolder holder = new ArticleViewHolder(v); - // nothing else of interest for those below anyway - if (article.id < 0) return; + // set on click handlers once when view is created + + holder.view.setOnClickListener(view -> { + int position = m_list.getChildAdapterPosition(view); - holder.view.setOnLongClickListener(v -> { - m_list.showContextMenuForChild(v); - return true; - }); + if (position != -1) { + Article article = m_adapter.getItem(position); - holder.view.setOnClickListener(v -> { - m_listener.onArticleSelected(article); + m_listener.onArticleSelected(article); - // only set active article when it makes sense (in DetailActivity) - if (getActivity() instanceof DetailActivity) { - m_activeArticleId = article.id; + // only set active article when it makes sense (in DetailActivity) + if (getActivity() instanceof DetailActivity) { + m_activeArticleId = article.id; - m_adapter.notifyItemChanged(position); + m_adapter.notifyItemChanged(position); + } } - }); + }); + + holder.view.setOnLongClickListener(view -> { + m_list.showContextMenuForChild(view); + return true; + }); // block footer clicks to make button/selection clicking easier if (holder.headlineFooter != null) { holder.headlineFooter.setOnClickListener(view -> { - // - }); + // + }); } - if (holder.textImage != null) { - updateTextCheckedState(holder, position); + if (holder.attachmentsView != null) { + holder.attachmentsView.setOnClickListener(view -> { + int position = m_list.getChildAdapterPosition(holder.view); - holder.textImage.setOnClickListener(view -> { - Article selectedArticle = getItem(position); + if (position != -1) { + Article article = m_adapter.getItem(position); + m_activity.displayAttachments(article); + } + }); + } + + if (holder.flavorImageView != null) { + holder.flavorImageView.setOnClickListener(view -> { + int position = m_list.getChildAdapterPosition(holder.view); + + if (position != -1) { + Article article = m_adapter.getItem(position); + openGalleryForType(article, holder, holder.flavorImageView); + } + }); + } - Log.d(TAG, "textImage onClick pos=" + position + " article=" + article); + if (holder.flavorImageOverflow != null) { + holder.flavorImageOverflow.setOnClickListener(view -> { + PopupMenu popup = new PopupMenu(getContext(), holder.flavorImageOverflow); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.content_gallery_entry, popup.getMenu()); + + int position = m_list.getChildAdapterPosition(holder.view); + + if (position != -1) { + Article article = m_adapter.getItem(position); + + popup.setOnMenuItemClickListener(item -> { + + Uri mediaUri = Uri.parse(article.flavorStreamUri != null ? article.flavorStreamUri : + article.flavorImageUri); + + int itemId = item.getItemId(); + if (itemId == R.id.article_img_open) { + m_activity.openUri(mediaUri); + return true; + } else if (itemId == R.id.article_img_copy) { + m_activity.copyToClipboard(mediaUri.toString()); + return true; + } else if (itemId == R.id.article_img_share) { + m_activity.shareImageFromUri(mediaUri.toString()); + return true; + } else if (itemId == R.id.article_img_share_url) { + m_activity.shareText(mediaUri.toString()); + return true; + } else if (itemId == R.id.article_img_view_caption) { + m_activity.displayImageCaption(article.flavorImageUri, article.content); + return true; + } + return false; + }); - selectedArticle.selected = !selectedArticle.selected; + popup.show(); + } + }); - updateTextCheckedState(holder, position); + holder.flavorImageView.setOnLongClickListener(view -> { + m_list.showContextMenuForChild(view); + return true; + }); + } - m_listener.onArticleListSelectionChange(); - }); - ViewCompat.setTransitionName(holder.textImage, "gallery:" + article.flavorImageUri); + if (holder.menuButtonView != null) { + holder.menuButtonView.setOnClickListener(view -> { - if (article.flavorImage != null) { + int position = m_list.getChildAdapterPosition(holder.view); - holder.textImage.setOnLongClickListener(v -> { + if (position != -1) { + PopupMenu popup = new PopupMenu(getContext(), view); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.context_headlines, popup.getMenu()); - openGalleryForType(article, holder, holder.textImage); + popup.getMenu().findItem(R.id.article_set_labels).setEnabled(m_activity.getApiLevel() >= 1); + popup.getMenu().findItem(R.id.article_edit_note).setEnabled(m_activity.getApiLevel() >= 1); - return true; - }); + popup.setOnMenuItemClickListener(item -> onArticleMenuItemSelected(item, + getItem(position), + m_list.getChildAdapterPosition(holder.view))); - } + popup.show(); + } + }); } - if (holder.titleView != null) { - holder.titleView.setText(Html.fromHtml(article.title)); - holder.titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.min(21, headlineFontSize + 3)); + if (holder.markedView != null) { + holder.markedView.setOnClickListener(view -> { + int position = m_list.getChildAdapterPosition(holder.view); - adjustTitleTextView(article.score, holder.titleView, position); + if (position != -1) { + Article article = new Article(getItem(position)); + article.marked = !article.marked; + + m_activity.saveArticleMarked(article); + + Application.getArticlesModel().updateById(article); + } + }); } - if (holder.feedTitleView != null) { - if (article.feed_title != null && m_feed != null && (m_feed.is_cat || m_feed.id < 0)) { - holder.feedTitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineSmallFontSize); - holder.feedTitleView.setText(article.feed_title); - } else { - holder.feedTitleView.setVisibility(View.GONE); - } + if (holder.selectionBoxView != null) { + holder.selectionBoxView.setOnClickListener(view -> { + int position = m_list.getChildAdapterPosition(holder.view); + + if (position != -1) { + Article article = new Article(getItem(position)); + + CheckBox cb = (CheckBox) view; + + article.selected = cb.isChecked(); + + Application.getArticlesModel().updateById(article); + + m_listener.onArticleListSelectionChange(); + } + }); } - if (holder.linkHost != null) { - if (article.isHostDistinct()) { - holder.linkHost.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineSmallFontSize); - holder.linkHost.setText(article.getLinkHost()); - holder.linkHost.setVisibility(View.VISIBLE); - } else { - holder.linkHost.setVisibility(View.GONE); - } + if (holder.publishedView != null) { + holder.publishedView.setOnClickListener(view -> { + int position = m_list.getChildAdapterPosition(holder.view); + + if (position != -1) { + Article article = new Article(getItem(position)); + article.published = !article.published; + + m_activity.saveArticlePublished(article); + + Application.getArticlesModel().updateById(article); + } + }); } - TypedValue tvTertiary = new TypedValue(); - m_activity.getTheme().resolveAttribute(R.attr.colorTertiary, tvTertiary, true); + if (holder.textImage != null) { + holder.textImage.setOnClickListener(view -> { + int position = m_list.getChildAdapterPosition(holder.view); - ColorStateList colorTertiary = ColorStateList.valueOf(ContextCompat.getColor(m_activity, tvTertiary.resourceId)); + if (position != -1) { + Article article = new Article(getItem(position)); + article.selected = !article.selected; - TypedValue tvPrimary = new TypedValue(); - m_activity.getTheme().resolveAttribute(R.attr.colorPrimary, tvPrimary, true); + Application.getArticlesModel().updateById(article); - ColorStateList colorPrimary = ColorStateList.valueOf(ContextCompat.getColor(m_activity, tvPrimary.resourceId)); + // updateTextCheckedState(holder, position); - if (holder.markedView != null) { - holder.markedView.setIconResource(article.marked ? R.drawable.baseline_star_24 : R.drawable.baseline_star_outline_24); - holder.markedView.setIconTint(article.marked ? colorTertiary : colorPrimary); + m_listener.onArticleListSelectionChange(); + } + }); + + holder.textImage.setOnLongClickListener(view -> { + int position = m_list.getChildAdapterPosition(holder.view); - holder.markedView.setOnClickListener(v -> { - Article selectedArticle = new Article(getItem(position)); - selectedArticle.marked = !selectedArticle.marked; + if (position != -1) { + Article article = getItem(position); - m_activity.saveArticleMarked(selectedArticle); - Application.getArticlesModel().update(position, selectedArticle); - }); + openGalleryForType(article, holder, holder.textImage); + } + + return true; + }); } if (holder.scoreView != null) { - int scoreDrawable = R.drawable.baseline_trending_flat_24; + if (m_activity.getApiLevel() >= 16) { + holder.scoreView.setOnClickListener(view -> { + int position = m_list.getChildAdapterPosition(holder.view); - if (article.score > 0) - scoreDrawable = R.drawable.baseline_trending_up_24; - else if (article.score < 0) - scoreDrawable = R.drawable.baseline_trending_down_24; + if (position != -1) { - holder.scoreView.setIconResource(scoreDrawable); + Article article = new Article(getItem(position)); - if (article.score > Article.SCORE_HIGH) - holder.scoreView.setIconTint(colorTertiary); - else - holder.scoreView.setIconTint(colorPrimary); + final EditText edit = new EditText(getActivity()); + edit.setText(String.valueOf(article.score)); - if (m_activity.getApiLevel() >= 16) { - holder.scoreView.setOnClickListener(v -> { - final EditText edit = new EditText(getActivity()); - edit.setText(String.valueOf(article.score)); - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()) - .setTitle(R.string.score_for_this_article) - .setPositiveButton(R.string.set_score, - (dialog, which) -> { - try { - article.score = Integer.parseInt(edit.getText().toString()); - m_activity.saveArticleScore(article); - m_adapter.notifyItemChanged(m_list.getChildAdapterPosition(holder.view)); - } catch (NumberFormatException e) { - m_activity.toast(R.string.score_invalid); - e.printStackTrace(); - } - }) - .setNegativeButton(getString(R.string.cancel), - (dialog, which) -> { }).setView(edit); - - Dialog dialog = builder.create(); - dialog.show(); - }); + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()) + .setTitle(R.string.score_for_this_article) + .setPositiveButton(R.string.set_score, + (dialog, which) -> { + try { + article.score = Integer.parseInt(edit.getText().toString()); + m_activity.saveArticleScore(article); + + Application.getArticlesModel().updateById(article); + + } catch (NumberFormatException e) { + m_activity.toast(R.string.score_invalid); + e.printStackTrace(); + } + }) + .setNegativeButton(getString(R.string.cancel), + (dialog, which) -> { + }).setView(edit); + + Dialog dialog = builder.create(); + dialog.show(); + } + }); + } else { + holder.scoreView.setVisibility(View.GONE); } } - if (holder.publishedView != null) { + return holder; + } - // otherwise we just use tinting in actionbar - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - holder.publishedView.setIconResource(article.published ? R.drawable.rss_box : R.drawable.rss); - } + @Override public void onViewRecycled(@NonNull ArticleViewHolder holder){ + super.onViewRecycled(holder); + + if (holder.flavorImageView != null) + Glide.with(HeadlinesFragment.this).clear(holder.flavorImageView); + } - holder.publishedView.setIconTint(article.published ? colorTertiary : colorPrimary); + @Override + // https://stackoverflow.com/questions/33176336/need-an-example-about-recyclerview-adapter-notifyitemchangedint-position-objec/50085835#50085835 + public void onBindViewHolder(@NonNull final ArticleViewHolder holder, final int position, final List<Object> payloads) { + if (!payloads.isEmpty()) { + Log.d(TAG, "onBindViewHolder, payloads=" + payloads + " position=" + position); + + final Article article = getItem(position); + + for (final Object pobject : payloads) { + ArticleDiffItemCallback.ChangePayload payload = (ArticleDiffItemCallback.ChangePayload) pobject; + + switch (payload) { + case UNREAD: + updateUnreadView(article, holder); + break; + case MARKED: + updateMarkedView(article, holder); + break; + case SELECTED: + updateSelectedView(article, holder); + updateTextImage(article, holder); + break; + case PUBLISHED: + updatePublishedView(article, holder); + break; + case SCORE: + updateScoreView(article, holder); + break; + } + } + } else { + super.onBindViewHolder(holder, position, payloads); + } + } - holder.publishedView.setOnClickListener(v -> { - Article selectedArticle = new Article(getItem(position)); - selectedArticle.published = !selectedArticle.published; + private void updateUnreadView(final Article article, final ArticleViewHolder holder) { + if (m_compactLayoutMode) { + holder.view.setBackgroundColor(article.unread ? m_colorSurfaceContainerLowest : 0); + } else { + MaterialCardView card = (MaterialCardView) holder.view; - m_activity.saveArticlePublished(selectedArticle); + card.setCardBackgroundColor(article.unread ? m_colorSurfaceContainerLowest : m_colorSurface); + } - Application.getArticlesModel().update(position, selectedArticle); - }); + if (holder.titleView != null) { + holder.titleView.setTypeface(null, article.unread ? Typeface.BOLD : Typeface.NORMAL); + holder.titleView.setTextColor(article.unread ? m_colorOnSurface : m_colorPrimary); } - if (holder.attachmentsView != null) { - if (article.attachments != null && !article.attachments.isEmpty()) { - holder.attachmentsView.setVisibility(View.VISIBLE); + updateActiveView(article, holder); + } - holder.attachmentsView.setOnClickListener(v -> m_activity.displayAttachments(article)); + private void updateActiveView(final Article article, final ArticleViewHolder holder) { + if (article.id == m_activeArticleId) { + holder.view.setBackgroundColor(m_colorTertiaryContainer); - } else { - holder.attachmentsView.setVisibility(View.GONE); + if (holder.titleView != null) { + holder.titleView.setTextColor(m_colorOnTertiaryContainer); } } if (holder.excerptView != null) { - if (!m_prefs.getBoolean("headlines_show_content", true)) { - holder.excerptView.setVisibility(View.GONE); - } else { - String excerpt = ""; + holder.excerptView.setTextColor(article.id == m_activeArticleId ? m_colorOnTertiaryContainer : m_colorOnSurface); + } - try { - if (article.excerpt != null) { - excerpt = article.excerpt; - } else if (article.articleDoc != null) { - excerpt = article.articleDoc.text(); + if (holder.feedTitleView != null) { + holder.feedTitleView.setTextColor(article.id == m_activeArticleId ? m_colorOnTertiaryContainer : m_colorSecondary); + } - if (excerpt.length() > CommonActivity.EXCERPT_MAX_LENGTH) - excerpt = excerpt.substring(0, CommonActivity.EXCERPT_MAX_LENGTH) + "…"; - } - } catch (Exception e) { - e.printStackTrace(); - excerpt = ""; - } + } - holder.excerptView.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineFontSize); - holder.excerptView.setText(excerpt); + @Override + public void onBindViewHolder(@NonNull final ArticleViewHolder holder, int position) { + Article article = getItem(position); - if (!excerpt.isEmpty()) { - holder.excerptView.setVisibility(View.VISIBLE); - } else { - holder.excerptView.setVisibility(View.GONE); - } + if (article.id == Article.TYPE_AMR_FOOTER && m_prefs.getBoolean("headlines_mark_read_scroll", false)) { + WindowManager wm = (WindowManager) m_activity.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + int screenHeight = (int)(display.getHeight() * 1.5); - if (!canShowFlavorImage()) { - holder.excerptView.setPadding(holder.excerptView.getPaddingLeft(), - 0, - holder.excerptView.getPaddingRight(), - holder.excerptView.getPaddingBottom()); - } - } + holder.view.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT, screenHeight)); } + // nothing else of interest for those below anyway + if (article.id < 0) return; + + updateUnreadView(article, holder); + updateTextImage(article, holder); + updateTitleView(article, holder); + updateMarkedView(article, holder); + updateScoreView(article, holder); + updatePublishedView(article, holder); + updateAttachmentsView(article, holder); + updateLinkHost(article, holder); + updateExcerptView(article, holder); + updateAuthorView(article, holder); + updateDateView(article, holder); + updateSelectedView(article, holder); + if (!m_compactLayoutMode && holder.flavorImageHolder != null) { - /* reset to default in case of convertview */ + // reset our view to default in case of recycling holder.flavorImageLoadingBar.setVisibility(View.GONE); holder.flavorImageLoadingBar.setIndeterminate(false); + holder.flavorImageView.setVisibility(View.GONE); holder.flavorVideoKindView.setVisibility(View.GONE); holder.flavorImageOverflow.setVisibility(View.GONE); holder.flavorVideoView.setVisibility(View.GONE); holder.flavorImageHolder.setVisibility(View.GONE); - Glide.clear(holder.flavorImageView); - - // this is needed if our flavor image goes behind base listview element - holder.headlineHeader.setOnClickListener(v -> { - m_listener.onArticleSelected(article); - }); - - holder.headlineHeader.setOnLongClickListener(v -> { - m_list.showContextMenuForChild(holder.view); - - return true; - }); - if (canShowFlavorImage() && article.flavorImageUri != null && holder.flavorImageView != null) { - if (holder.flavorImageOverflow != null) { - holder.flavorImageOverflow.setOnClickListener(v -> { - PopupMenu popup = new PopupMenu(getActivity(), holder.flavorImageOverflow); - MenuInflater inflater = popup.getMenuInflater(); - inflater.inflate(R.menu.content_gallery_entry, popup.getMenu()); - - popup.setOnMenuItemClickListener(item -> { - - Uri mediaUri = Uri.parse(article.flavorStreamUri != null ? article.flavorStreamUri : article.flavorImageUri); - - int itemId = item.getItemId(); - if (itemId == R.id.article_img_open) { - m_activity.openUri(mediaUri); - return true; - } else if (itemId == R.id.article_img_copy) { - m_activity.copyToClipboard(mediaUri.toString()); - return true; - } else if (itemId == R.id.article_img_share) { - m_activity.shareImageFromUri(mediaUri.toString()); - return true; - } else if (itemId == R.id.article_img_share_url) { - m_activity.shareText(mediaUri.toString()); - return true; - } else if (itemId == R.id.article_img_view_caption) { - m_activity.displayImageCaption(article.flavorImageUri, article.content); - return true; - } - return false; - }); - - popup.show(); - }); - - holder.flavorImageView.setOnLongClickListener(v -> { - m_list.showContextMenuForChild(holder.view); - return true; - }); - } + int maxImageHeight = (int) (m_screenHeight * 0.5f); - holder.flavorImageView.setVisibility(View.VISIBLE); - holder.flavorImageView.setMaxHeight((int)(m_screenHeight * 0.6f)); + // we also downsample below using glide to save RAM + holder.flavorImageView.setMaxHeight(maxImageHeight); - // only show holder if we're about to display a picture - holder.flavorImageHolder.setVisibility(View.VISIBLE); + if (m_headlinesFragmentModel.getFlavorImageSizes().containsKey(article.flavorImageUri)) { + Size size = m_headlinesFragmentModel.getFlavorImageSizes().get(article.flavorImageUri); - // prevent lower listiew entries from jumping around if this row is modified - if (m_flavorHeightsCache.containsKey(article.id)) { - int cachedHeight = m_flavorHeightsCache.get(article.id); + Log.d(TAG, "using cached resource size for " + article.flavorImageUri + " " + size.getWidth() + "x" + size.getHeight()); - if (cachedHeight > 0) { - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) holder.flavorImageView.getLayoutParams(); - lp.height = cachedHeight; + if (size.getWidth() > FLAVOR_IMG_MIN_SIZE && size.getHeight() > FLAVOR_IMG_MIN_SIZE) { + loadFlavorImage(article, holder, maxImageHeight); } - } - - holder.flavorProgressTarget.setModel(article.flavorImageUri); - - try { - - Glide.with(getContext()) - .load(article.flavorImageUri) - //.dontTransform() - .diskCacheStrategy(DiskCacheStrategy.ALL) - .skipMemoryCache(false) - .listener(new RequestListener<String, GlideDrawable>() { - @Override - public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) { - holder.flavorImageLoadingBar.setVisibility(View.GONE); - holder.flavorImageView.setVisibility(View.GONE); - - return false; - } - - @Override - public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) { - - holder.flavorImageLoadingBar.setVisibility(View.GONE); - - if (resource.getIntrinsicWidth() > FLAVOR_IMG_MIN_SIZE && resource.getIntrinsicHeight() > FLAVOR_IMG_MIN_SIZE) { - - holder.flavorImageView.setVisibility(View.VISIBLE); - holder.flavorImageOverflow.setVisibility(View.VISIBLE); - - adjustVideoKindView(holder, article); - - return false; - } else { - - holder.flavorImageOverflow.setVisibility(View.GONE); - holder.flavorImageView.setVisibility(View.GONE); - - return true; - } - } - }) - .into(holder.flavorProgressTarget); - } catch (OutOfMemoryError e) { - e.printStackTrace(); + } else { + Log.d(TAG, "checking resource size for " + article.flavorImageUri); + checkImageAndLoad(article, holder, maxImageHeight); } } - if (m_prefs.getBoolean("inline_video_player", false) && article.flavorImage != null && + /* if (m_prefs.getBoolean("inline_video_player", false) && article.flavorImage != null && "video".equalsIgnoreCase(article.flavorImage.tagName()) && article.flavorStreamUri != null) { - holder.flavorImageView.setOnLongClickListener(v -> { - releaseSurface(); - openGalleryForType(article, holder, holder.flavorImageView); - return true; - }); - holder.flavorVideoView.setOnLongClickListener(v -> { releaseSurface(); openGalleryForType(article, holder, holder.flavorImageView); @@ -1233,23 +1261,29 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { } else { holder.flavorImageView.setOnClickListener(view -> openGalleryForType(article, holder, holder.flavorImageView)); - } + } */ } + } - String articleAuthor = article.author != null ? article.author : ""; - - if (holder.authorView != null) { - holder.authorView.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineSmallFontSize); + private void updateTitleView(final Article article, final ArticleViewHolder holder) { + if (holder.titleView != null) { + holder.titleView.setText(Html.fromHtml(article.title)); + holder.titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, Math.min(21, m_headlineFontSize + 3)); + } - if (!articleAuthor.isEmpty()) { - holder.authorView.setText(getString(R.string.author_formatted, articleAuthor)); + if (holder.feedTitleView != null) { + if (article.feed_title != null && m_feed != null && (m_feed.is_cat || m_feed.id < 0)) { + holder.feedTitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, m_headlineSmallFontSize); + holder.feedTitleView.setText(article.feed_title); } else { - holder.authorView.setText(""); + holder.feedTitleView.setVisibility(View.GONE); } } + } + private void updateDateView(final Article article, final ArticleViewHolder holder) { if (holder.dateView != null) { - holder.dateView.setTextSize(TypedValue.COMPLEX_UNIT_SP, headlineSmallFontSize); + holder.dateView.setTextSize(TypedValue.COMPLEX_UNIT_SP, m_headlineSmallFontSize); Date d = new Date((long)article.updated * 1000); Date now = new Date(); @@ -1268,40 +1302,189 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { df.setTimeZone(TimeZone.getDefault()); holder.dateView.setText(df.format(d)); } + } + + private void updateAuthorView(final Article article, final ArticleViewHolder holder) { + String articleAuthor = article.author != null ? article.author : ""; + + if (holder.authorView != null) { + holder.authorView.setTextSize(TypedValue.COMPLEX_UNIT_SP, m_headlineSmallFontSize); + + if (!articleAuthor.isEmpty()) { + holder.authorView.setText(getString(R.string.author_formatted, articleAuthor)); + } else { + holder.authorView.setText(""); + } + } + } + + private void updateExcerptView(final Article article, final ArticleViewHolder holder) { + if (holder.excerptView != null) { + if (!m_prefs.getBoolean("headlines_show_content", true)) { + holder.excerptView.setVisibility(View.GONE); + } else { + String excerpt = ""; + + try { + if (article.excerpt != null) { + excerpt = article.excerpt; + } else if (article.articleDoc != null) { + excerpt = article.articleDoc.text(); + + if (excerpt.length() > CommonActivity.EXCERPT_MAX_LENGTH) + excerpt = excerpt.substring(0, CommonActivity.EXCERPT_MAX_LENGTH) + "…"; + } + } catch (Exception e) { + e.printStackTrace(); + excerpt = ""; + } + holder.excerptView.setTextSize(TypedValue.COMPLEX_UNIT_SP, m_headlineFontSize); + holder.excerptView.setText(excerpt); + if (!excerpt.isEmpty()) { + holder.excerptView.setVisibility(View.VISIBLE); + } else { + holder.excerptView.setVisibility(View.GONE); + } + } + } + } + + private void updateLinkHost(final Article article, final ArticleViewHolder holder) { + if (holder.linkHost != null) { + if (article.isHostDistinct()) { + holder.linkHost.setTextSize(TypedValue.COMPLEX_UNIT_SP, m_headlineSmallFontSize); + holder.linkHost.setText(article.getLinkHost()); + holder.linkHost.setVisibility(View.VISIBLE); + } else { + holder.linkHost.setVisibility(View.GONE); + } + } + } + + private void updateAttachmentsView(final Article article, final ArticleViewHolder holder) { + if (holder.attachmentsView != null) { + if (article.attachments != null && !article.attachments.isEmpty()) { + holder.attachmentsView.setVisibility(View.VISIBLE); + } else { + holder.attachmentsView.setVisibility(View.GONE); + } + } + } + + private void updateMarkedView(final Article article, final ArticleViewHolder holder) { + if (holder.markedView != null) { + holder.markedView.setIconResource(article.marked ? R.drawable.baseline_star_24 : R.drawable.baseline_star_outline_24); + holder.markedView.setIconTint(article.marked ? m_cslTertiary : m_cslPrimary); + } + } + + private void updateTextImage(final Article article, final ArticleViewHolder holder) { + if (holder.textImage != null) { + updateTextCheckedState(article, holder); + + ViewCompat.setTransitionName(holder.textImage, + "gallery:" + article.flavorImageUri); + } + } + + private void updateSelectedView(final Article article, final ArticleViewHolder holder) { if (holder.selectionBoxView != null) { holder.selectionBoxView.setChecked(article.selected); - holder.selectionBoxView.setOnClickListener(view -> { - Article currentArticle = getItem(position); + } + } - Log.d(TAG, "selectionCb onClick pos=" + position + " article=" + article); + private void updateScoreView(final Article article, final ArticleViewHolder holder) { + if (holder.scoreView != null) { + int scoreDrawable = R.drawable.baseline_trending_flat_24; - CheckBox cb = (CheckBox)view; + if (article.score > 0) + scoreDrawable = R.drawable.baseline_trending_up_24; + else if (article.score < 0) + scoreDrawable = R.drawable.baseline_trending_down_24; - currentArticle.selected = cb.isChecked(); + holder.scoreView.setIconResource(scoreDrawable); - m_listener.onArticleListSelectionChange(); - }); + if (article.score > Article.SCORE_HIGH) + holder.scoreView.setIconTint(m_cslTertiary); + else + holder.scoreView.setIconTint(m_cslPrimary); } + } - if (holder.menuButtonView != null) { - holder.menuButtonView.setOnClickListener(v -> { + private void updatePublishedView(final Article article, final ArticleViewHolder holder) { + if (holder.publishedView != null) { + // otherwise we just use tinting in actionbar + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + holder.publishedView.setIconResource(article.published ? R.drawable.rss_box : R.drawable.rss); + } + + holder.publishedView.setIconTint(article.published ? m_cslTertiary : m_cslPrimary); + } + } - PopupMenu popup = new PopupMenu(getActivity(), v); - MenuInflater inflater = popup.getMenuInflater(); - inflater.inflate(R.menu.context_headlines, popup.getMenu()); + private void loadFlavorImage(final Article article, final ArticleViewHolder holder, final int maxImageHeight) { + Glide.with(HeadlinesFragment.this) + .load(article.flavorImageUri) + .transition(DrawableTransitionOptions.withCrossFade()) + .override(m_screenWidth, maxImageHeight) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .skipMemoryCache(false) + .listener(new RequestListener<Drawable>() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) { + holder.flavorImageHolder.setVisibility(View.GONE); + + holder.flavorImageView.setVisibility(View.GONE); + holder.flavorImageOverflow.setVisibility(View.VISIBLE); + + return false; + } - popup.getMenu().findItem(R.id.article_set_labels).setEnabled(m_activity.getApiLevel() >= 1); - popup.getMenu().findItem(R.id.article_edit_note).setEnabled(m_activity.getApiLevel() >= 1); + @Override + public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { + holder.flavorImageHolder.setVisibility(View.VISIBLE); - popup.setOnMenuItemClickListener(item -> onArticleMenuItemSelected(item, - getItem(position), - m_list.getChildAdapterPosition(holder.view))); + holder.flavorImageView.setVisibility(View.VISIBLE); + holder.flavorImageOverflow.setVisibility(View.VISIBLE); - popup.show(); - }); - } + adjustVideoKindView(holder, article); + + return false; + } + }) + .into(new DrawableImageViewTarget(holder.flavorImageView)); + } + + private void checkImageAndLoad(final Article article, final ArticleViewHolder holder, final int maxImageHeight) { + FlavorProgressTarget<Size> flavorProgressTarget = new FlavorProgressTarget<>(new SimpleTarget<Size>() { + @Override + public void onResourceReady(@NonNull Size resource, @Nullable com.bumptech.glide.request.transition.Transition<? super Size> transition) { + Log.d(TAG, "got resource of " + resource.getWidth() + "x" + resource.getHeight()); + + m_headlinesFragmentModel.getFlavorImageSizes().put(article.flavorImageUri, resource); + + if (resource.getWidth() > FLAVOR_IMG_MIN_SIZE && resource.getHeight() > FLAVOR_IMG_MIN_SIZE) { + + // now we can actually load the image into our drawable + loadFlavorImage(article, holder, maxImageHeight); + + } else { + holder.flavorImageHolder.setVisibility(View.GONE); + + holder.flavorImageView.setVisibility(View.VISIBLE); + holder.flavorImageOverflow.setVisibility(View.VISIBLE); + } + } + }, article.flavorImageUri, holder); + + Glide.with(HeadlinesFragment.this) + .as(Size.class) + .load(article.flavorImageUri) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .skipMemoryCache(true) + .into(flavorProgressTarget); } @Override @@ -1310,20 +1493,12 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { if (a.id == Article.TYPE_AMR_FOOTER) { return VIEW_AMR_FOOTER; - } else if (a.id == m_activeArticleId && a.unread) { - return VIEW_ACTIVE_UNREAD; - } else if (a.id == m_activeArticleId) { - return VIEW_ACTIVE; - } else if (a.unread) { - return VIEW_UNREAD; } else { return VIEW_NORMAL; } } - private void updateTextCheckedState(final ArticleViewHolder holder, int position) { - Article article = getItem(position); - + private void updateTextCheckedState(final Article article, final ArticleViewHolder holder) { String tmp = !article.title.isEmpty() ? article.title.substring(0, 1).toUpperCase() : "?"; if (article.selected) { @@ -1336,27 +1511,15 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { if (!canShowFlavorImage() || article.flavorImage == null) { holder.textImage.setImageDrawable(textDrawable); - } else { - Glide.with(getContext()) + Glide.with(HeadlinesFragment.this) .load(article.flavorImageUri) + .transition(DrawableTransitionOptions.withCrossFade()) .placeholder(textDrawable) .thumbnail(0.5f) - .bitmapTransform(new CropCircleTransformation(getActivity())) + .apply(RequestOptions.circleCropTransform()) .diskCacheStrategy(DiskCacheStrategy.ALL) .skipMemoryCache(false) - .listener(new RequestListener<String, GlideDrawable>() { - @Override - public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) { - return false; - } - - @Override - public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) { - - return resource.getIntrinsicWidth() < THUMB_IMG_MIN_SIZE || resource.getIntrinsicHeight() < THUMB_IMG_MIN_SIZE; - } - }) .into(holder.textImage); } @@ -1364,7 +1527,7 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { } } - private void openGalleryForType(Article article, ArticleViewHolder holder, View transitionView) { + private void openGalleryForType(final Article article, final ArticleViewHolder holder, final View transitionView) { //Log.d(TAG, "openGalleryForType: " + article + " " + holder + " " + transitionView); if ("iframe".equalsIgnoreCase(article.flavorImage.tagName())) { @@ -1399,17 +1562,19 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { intent.putExtra("content", tempContent); - ActivityOptionsCompat options = + /* ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(m_activity, transitionView != null ? transitionView : holder.flavorImageView, "gallery:" + (article.flavorStreamUri != null ? article.flavorStreamUri : article.flavorImageUri)); - ActivityCompat.startActivity(m_activity, intent, options.toBundle()); + ActivityCompat.startActivity(m_activity, intent, options.toBundle()); */ + + startActivity(intent); } } - private void adjustVideoKindView(ArticleViewHolder holder, Article article) { + private void adjustVideoKindView(final ArticleViewHolder holder, final Article article) { if (article.flavorImage != null) { if (article.flavor_kind == Article.FLAVOR_KIND_YOUTUBE || "iframe".equalsIgnoreCase(article.flavorImage.tagName())) { holder.flavorVideoKindView.setImageResource(R.drawable.baseline_play_circle_outline_24); @@ -1424,20 +1589,6 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { holder.flavorVideoKindView.setVisibility(View.INVISIBLE); } } - - private void adjustTitleTextView(int score, TextView tv, int position) { - int viewType = getItemViewType(position); - if (origTitleColors[viewType] == null) - // store original color - origTitleColors[viewType] = tv.getCurrentTextColor(); - - if (score < Article.SCORE_LOW) { - tv.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } else { - tv.setTextColor(origTitleColors[viewType]); - tv.setPaintFlags(tv.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); - } - } } private void releaseSurface() { @@ -1482,36 +1633,34 @@ public class HeadlinesFragment extends androidx.fragment.app.Fragment { if (oldPosition != -1) m_adapter.notifyItemChanged(oldPosition); - m_adapter.notifyItemChanged(newPosition); + if (newPosition != -1) { + m_adapter.notifyItemChanged(newPosition); - scrollToArticleId(articleId); + scrollToArticleId(articleId); - if (newPosition >= articles.size() - 5) - new Handler().postDelayed(() -> refresh(true), 0); + if (newPosition >= articles.size() - 5) + new Handler().postDelayed(() -> refresh(true), 0); + } } } public void setSelection(ArticlesSelection select) { - ArticleList articlesWithoutFooters = Application.getArticles().getWithoutFooters(); - - for (Article a : articlesWithoutFooters) { - if (select == ArticlesSelection.ALL || select == ArticlesSelection.UNREAD && a.unread) { - a.selected = true; - - int position = Application.getArticles().getPositionById(a.id); - - if (position != -1) - m_adapter.notifyItemChanged(position); - - } else if (a.selected) { - a.selected = false; + ArticleList articles = Application.getArticles(); + ArticleList tmp = new ArticleList(); - int position = Application.getArticles().getPositionById(a.id); + for (Article a : articles) { + Article articleClone = new Article(a); - if (position != -1) - m_adapter.notifyItemChanged(position); + if (select == ArticlesSelection.ALL || select == ArticlesSelection.UNREAD && a.unread) { + articleClone.selected = true; + } else { + articleClone.selected = false; } + + tmp.add(articleClone); } + + Application.getArticlesModel().update(tmp); } public String getSearchQuery() { diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragmentModel.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragmentModel.java new file mode 100644 index 00000000..dd6d83cd --- /dev/null +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/HeadlinesFragmentModel.java @@ -0,0 +1,22 @@ +package org.fox.ttrss; + +import android.app.Application; +import android.util.Size; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import java.util.HashMap; + +// this is used to store fragment data which is temporary but should survive orientation changes +public class HeadlinesFragmentModel extends AndroidViewModel { + private HashMap<String, Size> m_flavorImageSizes = new HashMap<>(); + + public HashMap<String, Size> getFlavorImageSizes() { + return m_flavorImageSizes; + } + + public HeadlinesFragmentModel(@NonNull Application application) { + super(application); + } +} diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/MasterActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/MasterActivity.java index f2ef3aa0..19cfbe0c 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/MasterActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/MasterActivity.java @@ -417,26 +417,28 @@ public class MasterActivity extends OnlineActivity implements HeadlinesEventList } public void onArticleSelected(Article article, boolean open) { + Article articleClone = new Article(article); + + if (articleClone.unread) { + articleClone.unread = false; + saveArticleUnread(articleClone); + + Application.getArticlesModel().updateById(articleClone); + } + if (open) { - boolean alwaysOpenUri = m_prefs.getBoolean("always_open_uri", false); - if (alwaysOpenUri) { - if (article.unread) { - article.unread = false; - saveArticleUnread(article); - } + HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); - HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); + if (m_prefs.getBoolean("always_open_uri", false)) { - if (hf != null) { + if (hf != null) { hf.setActiveArticleId(article.id); } openUri(Uri.parse(article.link)); - } - else { - HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); + } else if (hf != null) { - Intent intent = new Intent(MasterActivity.this, DetailActivity.class); + Intent intent = new Intent(MasterActivity.this, DetailActivity.class); intent.putExtra("feed", hf.getFeed()); intent.putExtra("searchQuery", hf.getSearchQuery()); intent.putExtra("openedArticleId", article.id); @@ -446,12 +448,7 @@ public class MasterActivity extends OnlineActivity implements HeadlinesEventList } } else { invalidateOptionsMenu(); - - if (article.unread) { - article.unread = false; - saveArticleUnread(article); - } - } + } } @Override diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/OnlineActivity.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/OnlineActivity.java index 467b0bfd..3be7c454 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/OnlineActivity.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/OnlineActivity.java @@ -27,6 +27,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentTransaction; import androidx.preference.PreferenceManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -439,7 +440,7 @@ public class OnlineActivity extends CommonActivity { int selectedIndex = Arrays.asList(headlineModeValues).indexOf(headlineMode); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this) - .setTitle(R.string.headlines_set_view_mode) + .setTitle(R.string.headlines_set_display_mode) .setSingleChoiceItems(headlineModeNames, selectedIndex, (dialog2, which) -> { dialog2.cancel(); @@ -448,20 +449,19 @@ public class OnlineActivity extends CommonActivity { editor.putString("headline_mode", headlineModeValues[which]); editor.apply(); - Intent intent = getIntent(); + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - Feed feed = hf.getFeed(); + HeadlinesFragment hfnew = new HeadlinesFragment(); - if (feed != null) { - intent.putExtra("feed_id", feed.id); - intent.putExtra("feed_is_cat", feed.is_cat); - intent.putExtra("feed_title", feed.title); - } + hfnew.initialize(hf.getFeed()); + hfnew.setSearchQuery(hf.getSearchQuery()); + + ft.replace(R.id.headlines_fragment, hfnew, FRAG_HEADLINES); + + ft.commit(); - finish(); + invalidateOptionsMenu(); - startActivity(intent); - overridePendingTransition(0, 0); }); Dialog dialog = builder.create(); @@ -570,8 +570,6 @@ public class OnlineActivity extends CommonActivity { if (selectedArticle != null) { setArticleScore(selectedArticle); - - hf.notifyItemChanged(Application.getArticles().indexOf(selectedArticle)); } } return true; @@ -580,11 +578,13 @@ public class OnlineActivity extends CommonActivity { Article selectedArticle = Application.getArticles().getById(ap.getSelectedArticleId()); if (selectedArticle != null) { - selectedArticle.marked = !selectedArticle.marked; + Article articleClone = new Article(selectedArticle); + + articleClone.marked = !articleClone.marked; saveArticleMarked(selectedArticle); - hf.notifyItemChanged(Application.getArticles().indexOf(selectedArticle)); + Application.getArticlesModel().updateById(articleClone); } } return true; @@ -593,11 +593,13 @@ public class OnlineActivity extends CommonActivity { Article selectedArticle = Application.getArticles().getById(ap.getSelectedArticleId()); if (selectedArticle != null) { - selectedArticle.unread = !selectedArticle.unread; + Article articleClone = new Article(selectedArticle); + + articleClone.unread = !articleClone.unread; saveArticleUnread(selectedArticle); - hf.notifyItemChanged(Application.getArticles().indexOf(selectedArticle)); + Application.getArticlesModel().updateById(articleClone); } } return true; @@ -607,9 +609,11 @@ public class OnlineActivity extends CommonActivity { if (!selected.isEmpty()) { for (Article a : selected) { - a.unread = !a.unread; + Article articleClone = new Article(a); - hf.notifyItemChanged(Application.getArticles().indexOf(a)); + articleClone.unread = !articleClone.unread; + + Application.getArticlesModel().updateById(articleClone); } toggleArticlesUnread(selected); @@ -623,9 +627,11 @@ public class OnlineActivity extends CommonActivity { if (!selected.isEmpty()) { for (Article a : selected) { - a.marked = !a.marked; + Article articleClone = new Article(a); + + articleClone.marked = !articleClone.marked; - hf.notifyItemChanged(Application.getArticles().indexOf(a)); + Application.getArticlesModel().updateById(articleClone); } toggleArticlesMarked(selected); @@ -639,9 +645,11 @@ public class OnlineActivity extends CommonActivity { if (!selected.isEmpty()) { for (Article a : selected) { - a.published = !a.published; + Article articleClone = new Article(a); + + articleClone.published = !articleClone.published; - hf.notifyItemChanged(Application.getArticles().indexOf(a)); + Application.getArticlesModel().updateById(articleClone); } toggleArticlesPublished(selected); @@ -654,10 +662,12 @@ public class OnlineActivity extends CommonActivity { Article selectedArticle = Application.getArticles().getById(ap.getSelectedArticleId()); if (selectedArticle != null) { - selectedArticle.published = !selectedArticle.published; - saveArticlePublished(selectedArticle); + Article articleClone = new Article(selectedArticle); - hf.notifyItemChanged(Application.getArticles().indexOf(selectedArticle)); + articleClone.published = !articleClone.published; + saveArticlePublished(articleClone); + + Application.getArticlesModel().updateById(articleClone); } } return true; @@ -707,15 +717,14 @@ public class OnlineActivity extends CommonActivity { if (a.id == selectedArticleId) break; - if (a.unread) { - a.unread = false; - tmp.add(a); + Article articleClone = new Article(a); - if (hf != null) { - int position = Application.getArticles().indexOf(a); + if (articleClone.unread) { + articleClone.unread = false; - hf.notifyItemChanged(position); - } + tmp.add(articleClone); + + Application.getArticlesModel().updateById(articleClone); } } @@ -739,16 +748,6 @@ public class OnlineActivity extends CommonActivity { String note = topicEdit.getText().toString().trim(); saveArticleNote(article, note); - - int position = Application.getArticles().getPositionById(article.id); - - if (position != -1) { - HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES); - if (hf != null) hf.notifyItemChanged(position); - - ArticlePager ap = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE); - if (ap != null) ap.notifyItemChanged(position); - } }); builder.setNegativeButton(R.string.dialog_cancel, (dialog, which) -> { @@ -952,7 +951,11 @@ public class OnlineActivity extends CommonActivity { public void saveArticleNote(final Article article, final String note) { ApiRequest req = new ApiRequest(getApplicationContext()) { protected void onPostExecute(JsonElement result) { - article.note = note; + Article articleClone = new Article(article); + + articleClone.note = note; + + Application.getArticlesModel().updateById(articleClone); } }; @@ -983,9 +986,17 @@ public class OnlineActivity extends CommonActivity { (dialog, which) -> { try { -article.score = Integer.parseInt(edit.getText().toString()); + Article articleClone = new Article(article); + + articleClone.score = Integer.parseInt(edit.getText().toString()); + + saveArticleScore(articleClone); + + int position = Application.getArticles().getPositionById(articleClone.id); + + if (position != -1) + Application.getArticlesModel().updateById(articleClone); - saveArticleScore(article); } catch (NumberFormatException e) { toast(R.string.score_invalid); e.printStackTrace(); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/BitmapSizeDecoder.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/BitmapSizeDecoder.java new file mode 100644 index 00000000..1a709073 --- /dev/null +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/BitmapSizeDecoder.java @@ -0,0 +1,30 @@ +package org.fox.ttrss.glide; + +import java.io.File; +import java.io.IOException; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.SimpleResource; + +import android.graphics.BitmapFactory; +import android.util.Log; + +import androidx.annotation.NonNull; + +class BitmapSizeDecoder implements ResourceDecoder<File, BitmapFactory.Options> { + @Override + public Resource<BitmapFactory.Options> decode(File file, int width, int height, Options options) throws IOException { + BitmapFactory.Options bmOptions = new BitmapFactory.Options(); + bmOptions.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), bmOptions); + return new SimpleResource<>(bmOptions); + } + + @Override + public boolean handles(@NonNull File source, @NonNull Options options) throws IOException { + return true; + } +} + diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/OkHttpProgressGlideModule.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/OkHttpProgressGlideModule.java index bb868f8e..44f69b79 100644 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/OkHttpProgressGlideModule.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/OkHttpProgressGlideModule.java @@ -1,15 +1,21 @@ package org.fox.ttrss.glide; import android.content.Context; +import android.graphics.BitmapFactory; import android.os.Handler; import android.os.Looper; +import android.util.Log; +import android.util.Size; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader; import com.bumptech.glide.load.model.GlideUrl; -import com.bumptech.glide.module.GlideModule; +import com.bumptech.glide.module.AppGlideModule; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; @@ -28,21 +34,25 @@ import okio.ForwardingSource; import okio.Okio; import okio.Source; -public class OkHttpProgressGlideModule implements GlideModule { - @Override public void applyOptions(Context context, GlideBuilder builder) { - - } - @Override public void registerComponents(Context context, Glide glide) { +@GlideModule +public class OkHttpProgressGlideModule extends AppGlideModule { + @Override public void registerComponents(Context context, Glide glide, Registry registry) { OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(createInterceptor(new DispatchingProgressListener())) .build(); - glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client)); + + // registry.append() doesn't work... + registry.prepend(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client)); + + registry.prepend(File.class, BitmapFactory.Options.class, new BitmapSizeDecoder()); + registry.register(BitmapFactory.Options.class, Size.class, new OptionsSizeResourceTranscoder()); } public static Interceptor createInterceptor(final ResponseProgressListener listener) { return chain -> { Request request = chain.request(); Response response = chain.proceed(request); + return response.newBuilder() .body(new OkHttpProgressResponseBody(request.url(), response.body(), listener)) .build(); @@ -88,6 +98,7 @@ public class OkHttpProgressGlideModule implements GlideModule { @Override public void update(HttpUrl url, final long bytesRead, final long contentLength) { //System.out.printf("%s: %d/%d = %.2f%%%n", url, bytesRead, contentLength, (100f * bytesRead) / contentLength); + //Log.d("resource progress", "url=" + url + " bytesRead="+ bytesRead + " of " + contentLength); String key = url.toString(); final UIProgressListener listener = LISTENERS.get(key); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/OptionsSizeResourceTranscoder.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/OptionsSizeResourceTranscoder.java new file mode 100644 index 00000000..2fc56fe7 --- /dev/null +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/OptionsSizeResourceTranscoder.java @@ -0,0 +1,20 @@ +package org.fox.ttrss.glide; + +import android.graphics.BitmapFactory; +import android.util.Log; +import android.util.Size; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.SimpleResource; + +class OptionsSizeResourceTranscoder implements ResourceTranscoder<BitmapFactory.Options, Size> { + @Override + public Resource<Size> transcode(Resource<BitmapFactory.Options> resource, Options options) { + BitmapFactory.Options bmOptions = resource.get(); + Size size = new Size(bmOptions.outWidth, bmOptions.outHeight); + return new SimpleResource<>(size); + } +} + diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/ProgressTarget.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/ProgressTarget.java index 977d1954..172dd28a 100644 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/ProgressTarget.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/ProgressTarget.java @@ -2,10 +2,15 @@ package org.fox.ttrss.glide; import android.graphics.drawable.Drawable; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.bumptech.glide.Glide; -import com.bumptech.glide.request.animation.GlideAnimation; +import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.request.target.Target; +import com.bumptech.glide.request.transition.Transition; public abstract class ProgressTarget<T, Z> extends WrappingTarget<Z> implements OkHttpProgressGlideModule.UIProgressListener { private T model; @@ -22,7 +27,6 @@ public abstract class ProgressTarget<T, Z> extends WrappingTarget<Z> implements return model; } public final void setModel(T model) { - Glide.clear(this); // indirectly calls cleanup this.model = model; } /** @@ -95,13 +99,14 @@ public abstract class ProgressTarget<T, Z> extends WrappingTarget<Z> implements super.onLoadStarted(placeholder); start(); } - @Override public void onResourceReady(Z resource, GlideAnimation<? super Z> animation) { + /** @noinspection unchecked*/ + public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) { cleanup(); - super.onResourceReady(resource, animation); + super.onResourceReady(resource, (Transition)transition); } - @Override public void onLoadFailed(Exception e, Drawable errorDrawable) { + @Override public void onLoadFailed(Drawable errorDrawable) { cleanup(); - super.onLoadFailed(e, errorDrawable); + super.onLoadFailed(errorDrawable); } @Override public void onLoadCleared(Drawable placeholder) { cleanup(); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/WrappingTarget.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/WrappingTarget.java index 235acc3c..314800d9 100755 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/WrappingTarget.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/glide/WrappingTarget.java @@ -3,11 +3,12 @@ package org.fox.ttrss.glide; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.bumptech.glide.request.Request; -import com.bumptech.glide.request.animation.GlideAnimation; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.target.Target; +import com.bumptech.glide.request.transition.Transition; public class WrappingTarget<Z> implements Target<Z> { protected final @NonNull Target<? super Z> target; @@ -21,16 +22,24 @@ public class WrappingTarget<Z> implements Target<Z> { target.getSize(cb); } + @Override + public void removeCallback(@NonNull SizeReadyCallback cb) { + + } + @Override public void onLoadStarted(Drawable placeholder) { target.onLoadStarted(placeholder); } - @Override public void onLoadFailed(Exception e, Drawable errorDrawable) { - target.onLoadFailed(e, errorDrawable); + @Override public void onLoadFailed(Drawable errorDrawable) { + target.onLoadFailed(errorDrawable); } - @SuppressWarnings("unchecked") - @Override public void onResourceReady(Z resource, GlideAnimation<? super Z> glideAnimation) { - target.onResourceReady(resource, (GlideAnimation)glideAnimation); + + /** @noinspection unchecked*/ + @Override + public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) { + target.onResourceReady(resource, (Transition)transition); } + @Override public void onLoadCleared(Drawable placeholder) { target.onLoadCleared(placeholder); } diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ArticleDiffItemCallback.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ArticleDiffItemCallback.java index b037eea0..576048b3 100644 --- a/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ArticleDiffItemCallback.java +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ArticleDiffItemCallback.java @@ -7,15 +7,37 @@ import org.fox.ttrss.types.Article; public class ArticleDiffItemCallback extends DiffUtil.ItemCallback<Article> { private final String TAG = this.getClass().getSimpleName(); + + public enum ChangePayload { UNREAD, MARKED, SELECTED, PUBLISHED, NOTE, SCORE }; + @Override - public boolean areItemsTheSame(@NonNull Article a1, @NonNull Article a2) { - return a1.id == a2.id; + public boolean areItemsTheSame(@NonNull Article oldItem, @NonNull Article newItem) { + return oldItem.id == newItem.id; + } + + @Override + public Object getChangePayload(@NonNull Article oldItem, @NonNull Article newItem) { + + if (oldItem.unread != newItem.unread) + return ChangePayload.UNREAD; + else if (oldItem.marked != newItem.marked) + return ChangePayload.MARKED; + else if (oldItem.selected != newItem.selected) + return ChangePayload.SELECTED; + else if (oldItem.published != newItem.published) + return ChangePayload.PUBLISHED; + else if (!oldItem.note.equals(newItem.note)) + return ChangePayload.NOTE; + else if (oldItem.score != newItem.score) + return ChangePayload.SCORE; + + return null; } @Override - public boolean areContentsTheSame(@NonNull Article a1, @NonNull Article a2) { - return a1.id == a2.id && a1.unread == a2.unread && a1.marked == a2.marked - && a1.selected == a2.selected && a1.published == a2.published - && a1.note.equals(a2.note); + public boolean areContentsTheSame(@NonNull Article oldItem, @NonNull Article newItem) { + return oldItem.id == newItem.id && oldItem.unread == newItem.unread && oldItem.marked == newItem.marked + && oldItem.selected == newItem.selected && oldItem.published == newItem.published + && oldItem.score == newItem.score && oldItem.note.equals(newItem.note); } } diff --git a/org.fox.ttrss/src/main/res/drawable/indicator_dot.xml b/org.fox.ttrss/src/main/res/drawable/indicator_dot.xml new file mode 100644 index 00000000..b6dd6c82 --- /dev/null +++ b/org.fox.ttrss/src/main/res/drawable/indicator_dot.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- https://github.com/ongakuer/CircleIndicator/issues/160 --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:padding="10dp" + android:shape="oval"> + <solid android:color="?colorTertiary" /> + <corners android:radius="25dp" /> +</shape>
\ No newline at end of file diff --git a/org.fox.ttrss/src/main/res/layout/activity_gallery.xml b/org.fox.ttrss/src/main/res/layout/activity_gallery.xml index b0f67761..f1ad3f8c 100644 --- a/org.fox.ttrss/src/main/res/layout/activity_gallery.xml +++ b/org.fox.ttrss/src/main/res/layout/activity_gallery.xml @@ -5,7 +5,7 @@ android:animateLayoutChanges="true" android:layout_height="fill_parent"> - <com.bogdwellers.pinchtozoom.view.ImageViewPager + <androidx.viewpager2.widget.ViewPager2 android:id="@+id/gallery_pager" android:layout_width="fill_parent" android:layout_height="fill_parent" @@ -38,13 +38,14 @@ android:layout_marginLeft="@dimen/activity_horizontal_margin" android:layout_marginRight="@dimen/activity_horizontal_margin" /> - <me.relex.circleindicator.CircleIndicator + <me.relex.circleindicator.CircleIndicator3 android:id="@+id/gallery_pager_indicator" android:layout_width="fill_parent" android:layout_height="32dp" android:layout_marginBottom="55dp" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" + app:ci_drawable="@drawable/indicator_dot" android:layout_alignParentStart="true" android:visibility="visible" /> diff --git a/org.fox.ttrss/src/main/res/layout/fragment_gallery_entry.xml b/org.fox.ttrss/src/main/res/layout/fragment_gallery_entry.xml index 5bde3166..3c507cba 100644 --- a/org.fox.ttrss/src/main/res/layout/fragment_gallery_entry.xml +++ b/org.fox.ttrss/src/main/res/layout/fragment_gallery_entry.xml @@ -1,6 +1,6 @@ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/flavorImageHolder" + android:id="@+id/flavor_image_holder" android:layout_width="match_parent" android:layout_height="match_parent"> diff --git a/org.fox.ttrss/src/main/res/layout/headlines_row.xml b/org.fox.ttrss/src/main/res/layout/headlines_row.xml index 3c0ecb20..85a93f33 100755 --- a/org.fox.ttrss/src/main/res/layout/headlines_row.xml +++ b/org.fox.ttrss/src/main/res/layout/headlines_row.xml @@ -25,7 +25,6 @@ android:id="@+id/headline_header" android:layout_width="match_parent" android:layout_height="wrap_content" - android:clickable="true" android:layout_span="2" android:padding="16dp"> @@ -100,17 +99,20 @@ android:layout_height="match_parent" > <FrameLayout - android:id="@+id/flavorImageHolder" + android:id="@+id/flavor_image_holder" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginBottom="16dp" android:layout_span="2"> <ProgressBar - android:id="@+id/flavorImageLoadingBar" + android:id="@+id/flavor_image_progressbar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" android:indeterminate="false" android:max="100" android:visibility="visible" /> @@ -121,7 +123,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:adjustViewBounds="true" - android:background="@android:color/transparent" + android:background="@null" android:cropToPadding="true" android:scaleType="centerCrop" tools:src="@drawable/ic_launcher_background" @@ -178,7 +180,8 @@ android:textAlignment="viewStart" android:lineSpacingExtra="2sp" android:maxLines="5" - android:padding="16dp" + android:paddingStart="16dp" + android:paddingEnd="16dp" tools:text="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." android:textSize="13sp" /> </TableRow> @@ -193,8 +196,7 @@ android:layout_height="wrap_content" android:layout_span="2" android:gravity="center_vertical" - android:paddingBottom="8dp" - android:paddingLeft="8dp"> + android:paddingBottom="8dp"> <com.google.android.material.checkbox.MaterialCheckBox android:id="@+id/selected" diff --git a/org.fox.ttrss/src/main/res/layout/headlines_row_compact_active.xml b/org.fox.ttrss/src/main/res/layout/headlines_row_compact_active.xml deleted file mode 100755 index 72ae99b8..00000000 --- a/org.fox.ttrss/src/main/res/layout/headlines_row_compact_active.xml +++ /dev/null @@ -1,109 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout - xmlns:tools="http://schemas.android.com/tools" - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/headlines_row" - android:paddingTop="16dp" - android:paddingBottom="16dp" - android:paddingStart="16dp" - android:paddingEnd="8dp" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:background="?colorTertiaryContainer" - android:orientation="horizontal"> - - <FrameLayout - android:layout_gravity="center_vertical|start" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginEnd="16dp"> - - <ImageView - android:clickable="true" - android:focusable="true" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_gravity="center" - android:id="@+id/text_image"/> - - <ImageView - android:layout_width="48dp" - android:layout_height="48dp" - android:src="@drawable/check_sm" - android:id="@+id/text_checked" - android:layout_gravity="center" /> - - </FrameLayout> - - <LinearLayout - android:layout_weight="1" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <TextView - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:maxLines="2" - android:ellipsize="end" - tools:text="Sample entry title" - android:textColor="?colorOnTertiaryContainer" - android:textSize="18sp" /> - - <TextView - android:id="@+id/feed_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:ellipsize="middle" - android:fontFamily="sans-serif-light" - android:singleLine="true" - tools:text="Example Feed AAA AAA AAAAAA AAAA AAAAA AA A A AA AA" - android:textColor="?colorOnTertiaryContainer" - android:textSize="12sp" - android:layout_marginTop="4dp" /> - - <TextView - android:id="@+id/excerpt" - android:maxLines="2" - android:ellipsize="end" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:text="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - android:textColor="?colorOnTertiaryContainer" - android:textSize="13sp" - android:layout_marginTop="4dp" /> - </LinearLayout> - - <LinearLayout - android:orientation="vertical" - android:layout_weight="0" - android:layout_width="48dp" - android:layout_height="match_parent"> - - <TextView - android:id="@+id/date" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fontFamily="sans-serif-light" - android:singleLine="true" - android:ellipsize="none" - android:textAlignment="viewEnd" - android:layout_weight="0.5" - tools:text="Jan 01" - android:textColor="?colorSecondary" - android:textSize="12sp" - /> - - <com.google.android.material.button.MaterialButton - android:id="@+id/marked" - style="?attr/materialIconButtonStyle" - android:layout_width="wrap_content" - android:paddingEnd="0dp" - android:layout_gravity="end" - android:layout_height="24dp" - android:layout_weight="0.5" - app:icon="@drawable/baseline_star_outline_24" /> - </LinearLayout> -</LinearLayout>
\ No newline at end of file diff --git a/org.fox.ttrss/src/main/res/layout/headlines_row_compact_active_unread.xml b/org.fox.ttrss/src/main/res/layout/headlines_row_compact_active_unread.xml deleted file mode 100755 index b2b3a21e..00000000 --- a/org.fox.ttrss/src/main/res/layout/headlines_row_compact_active_unread.xml +++ /dev/null @@ -1,110 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout - xmlns:tools="http://schemas.android.com/tools" - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/headlines_row" - android:paddingTop="16dp" - android:paddingBottom="16dp" - android:paddingStart="16dp" - android:paddingEnd="8dp" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:background="?colorTertiaryContainer" - android:orientation="horizontal"> - - <FrameLayout - android:layout_gravity="center_vertical|start" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginEnd="16dp"> - - <ImageView - android:clickable="true" - android:focusable="true" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_gravity="center" - android:id="@+id/text_image"/> - - <ImageView - android:layout_width="48dp" - android:layout_height="48dp" - android:src="@drawable/check_sm" - android:id="@+id/text_checked" - android:layout_gravity="center" /> - - </FrameLayout> - - <LinearLayout - android:layout_weight="1" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <TextView - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:maxLines="2" - android:ellipsize="end" - tools:text="Sample entry title" - android:textColor="?colorOnTertiaryContainer" - android:textStyle="bold" - android:textSize="18sp" /> - - <TextView - android:id="@+id/feed_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:ellipsize="middle" - android:fontFamily="sans-serif-light" - android:singleLine="true" - tools:text="Example Feed AAA AAA AAAAAA AAAA AAAAA AA A A AA AA" - android:textColor="?colorOnTertiaryContainer" - android:textSize="12sp" - android:layout_marginTop="4dp" /> - - <TextView - android:id="@+id/excerpt" - android:maxLines="2" - android:ellipsize="end" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:text="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - android:textColor="?colorOnTertiaryContainer" - android:textSize="13sp" - android:layout_marginTop="4dp" /> - </LinearLayout> - - <LinearLayout - android:orientation="vertical" - android:layout_weight="0" - android:layout_width="48dp" - android:layout_height="match_parent"> - - <TextView - android:id="@+id/date" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fontFamily="sans-serif-light" - android:singleLine="true" - android:ellipsize="none" - android:textAlignment="viewEnd" - android:layout_weight="0.5" - tools:text="Jan 01" - android:textColor="?colorSecondary" - android:textSize="12sp" - /> - - <com.google.android.material.button.MaterialButton - android:id="@+id/marked" - style="?attr/materialIconButtonStyle" - android:layout_width="wrap_content" - android:paddingEnd="0dp" - android:layout_gravity="end" - android:layout_height="24dp" - android:layout_weight="0.5" - app:icon="@drawable/baseline_star_outline_24" /> - </LinearLayout> -</LinearLayout>
\ No newline at end of file diff --git a/org.fox.ttrss/src/main/res/layout/headlines_row_compact_unread.xml b/org.fox.ttrss/src/main/res/layout/headlines_row_compact_unread.xml deleted file mode 100755 index 2fbbe062..00000000 --- a/org.fox.ttrss/src/main/res/layout/headlines_row_compact_unread.xml +++ /dev/null @@ -1,110 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout - xmlns:tools="http://schemas.android.com/tools" - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/headlines_row" - android:paddingTop="16dp" - android:paddingBottom="16dp" - android:paddingStart="16dp" - android:paddingEnd="8dp" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:background="?colorSurfaceContainerLowest" - android:orientation="horizontal"> - - <FrameLayout - android:layout_gravity="center_vertical|start" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginEnd="16dp"> - - <ImageView - android:clickable="true" - android:focusable="true" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_gravity="center" - android:id="@+id/text_image"/> - - <ImageView - android:layout_width="48dp" - android:layout_height="48dp" - android:src="@drawable/check_sm" - android:id="@+id/text_checked" - android:layout_gravity="center" /> - - </FrameLayout> - - <LinearLayout - android:layout_weight="1" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <TextView - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:maxLines="2" - android:ellipsize="end" - tools:text="Sample entry title" - android:textStyle="bold" - android:textSize="18sp" /> - - <TextView - android:id="@+id/feed_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:ellipsize="middle" - android:fontFamily="sans-serif-light" - android:singleLine="true" - tools:text="Example Feed AAA AAA AAAAAA AAAA AAAAA AA A A AA AA" - android:textColor="?colorSecondary" - android:textSize="12sp" - android:layout_marginTop="4dp" /> - - <TextView - android:id="@+id/excerpt" - android:maxLines="2" - android:ellipsize="end" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:text="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - android:textColor="?colorSecondary" - android:textSize="13sp" - android:layout_marginTop="4dp" /> - - </LinearLayout> - - <LinearLayout - android:orientation="vertical" - android:layout_weight="0" - android:layout_width="48dp" - android:layout_height="match_parent"> - - <TextView - android:id="@+id/date" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fontFamily="sans-serif-light" - android:singleLine="true" - android:ellipsize="none" - android:textAlignment="viewEnd" - android:layout_weight="0.5" - tools:text="Jan 01" - android:textColor="?colorSecondary" - android:textSize="12sp" - /> - - <com.google.android.material.button.MaterialButton - android:id="@+id/marked" - style="?attr/materialIconButtonStyle" - android:layout_width="wrap_content" - android:paddingEnd="0dp" - android:layout_gravity="end" - android:layout_height="24dp" - android:layout_weight="0.5" - app:icon="@drawable/baseline_star_outline_24" /> - </LinearLayout> -</LinearLayout>
\ No newline at end of file diff --git a/org.fox.ttrss/src/main/res/layout/headlines_row_unread.xml b/org.fox.ttrss/src/main/res/layout/headlines_row_unread.xml deleted file mode 100755 index 7cae0fb1..00000000 --- a/org.fox.ttrss/src/main/res/layout/headlines_row_unread.xml +++ /dev/null @@ -1,273 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/headlines_row" - app:cardBackgroundColor="?colorSurfaceContainerLowest" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:layout_marginTop="8dp" - app:strokeWidth="0dp" - android:layout_width="wrap_content" - android:layout_height="wrap_content"> - - <TableLayout - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:baselineAligned="false" - android:shrinkColumns="0,1" - android:stretchColumns="0,1"> - - <TableRow - android:layout_width="fill_parent" - android:layout_height="wrap_content"> - - <RelativeLayout - android:id="@+id/headline_header" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:clickable="true" - android:layout_span="2" - android:padding="16dp"> - - <TextView - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:ellipsize="end" - android:maxLines="3" - android:paddingBottom="8dp" - android:singleLine="false" - android:textColor="?colorOnSurface" - android:textSize="18sp" - android:textStyle="bold" - tools:text="Sample entry title which is overwhelmingly long blah blah blah" /> - - <LinearLayout - android:id="@+id/linearLayout2" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@+id/title" - android:orientation="horizontal"> - - <TextView - android:id="@+id/feed_title" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="0.5" - android:ellipsize="middle" - android:fontFamily="sans-serif-light" - android:singleLine="true" - android:textAlignment="viewStart" - android:textColor="?colorSecondary" - android:textSize="12sp" - tools:text="Example Feed AAA AAA AAAAAA AAAA AAAAA AA A A AA AA" /> - - <TextView - android:id="@+id/date" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="0.5" - android:ellipsize="none" - android:fontFamily="sans-serif-light" - android:singleLine="true" - android:textAlignment="viewEnd" - android:textColor="?colorSecondary" - android:textSize="12sp" - tools:text="Jan 01, 12:00, 1970" /> - - </LinearLayout> - - <TextView - android:id="@+id/link_host" - android:layout_width="match_parent" - android:layout_marginTop="8dp" - android:layout_below="@+id/linearLayout2" - android:layout_height="wrap_content" - android:ellipsize="middle" - android:fontFamily="sans-serif-light" - android:singleLine="true" - android:textAlignment="viewStart" - android:textColor="?colorSecondary" - android:textSize="12sp" - tools:text="example.com" /> - - </RelativeLayout> - - </TableRow> - - <TableRow - android:layout_width="match_parent" - android:layout_height="match_parent" > - - <FrameLayout - android:id="@+id/flavorImageHolder" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_span="2"> - - <ProgressBar - android:id="@+id/flavorImageLoadingBar" - style="?android:attr/progressBarStyleHorizontal" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:indeterminate="false" - android:max="100" - android:visibility="visible" /> - - <ImageView - android:id="@+id/flavor_image" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:adjustViewBounds="true" - android:background="@android:color/transparent" - android:cropToPadding="true" - android:scaleType="centerCrop" - tools:src="@drawable/ic_launcher_background" - android:visibility="visible" /> - - <TextureView - android:id="@+id/flavor_video" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:adjustViewBounds="true" - android:background="@null" - android:cropToPadding="true" - android:foreground="@null" - android:scaleType="fitCenter" - android:visibility="gone" /> - - <ImageView - android:id="@+id/flavor_video_kind" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="start|bottom" - android:layout_marginStart="8dp" - android:layout_marginBottom="10dp" - android:scaleType="fitXY" - android:src="@drawable/baseline_play_circle_24" - android:visibility="visible" - app:tint="?colorTertiary" /> - - <com.google.android.material.button.MaterialButton - android:id="@+id/gallery_overflow" - style="?attr/materialIconButtonStyle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="bottom|end" - android:layout_marginEnd="-8dp" - app:icon="@drawable/baseline_more_vert_24" - app:iconTint="?colorTertiary"/> - - </FrameLayout> - </TableRow> - - <TableRow - android:layout_width="fill_parent" - android:layout_height="wrap_content"> - - <TextView - android:id="@+id/excerpt" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_span="2" - android:ellipsize="end" - android:textAlignment="viewStart" - android:lineSpacingExtra="2sp" - android:maxLines="5" - android:padding="16dp" - tools:text="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - android:textSize="13sp" /> - </TableRow> - - <TableRow - android:layout_width="fill_parent" - android:layout_height="wrap_content"> - - <LinearLayout - android:id="@+id/headline_footer" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_span="2" - android:gravity="center_vertical" - android:paddingBottom="8dp" - android:paddingLeft="8dp"> - - <com.google.android.material.checkbox.MaterialCheckBox - android:id="@+id/selected" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_weight="0" - android:focusable="false" /> - - <TextView - android:id="@+id/author" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:layout_weight="1" - android:ellipsize="middle" - android:fontFamily="sans-serif-light" - android:gravity="center_vertical" - android:singleLine="true" - tools:text="by Author" - android:textAlignment="viewStart" - android:textColor="?colorSecondary" - android:textSize="12sp" - android:textStyle="italic" /> - - <com.google.android.material.button.MaterialButton - style="?attr/materialIconButtonStyle" - android:id="@+id/score" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_weight="0" - android:paddingLeft="4dp" - android:paddingRight="4dp" - app:icon="@drawable/baseline_trending_flat_24" /> - - <com.google.android.material.button.MaterialButton - style="?attr/materialIconButtonStyle" - android:id="@+id/attachments" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_weight="0" - android:paddingLeft="4dp" - android:paddingRight="4dp" - app:icon="@drawable/baseline_attachment_24" /> - - <com.google.android.material.button.MaterialButton - style="?attr/materialIconButtonStyle" - android:id="@+id/marked" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_weight="0" - android:paddingLeft="4dp" - android:paddingRight="4dp" - app:icon="@drawable/baseline_star_outline_24" /> - - <com.google.android.material.button.MaterialButton - style="?attr/materialIconButtonStyle" - android:id="@+id/published" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_weight="0" - android:paddingLeft="4dp" - android:paddingRight="4dp" - app:icon="@drawable/rss" /> - - <com.google.android.material.button.MaterialButton - style="?attr/materialIconButtonStyle" - android:id="@+id/article_menu_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_weight="0" - android:paddingLeft="4dp" - android:paddingRight="4dp" - app:icon="@drawable/baseline_more_vert_24" /> - </LinearLayout> - </TableRow> - </TableLayout> -</com.google.android.material.card.MaterialCardView>
\ No newline at end of file diff --git a/org.fox.ttrss/src/main/res/values/strings.xml b/org.fox.ttrss/src/main/res/values/strings.xml index df82688b..6f34b705 100755 --- a/org.fox.ttrss/src/main/res/values/strings.xml +++ b/org.fox.ttrss/src/main/res/values/strings.xml @@ -305,4 +305,7 @@ <string name="open_on_startup">Open on startup</string> <string name="error_success">Operation completed successfully</string> <string name="error_bad_request">Error: 400 bad request</string> + <string name="headlines_set_display_mode">Set display mode</string> + <string name="window_secure_mode_summary">Disables screenshots and hides window contents on non-secure displays</string> + <string name="window_secure_mode">Secure window mode</string> </resources> diff --git a/org.fox.ttrss/src/main/res/xml/preferences.xml b/org.fox.ttrss/src/main/res/xml/preferences.xml index 8d064587..9e1f6642 100755 --- a/org.fox.ttrss/src/main/res/xml/preferences.xml +++ b/org.fox.ttrss/src/main/res/xml/preferences.xml @@ -67,6 +67,12 @@ android:key="force_phone_layout" android:summary="@string/force_phone_layout_summary" android:title="@string/force_phone_layout" /> + + <SwitchPreferenceCompat + android:defaultValue="false" + android:key="window_secure_mode" + android:summary="@string/window_secure_mode_summary" + android:title="@string/window_secure_mode" /> </PreferenceCategory> <PreferenceCategory @@ -134,6 +140,7 @@ android:summary="@string/prefs_always_downsample_images_long" /> <SwitchPreferenceCompat + android:enabled="false" android:defaultValue="false" android:key="inline_video_player" android:summary="@string/prefs_inline_video_player" |