diff --git a/app/build.gradle b/app/build.gradle
index b52220caf..ac1869639 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -74,4 +74,5 @@ dependencies {
implementation 'com.mikepenz:aboutlibraries:7.1.0'
implementation 'com.github.di72nn.wallabag-api-wrapper:api-wrapper:v2.0.0-beta.6'
implementation 'org.slf4j:slf4j-android:1.7.36'
+ implementation 'io.github.panpf.zoomimage:zoomimage-view:1.4.0'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9ff75dc6e..51db8ca2e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -55,6 +55,9 @@
android:name="fr.gaulupeau.apps.Poche.ui.ReadArticleActivity"
android:hardwareAccelerated="true"
android:configChanges="keyboardHidden|orientation|screenSize"/>
+
diff --git a/app/src/main/assets/image-zoom-handler.js b/app/src/main/assets/image-zoom-handler.js
new file mode 100644
index 000000000..8ab53cb2f
--- /dev/null
+++ b/app/src/main/assets/image-zoom-handler.js
@@ -0,0 +1,10 @@
+document.addEventListener("DOMContentLoaded", function() {
+ document.addEventListener("click", function(e) {
+ var target = e.target;
+ if (target.tagName === "IMG" && target.src) {
+ e.preventDefault();
+ e.stopPropagation();
+ hostImageController.onImageClicked(target.src);
+ }
+ }, true);
+});
diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java
new file mode 100644
index 000000000..2fec3287c
--- /dev/null
+++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java
@@ -0,0 +1,136 @@
+package fr.gaulupeau.apps.Poche.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.ProgressBar;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.github.panpf.zoomimage.ZoomImageView;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+
+import fr.gaulupeau.apps.InThePoche.R;
+import fr.gaulupeau.apps.Poche.network.ImageCacheUtils;
+
+public class ImageViewActivity extends AppCompatActivity {
+
+ public static final String EXTRA_IMAGE_URL = "ImageViewActivity.imageUrl";
+ public static final String EXTRA_ARTICLE_ID = "ImageViewActivity.articleId";
+
+ private static final String TAG = ImageViewActivity.class.getSimpleName();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_image_view);
+
+ ZoomImageView photoView = findViewById(R.id.photoView);
+ ProgressBar progressBar = findViewById(R.id.progressBar);
+
+ String imageUrl = getIntent().getStringExtra(EXTRA_IMAGE_URL);
+ long articleId = getIntent().getLongExtra(EXTRA_ARTICLE_ID, -1);
+
+ if (imageUrl == null || imageUrl.isEmpty()) {
+ Log.w(TAG, "onCreate() no image URL");
+ finish();
+ return;
+ }
+
+ photoView.setOnClickListener(v -> finish());
+
+ new Thread(() -> {
+ Bitmap bitmap = loadBitmap(imageUrl, articleId);
+ runOnUiThread(() -> {
+ progressBar.setVisibility(View.GONE);
+ if (bitmap != null) {
+ photoView.setImageBitmap(bitmap);
+ } else {
+ Log.w(TAG, "onCreate() failed to load image");
+ finish();
+ }
+ });
+ }).start();
+ }
+
+ private Bitmap loadBitmap(String imageUrl, long articleId) {
+ // Try loading from local cache first
+ if (articleId >= 0) {
+ try {
+ File file = ImageCacheUtils.getCachedImageFile(imageUrl, articleId);
+ if (file != null) {
+ Bitmap bitmap = decodeFileScaled(file.getAbsolutePath());
+ if (bitmap != null) return bitmap;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "loadBitmap() cache load failed", e);
+ }
+ }
+
+ // Try loading from file:// URL (cached images served to WebView)
+ if (imageUrl.startsWith("file://")) {
+ try {
+ String path = imageUrl.substring("file://".length());
+ Bitmap bitmap = decodeFileScaled(path);
+ if (bitmap != null) return bitmap;
+ } catch (Exception e) {
+ Log.w(TAG, "loadBitmap() file URL load failed", e);
+ }
+ }
+
+ // Fall back to loading from network
+ try {
+ URL url = new URL(imageUrl);
+ try (InputStream is = url.openStream()) {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ byte[] chunk = new byte[8192];
+ int n;
+ while ((n = is.read(chunk)) != -1) {
+ buffer.write(chunk, 0, n);
+ }
+ return decodeBytesScaled(buffer.toByteArray());
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "loadBitmap() remote load failed", e);
+ }
+
+ return null;
+ }
+
+ // Canvas hardware-accelerated draw limit is ~100MB; cap at 4096px (64MB @ ARGB_8888).
+ private static final int MAX_BITMAP_DIMENSION = 4096;
+
+ private static Bitmap decodeFileScaled(String path) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, options);
+ options.inSampleSize = computeSampleSize(options.outWidth, options.outHeight);
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeFile(path, options);
+ }
+
+ private static Bitmap decodeBytesScaled(byte[] data) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(data, 0, data.length, options);
+ options.inSampleSize = computeSampleSize(options.outWidth, options.outHeight);
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeByteArray(data, 0, data.length, options);
+ }
+
+ private static int computeSampleSize(int width, int height) {
+ int sampleSize = 1;
+ while (width > 0 && height > 0
+ && (width / sampleSize > MAX_BITMAP_DIMENSION
+ || height / sampleSize > MAX_BITMAP_DIMENSION)) {
+ sampleSize *= 2;
+ }
+ return sampleSize;
+ }
+}
diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java
index 559e3e145..da592af70 100644
--- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java
+++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java
@@ -22,6 +22,7 @@
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
+import android.webkit.JavascriptInterface;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.FrameLayout;
@@ -642,6 +643,7 @@ private void initWebView() {
initTtsController();
initAnnotationController();
+ initImageController();
webViewContent.setWebChromeClient(new WebChromeClient() {
private View customView;
@@ -800,6 +802,22 @@ private void initTtsController() {
webViewContent.addJavascriptInterface(jsTtsController, "hostWebViewTextController");
}
+ private void initImageController() {
+ webViewContent.addJavascriptInterface(new Object() {
+ @SuppressWarnings("unused")
+ @JavascriptInterface
+ public void onImageClicked(String imageUrl) {
+ Log.d(TAG, "onImageClicked() url: " + imageUrl);
+ Intent intent = new Intent(ReadArticleActivity.this, ImageViewActivity.class);
+ intent.putExtra(ImageViewActivity.EXTRA_IMAGE_URL, imageUrl);
+ if (article != null) {
+ intent.putExtra(ImageViewActivity.EXTRA_ARTICLE_ID, article.getArticleId().longValue());
+ }
+ startActivity(intent);
+ }
+ }, "hostImageController");
+ }
+
private void initAnnotationController() {
if (!annotationsEnabled) return;
@@ -918,6 +936,8 @@ private String getHtmlBase() {
private String getExtraHead() {
String extra = "";
+ extra += "\n\t\t";
+
if (annotationsEnabled) {
extra += "\n" +
"\t\t" +
diff --git a/app/src/main/res/layout/activity_image_view.xml b/app/src/main/res/layout/activity_image_view.xml
new file mode 100644
index 000000000..6e3bc9395
--- /dev/null
+++ b/app/src/main/res/layout/activity_image_view.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-in b/app/src/main/res/values-in
deleted file mode 120000
index f7118b95e..000000000
--- a/app/src/main/res/values-in
+++ /dev/null
@@ -1 +0,0 @@
-values-id
\ No newline at end of file