diff --git a/app/build.gradle b/app/build.gradle
index 83937b28de..dfd8ae5b33 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -35,7 +35,7 @@ android {
//noinspection ExpiringTargetSdkVersion
targetSdkVersion 31
versionCode 130 // is updated automatically by BitRise; only used when building locally
- versionName '1.15.19'
+ versionName '1.15.20'
def includeObjectBoxBrowser = System.getenv("INCLUDE_OBJECTBOX_BROWSER") ?: "false"
def includeLeakCanary = System.getenv("INCLUDE_LEAK_CANARY") ?: "false"
@@ -75,7 +75,9 @@ android {
}
}
packagingOptions {
- exclude 'META-INF/rxjava.properties'
+ resources {
+ excludes += ['META-INF/rxjava.properties']
+ }
}
testOptions {
unitTests.includeAndroidResources = true
@@ -111,7 +113,7 @@ dependencies {
// Support libraries
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.appcompat:appcompat:1.4.1'
- implementation 'com.google.android.material:material:1.5.0-beta01'
+ implementation 'com.google.android.material:material:1.6.0-alpha02'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'androidx.paging:paging-runtime:2.1.2'
@@ -232,7 +234,7 @@ dependencies {
// ObjectBox browser dependencies must be set before applying ObjectBox plugin so it does not add objectbox-android
// (would result in two conflicting versions, e.g. "Duplicate files copied in APK lib/armeabi-v7a/libobjectbox.so").
- def objectbox_version = "3.0.1"
+ def objectbox_version = "3.1.1"
if (includeObjectBoxBrowser.toBoolean()) {
debugImplementation "io.objectbox:objectbox-android-objectbrowser:$objectbox_version"
releaseImplementation "io.objectbox:objectbox-android:$objectbox_version"
diff --git a/app/objectbox-models/default.json b/app/objectbox-models/default.json
index c48bd785af..578697c36e 100644
--- a/app/objectbox-models/default.json
+++ b/app/objectbox-models/default.json
@@ -422,7 +422,7 @@
},
{
"id": "10:5605025377139576552",
- "lastPropertyId": "13:1008829494719594190",
+ "lastPropertyId": "14:8168924561252314098",
"name": "Group",
"properties": [
{
@@ -448,14 +448,6 @@
"name": "order",
"type": 5
},
- {
- "id": "5:5434514687553672562",
- "name": "pictureId",
- "indexId": "10:3892013958767955149",
- "type": 11,
- "flags": 520,
- "relationTarget": "ImageFile"
- },
{
"id": "6:1399053985046563297",
"name": "isBeingDeleted",
@@ -490,6 +482,14 @@
"id": "13:1008829494719594190",
"name": "favourite",
"type": 1
+ },
+ {
+ "id": "14:8168924561252314098",
+ "name": "coverContentId",
+ "indexId": "21:2677634342952249814",
+ "type": 11,
+ "flags": 520,
+ "relationTarget": "Content"
}
],
"relations": []
@@ -680,7 +680,7 @@
}
],
"lastEntityId": "15:2260398621202196035",
- "lastIndexId": "20:5101223626385692255",
+ "lastIndexId": "21:2677634342952249814",
"lastRelationId": "3:1412032361666532056",
"lastSequenceId": "0:0",
"modelVersion": 5,
@@ -693,7 +693,8 @@
9049773303925818273,
4642610167694780718,
9182121386646774745,
- 372149557770070472
+ 372149557770070472,
+ 3892013958767955149
],
"retiredPropertyUids": [
1563990578201199392,
@@ -710,7 +711,8 @@
753426778229264571,
2206907103185425670,
6457903105882729256,
- 4015520791093060143
+ 4015520791093060143,
+ 5434514687553672562
],
"retiredRelationUids": [
4182320255043105081,
diff --git a/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.java
index a3dc404003..1c006484f7 100644
--- a/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.java
+++ b/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.java
@@ -312,7 +312,7 @@ public void onDrawerClosed(View view) {
initSelectionToolbar();
initUI();
updateToolbar();
- updateSelectionToolbar(0, 0, 0);
+ updateSelectionToolbar(0, 0, 0, 0);
onCreated();
sortCommandsAutoHide = new Debouncer<>(this, 3000, this::hideSearchSortBar);
@@ -436,7 +436,7 @@ public void onPageSelected(int position) {
enableCurrentFragment();
hideSearchSortBar(false);
updateToolbar();
- updateSelectionToolbar(0, 0, 0);
+ updateSelectionToolbar(0, 0, 0, 0);
}
});
viewPager.setAdapter(pagerAdapter);
@@ -952,7 +952,8 @@ private void updateToolbar() {
public void updateSelectionToolbar(
long selectedTotalCount,
long selectedLocalCount,
- long selectedStreamedCount) {
+ long selectedStreamedCount,
+ long selectedEligibleExternalCount) {
boolean isMultipleSelection = selectedTotalCount > 1;
long selectedDownloadedCount = selectedLocalCount - selectedStreamedCount;
long selectedExternalCount = selectedTotalCount - selectedLocalCount;
@@ -987,8 +988,8 @@ public void updateSelectionToolbar(
mergeMenu.setVisible(
(selectedLocalCount > 1 && 0 == selectedStreamedCount && 0 == selectedExternalCount)
|| (selectedStreamedCount > 1 && 0 == selectedLocalCount && 0 == selectedExternalCount)
- || (selectedExternalCount > 1 && 0 == selectedLocalCount && 0 == selectedStreamedCount)
- ); // Can only merge downloaded or streamed content together
+ || (selectedExternalCount > 1 && 0 == selectedLocalCount && 0 == selectedStreamedCount && selectedEligibleExternalCount == selectedExternalCount)
+ ); // Can only merge downloaded, streamed or non-archive external content together
splitMenu.setVisible(!isMultipleSelection && 1 == selectedLocalCount);
}
}
diff --git a/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.kt
index 29f7c153b7..d44fe2b040 100644
--- a/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.kt
+++ b/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.kt
@@ -7,7 +7,7 @@ import androidx.fragment.app.commit
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import me.devsaki.hentoid.R
-import me.devsaki.hentoid.activities.bundles.PrefsActivityBundle
+import me.devsaki.hentoid.activities.bundles.PrefsBundle
import me.devsaki.hentoid.events.ProcessEvent
import me.devsaki.hentoid.fragments.preferences.PreferencesFragment
import me.devsaki.hentoid.util.FileHelper
@@ -46,28 +46,28 @@ class PrefsActivity : BaseActivity() {
private fun isViewerPrefs(): Boolean {
return if (intent.extras != null) {
- val parser = PrefsActivityBundle.Parser(intent.extras!!)
+ val parser = PrefsBundle(intent.extras!!)
parser.isViewerPrefs
} else false
}
private fun isBrowserPrefs(): Boolean {
return if (intent.extras != null) {
- val parser = PrefsActivityBundle.Parser(intent.extras!!)
+ val parser = PrefsBundle(intent.extras!!)
parser.isBrowserPrefs
} else false
}
private fun isDownloaderPrefs(): Boolean {
return if (intent.extras != null) {
- val parser = PrefsActivityBundle.Parser(intent.extras!!)
+ val parser = PrefsBundle(intent.extras!!)
parser.isDownloaderPrefs
} else false
}
private fun isStoragePrefs(): Boolean {
return if (intent.extras != null) {
- val parser = PrefsActivityBundle.Parser(intent.extras!!)
+ val parser = PrefsBundle(intent.extras!!)
parser.isStoragePrefs
} else false
}
diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/BaseWebActivityBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/BaseWebActivityBundle.java
deleted file mode 100644
index caac6885d0..0000000000
--- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/BaseWebActivityBundle.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package me.devsaki.hentoid.activities.bundles;
-
-import android.os.Bundle;
-
-import javax.annotation.Nonnull;
-
-/**
- * Helper class to transfer data from any Activity to {@link me.devsaki.hentoid.activities.sources.BaseWebActivity}
- * through a Bundle
- *
- * Use Builder class to set data; use Parser class to get data
- */
-public class BaseWebActivityBundle {
- private static final String KEY_URL = "url";
-
- private BaseWebActivityBundle() {
- throw new UnsupportedOperationException();
- }
-
- public static final class Builder {
-
- private final Bundle bundle = new Bundle();
-
- public void setUrl(String url) {
- bundle.putString(KEY_URL, url);
- }
-
- public Bundle getBundle() {
- return bundle;
- }
- }
-
- public static final class Parser {
-
- private final Bundle bundle;
-
- public Parser(@Nonnull Bundle bundle) {
- this.bundle = bundle;
- }
-
- public String getUrl() {
- return bundle.getString(KEY_URL, "");
- }
- }
-}
diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/BaseWebActivityBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/BaseWebActivityBundle.kt
new file mode 100644
index 0000000000..c804735a0e
--- /dev/null
+++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/BaseWebActivityBundle.kt
@@ -0,0 +1,17 @@
+package me.devsaki.hentoid.activities.bundles
+
+import android.os.Bundle
+import me.devsaki.hentoid.util.string
+
+/**
+ * Helper class to transfer data from any Activity to {@link me.devsaki.hentoid.activities.PrefsActivity}
+ * through a Bundle
+ */
+class BaseWebActivityBundle(private val bundle: Bundle) {
+
+ constructor() : this(Bundle())
+
+ var url by bundle.string(default = "")
+
+ fun toBundle() = bundle
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImportActivityBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImportActivityBundle.java
deleted file mode 100644
index 6d522988a4..0000000000
--- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImportActivityBundle.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package me.devsaki.hentoid.activities.bundles;
-
-import android.os.Bundle;
-
-import javax.annotation.Nonnull;
-
-/**
- * Helper class to transfer data from any Activity to {@link me.devsaki.hentoid.workers.ImportWorker}
- * through a Bundle
- *
- * Use Builder class to set data; use Parser class to get data
- */
-public class ImportActivityBundle {
- private static final String KEY_REFRESH = "refresh";
- private static final String KEY_REFRESH_RENAME = "rename";
- private static final String KEY_REFRESH_CLEAN_NO_JSON = "cleanNoJson";
- private static final String KEY_REFRESH_CLEAN_NO_IMAGES = "cleanNoImages";
-
- private ImportActivityBundle() {
- throw new UnsupportedOperationException();
- }
-
- public static final class Builder {
-
- private final Bundle bundle = new Bundle();
-
- public void setRefresh(boolean refresh) {
- bundle.putBoolean(KEY_REFRESH, refresh);
- }
-
- public void setRefreshRename(boolean rename) {
- bundle.putBoolean(KEY_REFRESH_RENAME, rename);
- }
-
- public void setRefreshCleanNoJson(boolean refresh) {
- bundle.putBoolean(KEY_REFRESH_CLEAN_NO_JSON, refresh);
- }
-
- public void setRefreshCleanNoImages(boolean refresh) {
- bundle.putBoolean(KEY_REFRESH_CLEAN_NO_IMAGES, refresh);
- }
-
- public Bundle getBundle() {
- return bundle;
- }
- }
-
- public static final class Parser {
-
- private final Bundle bundle;
-
- public Parser(@Nonnull Bundle bundle) {
- this.bundle = bundle;
- }
-
- public boolean getRefresh() {
- return bundle.getBoolean(KEY_REFRESH, false);
- }
-
- public boolean getRefreshRename() {
- return bundle.getBoolean(KEY_REFRESH_RENAME, false);
- }
-
- public boolean getRefreshCleanNoJson() {
- return bundle.getBoolean(KEY_REFRESH_CLEAN_NO_JSON, false);
- }
-
- public boolean getRefreshCleanNoImages() {
- return bundle.getBoolean(KEY_REFRESH_CLEAN_NO_IMAGES, false);
- }
- }
-}
diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/PrefsActivityBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/PrefsActivityBundle.java
deleted file mode 100644
index a87c2769e2..0000000000
--- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/PrefsActivityBundle.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package me.devsaki.hentoid.activities.bundles;
-
-import android.os.Bundle;
-
-import javax.annotation.Nonnull;
-
-/**
- * Helper class to transfer data from any Activity to {@link me.devsaki.hentoid.activities.PrefsActivity}
- * through a Bundle
- *
- * Use Builder class to set data; use Parser class to get data
- */
-public class PrefsActivityBundle {
- private static final String KEY_IS_VIEWER_PREFS = "isViewer";
- private static final String KEY_IS_BROWSER_PREFS = "isBrowser";
- private static final String KEY_IS_DOWNLOADER_PREFS = "isDownloader";
- private static final String KEY_IS_STORAGE_PREFS = "isStorage";
-
- private PrefsActivityBundle() {
- throw new UnsupportedOperationException();
- }
-
- public static final class Builder {
-
- private final Bundle bundle = new Bundle();
-
- public void setIsViewerPrefs(boolean value) {
- bundle.putBoolean(KEY_IS_VIEWER_PREFS, value);
- }
-
- public void setIsBrowserPrefs(boolean value) {
- bundle.putBoolean(KEY_IS_BROWSER_PREFS, value);
- }
-
- public void setIsDownloaderPrefs(boolean value) {
- bundle.putBoolean(KEY_IS_DOWNLOADER_PREFS, value);
- }
-
- public void setIsStoragePrefs(boolean value) {
- bundle.putBoolean(KEY_IS_STORAGE_PREFS, value);
- }
-
- public Bundle getBundle() {
- return bundle;
- }
- }
-
- public static final class Parser {
-
- private final Bundle bundle;
-
- public Parser(@Nonnull Bundle bundle) {
- this.bundle = bundle;
- }
-
- public boolean isViewerPrefs() {
- return bundle.getBoolean(KEY_IS_VIEWER_PREFS, false);
- }
-
- public boolean isBrowserPrefs() {
- return bundle.getBoolean(KEY_IS_BROWSER_PREFS, false);
- }
-
- public boolean isDownloaderPrefs() {
- return bundle.getBoolean(KEY_IS_DOWNLOADER_PREFS, false);
- }
-
- public boolean isStoragePrefs() {
- return bundle.getBoolean(KEY_IS_STORAGE_PREFS, false);
- }
- }
-}
diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/PrefsBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/PrefsBundle.kt
new file mode 100644
index 0000000000..88660f0cfd
--- /dev/null
+++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/PrefsBundle.kt
@@ -0,0 +1,23 @@
+package me.devsaki.hentoid.activities.bundles
+
+import android.os.Bundle
+import me.devsaki.hentoid.util.boolean
+
+/**
+ * Helper class to transfer data from any Activity to {@link me.devsaki.hentoid.activities.PrefsActivity}
+ * through a Bundle
+ */
+class PrefsBundle(private val bundle: Bundle) {
+
+ constructor() : this(Bundle())
+
+ var isViewerPrefs by bundle.boolean(default = false)
+
+ var isBrowserPrefs by bundle.boolean(default = false)
+
+ var isDownloaderPrefs by bundle.boolean(default = false)
+
+ var isStoragePrefs by bundle.boolean(default = false)
+
+ fun toBundle() = bundle
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java
index 85f905e815..6a353510a2 100644
--- a/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java
+++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java
@@ -66,6 +66,7 @@
import java.util.Map;
import java.util.Set;
+import io.objectbox.relation.ToOne;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@@ -77,7 +78,7 @@
import me.devsaki.hentoid.activities.PrefsActivity;
import me.devsaki.hentoid.activities.QueueActivity;
import me.devsaki.hentoid.activities.bundles.BaseWebActivityBundle;
-import me.devsaki.hentoid.activities.bundles.PrefsActivityBundle;
+import me.devsaki.hentoid.activities.bundles.PrefsBundle;
import me.devsaki.hentoid.activities.bundles.QueueActivityBundle;
import me.devsaki.hentoid.database.CollectionDAO;
import me.devsaki.hentoid.database.ObjectBoxDAO;
@@ -312,8 +313,8 @@ protected void onCreate(Bundle savedInstanceState) {
private String getStartUrl() {
// Priority 1 : URL specifically given to the activity (e.g. "view source" action)
if (getIntent().getExtras() != null) {
- BaseWebActivityBundle.Parser parser = new BaseWebActivityBundle.Parser(getIntent().getExtras());
- String intentUrl = parser.getUrl();
+ BaseWebActivityBundle bundle = new BaseWebActivityBundle(getIntent().getExtras());
+ String intentUrl = StringHelper.protect(bundle.getUrl());
if (!intentUrl.isEmpty()) return intentUrl;
}
@@ -397,9 +398,9 @@ protected void onSaveInstanceState(@NonNull Bundle outState) {
// NB : This doesn't restore the browsing history, but WebView.saveState/restoreState
// doesn't work that well (bugged when using back/forward commands). A valid solution still has to be found
- BaseWebActivityBundle.Builder builder = new BaseWebActivityBundle.Builder();
- builder.setUrl(webView.getUrl());
- outState.putAll(builder.getBundle());
+ BaseWebActivityBundle bundle = new BaseWebActivityBundle();
+ bundle.setUrl(webView.getUrl());
+ outState.putAll(bundle.toBundle());
}
@Override
@@ -408,7 +409,7 @@ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
// NB : This doesn't restore the browsing history, but WebView.saveState/restoreState
// doesn't work that well (bugged when using back/forward commands). A valid solution still has to be found
- String url = new BaseWebActivityBundle.Parser(savedInstanceState).getUrl();
+ String url = new BaseWebActivityBundle(savedInstanceState).getUrl();
if (url != null && !url.isEmpty())
webView.loadUrl(url);
}
@@ -872,7 +873,7 @@ void processDownload(boolean quickDownload, boolean isDownloadPlus) {
}
if (isDownloadPlus) {
- // Copy the _current_ content's download params to the images
+ // Copy the _current_ content's download params to the extra images
String downloadParamsStr = currentContent.getDownloadParams();
if (downloadParamsStr != null && downloadParamsStr.length() > 2) {
for (ImageFile i : extraImages) i.setDownloadParams(downloadParamsStr);
@@ -884,20 +885,40 @@ void processDownload(boolean quickDownload, boolean isDownloadPlus) {
if (null == currentContent) return;
}
- // Append additional pages to the base book's list of pages
+ // Append additional pages & chapters to the base book's list of pages & chapters
List updatedImgs = new ArrayList<>(); // Entire image set to update
- Set existingUrls = new HashSet<>(); // URLs of known images
+ Set existingImageUrls = new HashSet<>(); // URLs of known images
+ Set existingChapterOrders = new HashSet<>(); // Positions of known chapters
if (currentContent.getImageFiles() != null) {
- existingUrls.addAll(Stream.of(currentContent.getImageFiles()).map(ImageFile::getUrl).toList());
+ existingImageUrls.addAll(Stream.of(currentContent.getImageFiles()).map(ImageFile::getUrl).toList());
+ existingChapterOrders.addAll(Stream.of(currentContent.getImageFiles()).map(i -> {
+ if (null == i.getChapter()) return -1;
+ if (null == i.getChapter().getTarget()) return -1;
+ return i.getChapter().getTarget().getOrder();
+ }).toList());
updatedImgs.addAll(currentContent.getImageFiles());
}
- // Save additional detected pages references to base book, without duplicate URLs
- List additionalNonExistingImages = Stream.of(extraImages).filterNot(i -> existingUrls.contains(i.getUrl())).toList();
+ // Save additional pages references to stored book, without duplicate URLs
+ List additionalNonExistingImages = Stream.of(extraImages).filterNot(i -> existingImageUrls.contains(i.getUrl())).toList();
if (!additionalNonExistingImages.isEmpty()) {
updatedImgs.addAll(additionalNonExistingImages);
currentContent.setImageFiles(updatedImgs);
}
+ // Save additional chapters to stored book
+ List additionalNonExistingChapters = Stream.of(additionalNonExistingImages)
+ .map(ImageFile::getChapter).withoutNulls()
+ .map(ToOne::getTarget).withoutNulls()
+ .filterNot(c -> existingChapterOrders.contains(c.getOrder())).toList();
+ if (!additionalNonExistingChapters.isEmpty()) {
+ List updatedChapters;
+ if (currentContent.getChapters() != null)
+ updatedChapters = new ArrayList<>(currentContent.getChapters());
+ else
+ updatedChapters = new ArrayList<>();
+ updatedChapters.addAll(additionalNonExistingChapters);
+ currentContent.setChapters(updatedChapters);
+ }
currentContent.setStatus(StatusContent.SAVED);
dao.insertContent(currentContent);
@@ -1150,9 +1171,9 @@ private List doSearchForExtraImages(@NonNull final Content storedCont
positionMap.put(img.getOrder(), img.getLinkedChapter());
}
+ // Attach chapters to stored images if they don't have any (old downloads made with versions of the app that didn't detect chapters)
List storedChapters = storedContent.getChapters();
if (!positionMap.isEmpty() && minOnlineImageOrder < maxStoredImageOrder && (null == storedChapters || storedChapters.isEmpty())) {
- // Attach chapters to stored images
List storedImages = storedContent.getImageFiles();
if (null == storedImages) storedImages = Collections.emptyList();
for (ImageFile img : storedImages) {
@@ -1287,9 +1308,9 @@ private String formatAlertMessage(@NonNull final UpdateInfo.SourceAlert alert) {
private void onSettingsClick() {
Intent intent = new Intent(this, PrefsActivity.class);
- PrefsActivityBundle.Builder builder = new PrefsActivityBundle.Builder();
- builder.setIsBrowserPrefs(true);
- intent.putExtras(builder.getBundle());
+ PrefsBundle prefsBundle = new PrefsBundle();
+ prefsBundle.setBrowserPrefs(true);
+ intent.putExtras(prefsBundle.toBundle());
startActivity(intent);
}
diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/CustomWebViewClient.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/CustomWebViewClient.java
index 4bfaf56a14..248491e1d7 100644
--- a/app/src/main/java/me/devsaki/hentoid/activities/sources/CustomWebViewClient.java
+++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/CustomWebViewClient.java
@@ -502,7 +502,7 @@ protected WebResourceResponse parseResponse(@NonNull String urlStr, @Nullable Ma
targetUrl = StringHelper.protect(response.header("Location"));
if (BuildConfig.DEBUG)
Timber.v("WebView : redirection from %s to %s", urlStr, targetUrl);
- if (!targetUrl.isEmpty()) browserLoad(targetUrl);
+ if (!targetUrl.isEmpty()) browserLoad(HttpHelper.fixUrl(targetUrl, site.getUrl()));
return null;
}
diff --git a/app/src/main/java/me/devsaki/hentoid/core/AppStartup.java b/app/src/main/java/me/devsaki/hentoid/core/AppStartup.java
index cd256ecb19..49d01b3bb6 100644
--- a/app/src/main/java/me/devsaki/hentoid/core/AppStartup.java
+++ b/app/src/main/java/me/devsaki/hentoid/core/AppStartup.java
@@ -70,7 +70,10 @@ public void initApp(
@NonNull Consumer onSecondaryProgress,
@NonNull Runnable onComplete
) {
- if (isInitialized) onComplete.run();
+ if (isInitialized) {
+ onComplete.run();
+ return;
+ }
// Wait until pre-launch tasks are completed
launchTasks = getPreLaunchTasks(context);
diff --git a/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java b/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java
index 634fb366de..2c4e47dd5b 100644
--- a/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java
+++ b/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java
@@ -4,6 +4,7 @@
import androidx.annotation.NonNull;
+import com.annimon.stream.Optional;
import com.annimon.stream.Stream;
import org.apache.commons.lang3.tuple.ImmutableTriple;
@@ -17,6 +18,7 @@
import io.reactivex.ObservableEmitter;
import io.reactivex.functions.BiConsumer;
import me.devsaki.hentoid.database.domains.Attribute;
+import me.devsaki.hentoid.database.domains.Chapter;
import me.devsaki.hentoid.database.domains.Content;
import me.devsaki.hentoid.database.domains.Group;
import me.devsaki.hentoid.database.domains.GroupItem;
@@ -45,9 +47,12 @@ public static List> getPreLaunchCleanupTasks(@NonNull final Co
result.add(createObservableFrom(context, DatabaseMaintenance::cleanPropertiesOneShot1));
result.add(createObservableFrom(context, DatabaseMaintenance::cleanPropertiesOneShot2));
result.add(createObservableFrom(context, DatabaseMaintenance::cleanPropertiesOneShot3));
+ result.add(createObservableFrom(context, DatabaseMaintenance::cleanPropertiesOneShot4));
+ result.add(createObservableFrom(context, DatabaseMaintenance::renameEmptyChapters));
result.add(createObservableFrom(context, DatabaseMaintenance::computeContentSize));
result.add(createObservableFrom(context, DatabaseMaintenance::createGroups));
result.add(createObservableFrom(context, DatabaseMaintenance::computeReadingProgress));
+ result.add(createObservableFrom(context, DatabaseMaintenance::reattachGroupCovers));
return result;
}
@@ -196,6 +201,61 @@ private static void cleanPropertiesOneShot3(@NonNull final Context context, Obse
}
}
+ private static void cleanPropertiesOneShot4(@NonNull final Context context, ObservableEmitter emitter) {
+ ObjectBoxDB db = ObjectBoxDB.getInstance(context);
+ try {
+ // Update URLs from deprecated Hitomi image covers
+ Timber.i("Fixing M18 covers : start");
+ List contents = db.selectDownloadedM18Books();
+ contents = Stream.of(contents).filter(DatabaseMaintenance::isM18WrongCover).toList();
+ Timber.i("Fixing M18 covers : %s books detected", contents.size());
+ int max = contents.size();
+ float pos = 1;
+ for (Content c : contents) {
+ List images = c.getImageFiles();
+ if (null != images) {
+ ImageFile newCover = ImageFile.newCover(c.getCoverImageUrl(), StatusContent.ONLINE).setContentId(c.getId());
+ images.add(0, newCover);
+ images.get(1).setIsCover(false);
+ db.insertImageFiles(images);
+ }
+ emitter.onNext(pos++ / max);
+ }
+ Timber.i("Fixing M18 covers : done");
+ } finally {
+ db.closeThreadResources();
+ emitter.onComplete();
+ }
+ }
+
+ private static boolean isM18WrongCover(@NonNull Content c) {
+ List images = c.getImageFiles();
+ if (null == images || images.isEmpty()) return false;
+ Optional cover = Stream.of(images).filter(ImageFile::isCover).findFirst();
+ return (cover.isEmpty() || (cover.get().getOrder() == 1 && !cover.get().getUrl().equals(c.getCoverImageUrl())));
+ }
+
+ private static void renameEmptyChapters(@NonNull final Context context, ObservableEmitter emitter) {
+ ObjectBoxDB db = ObjectBoxDB.getInstance(context);
+ try {
+ // Update URLs from deprecated Hitomi image covers
+ Timber.i("Empying empty chapters : start");
+ List chapters = db.selecChaptersEmptyName();
+ Timber.i("Empying empty chapters : %s chapters detected", chapters.size());
+ int max = chapters.size();
+ float pos = 1;
+ for (Chapter c : chapters) {
+ c.setName("Chapter " + (c.getOrder() + 1)); // 0-indexed
+ emitter.onNext(pos++ / max);
+ }
+ db.insertChapters(chapters);
+ Timber.i("Empying empty chapters : done");
+ } finally {
+ db.closeThreadResources();
+ emitter.onComplete();
+ }
+ }
+
private static void cleanBookmarksOneShot(@NonNull final Context context, ObservableEmitter emitter) {
ObjectBoxDB db = ObjectBoxDB.getInstance(context);
try {
@@ -296,7 +356,7 @@ private static void createGroups(@NonNull final Context context, ObservableEmitt
Group group = new Group(Grouping.ARTIST, a.getName(), order++);
group.setSubtype(a.getType().equals(AttributeType.ARTIST) ? Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS : Preferences.Constant.ARTIST_GROUP_VISIBILITY_GROUPS);
if (!a.contents.isEmpty())
- group.picture.setTarget(a.contents.get(0).getCover());
+ group.coverContent.setTarget(a.contents.get(0));
bookInsertCount += a.contents.size();
toInsert.add(new ImmutableTriple<>(group, a, Stream.of(a.contents).map(Content::getId).toList()));
@@ -373,6 +433,30 @@ private static void computeReadingProgress(@NonNull final Context context, Obser
}
}
+ private static void reattachGroupCovers(@NonNull final Context context, ObservableEmitter emitter) {
+ ObjectBoxDB db = ObjectBoxDB.getInstance(context);
+ try {
+ // Compute missing downloaded Content size according to underlying ImageFile sizes
+ Timber.i("Reattaching group covers : start");
+ List groups = db.selecGroupsWithNoCoverContent();
+ Timber.i("Reattaching group covers : %s groups detected", groups.size());
+ int max = groups.size();
+ float pos = 1;
+ for (Group g : groups) {
+ List contentIds = g.getContentIds();
+ if (!contentIds.isEmpty()) {
+ g.coverContent.setTargetId(contentIds.get(0));
+ db.insertGroup(g);
+ }
+ emitter.onNext(pos++ / max);
+ }
+ Timber.i("Reattaching group covers : done");
+ } finally {
+ db.closeThreadResources();
+ emitter.onComplete();
+ }
+ }
+
private static void cleanOrphanAttributes(@NonNull final Context context, ObservableEmitter emitter) {
ObjectBoxDB db = ObjectBoxDB.getInstance(context);
try {
diff --git a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.java b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.java
index c200c98d35..071d1b2815 100644
--- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.java
+++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.java
@@ -27,6 +27,7 @@
import io.objectbox.android.ObjectBoxDataSource;
import io.objectbox.android.ObjectBoxLiveData;
import io.objectbox.query.Query;
+import io.objectbox.relation.ToMany;
import io.objectbox.relation.ToOne;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -469,7 +470,7 @@ public LiveData> selectGroupsLive(
private Group enrichGroupWithItemsByDlDate(@NonNull final Group g, int minDays, int maxDays) {
List items = selectGroupItemsByDlDate(g, minDays, maxDays);
g.setItems(items);
- if (!items.isEmpty()) g.picture.setTarget(items.get(0).content.getTarget().getCover());
+ if (!items.isEmpty()) g.coverContent.setTarget(items.get(0).content.getTarget());
return g;
}
@@ -545,9 +546,9 @@ public long insertGroupItem(GroupItem item) {
item.order = db.getMaxGroupItemOrderFor(item.getGroupId()) + 1;
// If target group doesn't have a cover, get the corresponding Content's
- ToOne groupCover = item.group.getTarget().picture;
- if (!groupCover.isResolvedAndNotNull())
- groupCover.setAndPutTarget(item.content.getTarget().getCover());
+ ToOne groupCoverContent = item.group.getTarget().coverContent;
+ if (!groupCoverContent.isResolvedAndNotNull())
+ groupCoverContent.setAndPutTarget(item.content.getTarget());
return db.insertGroupItem(item);
}
@@ -565,10 +566,10 @@ public void deleteGroupItems(@NonNull final List groupItemIds) {
// Check if one of the GroupItems to delete is linked to the content that contains the group's cover picture
List groupItems = db.selectGroupItems(Helper.getPrimitiveArrayFromList(groupItemIds));
for (GroupItem gi : groupItems) {
- ToOne groupPicture = gi.group.getTarget().picture;
+ ToOne groupCoverContent = gi.group.getTarget().coverContent;
// If so, remove the cover picture
- if (groupPicture.isResolvedAndNotNull() && groupPicture.getTarget().getContent().getTargetId() == gi.content.getTargetId())
- gi.group.getTarget().picture.setAndPutTarget(null);
+ if (groupCoverContent.isResolvedAndNotNull() && groupCoverContent.getTargetId() == gi.content.getTargetId())
+ gi.group.getTarget().coverContent.setAndPutTarget(null);
}
db.deleteGroupItems(Helper.getPrimitiveArrayFromList(groupItemIds));
diff --git a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java
index 7139239aa0..890c1a6a72 100644
--- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java
+++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java
@@ -1492,6 +1492,14 @@ List selectContentWithOldHitomiCovers() {
return store.boxFor(Content.class).query().equal(Content_.site, Site.HITOMI.getCode()).contains(Content_.coverImageUrl, "/smallbigtn/", QueryBuilder.StringOrder.CASE_INSENSITIVE).build().find();
}
+ List selectDownloadedM18Books() {
+ return store.boxFor(Content.class).query().equal(Content_.site, Site.MANHWA18.getCode()).in(Content_.status, libraryStatus).build().find();
+ }
+
+ List selecChaptersEmptyName() {
+ return store.boxFor(Chapter.class).query().equal(Chapter_.name, "", QueryBuilder.StringOrder.CASE_INSENSITIVE).build().find();
+ }
+
List selectDownloadedContentWithNoSize() {
return store.boxFor(Content.class).query().in(Content_.status, libraryStatus).isNull(Content_.size).build().find();
}
@@ -1500,6 +1508,10 @@ List selectDownloadedContentWithNoReadProgress() {
return store.boxFor(Content.class).query().in(Content_.status, libraryStatus).isNull(Content_.readProgress).build().find();
}
+ List selecGroupsWithNoCoverContent() {
+ return store.boxFor(Group.class).query().isNull(Group_.coverContentId).build().find();
+ }
+
List selectContentWithNullCompleteField() {
return store.boxFor(Content.class).query().isNull(Content_.completed).build().find();
}
diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/Content.java b/app/src/main/java/me/devsaki/hentoid/database/domains/Content.java
index 1a29ff2138..d6bb59060c 100644
--- a/app/src/main/java/me/devsaki/hentoid/database/domains/Content.java
+++ b/app/src/main/java/me/devsaki/hentoid/database/domains/Content.java
@@ -813,7 +813,7 @@ public void increaseNumberDownloadRetries() {
}
public boolean isArchive() {
- return ArchiveHelper.isSupportedArchive(getStorageUri()); // Warning : this shortcut assumes the URI contains the file name, which is not guaranteed !
+ return ArchiveHelper.isSupportedArchive(getStorageUri()); // Warning : this shortcut assumes the URI contains the file name, which is not guaranteed (not in any spec) !
}
public String getArchiveLocationUri() {
diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/Group.java b/app/src/main/java/me/devsaki/hentoid/database/domains/Group.java
index d655125881..11dc90e0d1 100644
--- a/app/src/main/java/me/devsaki/hentoid/database/domains/Group.java
+++ b/app/src/main/java/me/devsaki/hentoid/database/domains/Group.java
@@ -29,7 +29,10 @@ public class Group {
public String name;
@Backlink(to = "group")
public ToMany items;
- public ToOne picture;
+ // Targetting the content instead of the picture itself because
+ // 1- That's the logic of the UI
+ // 2- Pictures within a given Content are sometimes entirely replaced, breaking that link
+ public ToOne coverContent;
// in Grouping.ARTIST : 0 = Artist; 1 = Group
// in Grouping.CUSTOM : 0 = Custom; 1 = Ungrouped
public int subtype;
diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/ImageFile.java b/app/src/main/java/me/devsaki/hentoid/database/domains/ImageFile.java
index 6cfe592cf0..4e397d987a 100644
--- a/app/src/main/java/me/devsaki/hentoid/database/domains/ImageFile.java
+++ b/app/src/main/java/me/devsaki/hentoid/database/domains/ImageFile.java
@@ -45,6 +45,8 @@ public class ImageFile {
// Temporary attributes during SAVED state only; no need to expose them for JSON persistence
private String downloadParams = "";
+ // WARNING : Update copy constructor when adding attributes
+
// Runtime attributes; no need to expose them nor to persist them
@@ -58,6 +60,9 @@ public class ImageFile {
@Transient
private boolean isBackup = false;
+ // WARNING : Update copy constructor when adding attributes
+
+
public ImageFile() { // Required by ObjectBox when an alternate constructor exists
}
@@ -78,6 +83,7 @@ public ImageFile(ImageFile img) {
this.size = img.size;
this.imageHash = img.imageHash;
this.downloadParams = img.downloadParams;
+
this.displayOrder = img.displayOrder;
this.backupUrl = img.backupUrl;
this.isBackup = img.isBackup;
@@ -236,8 +242,9 @@ public ImageFile setMimeType(String mimeType) {
return this;
}
- public void setContentId(long contentId) {
+ public ImageFile setContentId(long contentId) {
this.content.setTargetId(contentId);
+ return this;
}
public long getSize() {
diff --git a/app/src/main/java/me/devsaki/hentoid/enums/StatusContent.java b/app/src/main/java/me/devsaki/hentoid/enums/StatusContent.java
index 7198024e89..194a6e6e61 100644
--- a/app/src/main/java/me/devsaki/hentoid/enums/StatusContent.java
+++ b/app/src/main/java/me/devsaki/hentoid/enums/StatusContent.java
@@ -18,7 +18,7 @@ public enum StatusContent {
IGNORED(6, "Ignored"), // Transient status set by the web parser to indicate a content page that cannot be parsed
UNHANDLED_ERROR(7, "Unhandled Error"), // Default status for image files
CANCELED(8, "Canceled"), // Unused value; kept for retrocompatibility
- ONLINE(9, "Online"), // Used for ImageFiles only : image can be viewed on-demand (streamed content)
+ ONLINE(9, "Online"), // Used for ImageFiles only : image can be viewed on-demand (streamed content; undownloaded covers)
EXTERNAL(10, "External"); // Content is accessible in the external library
private final int code;
diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.java
index 050f39d8ff..f06b8ae114 100644
--- a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.java
+++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.java
@@ -867,6 +867,7 @@ private void askSetCover() {
if (selectedItems.isEmpty()) return;
Content content = Stream.of(selectedItems).findFirst().get().getContent();
+ if (null == content) return;
new MaterialAlertDialogBuilder(requireContext(), ThemeHelper.getIdForCurrentTheme(requireContext(), R.style.Theme_Light_Dialog))
.setCancelable(false)
@@ -875,7 +876,7 @@ private void askSetCover() {
.setPositiveButton(R.string.yes,
(dialog1, which) -> {
dialog1.dismiss();
- viewModel.setGroupCover(group.id, content.getCover());
+ viewModel.setGroupCoverContent(group.id, content);
leaveSelectionMode();
})
.setNegativeButton(R.string.no,
@@ -1465,9 +1466,11 @@ private void onSelectionChanged() {
activity.get().getSelectionToolbar().setVisibility(View.GONE);
selectExtension.setSelectOnLongClick(true);
} else {
- long selectedLocalCount = Stream.of(selectedItems).map(ContentItem::getContent).withoutNulls().map(Content::getStatus).filterNot(s -> s.equals(StatusContent.EXTERNAL)).count();
- long selectedStreamedCount = Stream.of(selectedItems).map(ContentItem::getContent).withoutNulls().map(Content::getDownloadMode).filter(m -> m == Content.DownloadMode.STREAM).count();
- activity.get().updateSelectionToolbar(selectedCount, selectedLocalCount, selectedStreamedCount);
+ List contentList = Stream.of(selectedItems).map(ContentItem::getContent).withoutNulls().toList();
+ long selectedLocalCount = Stream.of(contentList).map(Content::getStatus).filterNot(s -> s.equals(StatusContent.EXTERNAL)).count();
+ long selectedStreamedCount = Stream.of(contentList).map(Content::getDownloadMode).filter(m -> m == Content.DownloadMode.STREAM).count();
+ long selectedEligibleExternalCount = Stream.of(contentList).filter(c -> c.getStatus().equals(StatusContent.EXTERNAL) && !c.isArchive()).count();
+ activity.get().updateSelectionToolbar(selectedCount, selectedLocalCount, selectedStreamedCount, selectedEligibleExternalCount);
activity.get().getSelectionToolbar().setVisibility(View.VISIBLE);
}
}
diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.java
index b55440bc37..edeb09b26a 100644
--- a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.java
+++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.java
@@ -69,7 +69,7 @@
import me.devsaki.hentoid.activities.LibraryActivity;
import me.devsaki.hentoid.activities.PrefsActivity;
import me.devsaki.hentoid.activities.bundles.GroupItemBundle;
-import me.devsaki.hentoid.activities.bundles.PrefsActivityBundle;
+import me.devsaki.hentoid.activities.bundles.PrefsBundle;
import me.devsaki.hentoid.database.domains.Content;
import me.devsaki.hentoid.database.domains.Group;
import me.devsaki.hentoid.enums.Site;
@@ -143,7 +143,7 @@ public boolean areItemsTheSame(GroupDisplayItem oldItem, GroupDisplayItem newIte
@Override
public boolean areContentsTheSame(GroupDisplayItem oldItem, GroupDisplayItem newItem) {
- return oldItem.getGroup().picture.getTargetId() == newItem.getGroup().picture.getTargetId()
+ return oldItem.getGroup().coverContent.getTargetId() == newItem.getGroup().coverContent.getTargetId()
&& oldItem.getGroup().isFavourite() == newItem.getGroup().isFavourite()
&& oldItem.getGroup().items.size() == newItem.getGroup().items.size();
}
@@ -152,8 +152,8 @@ public boolean areContentsTheSame(GroupDisplayItem oldItem, GroupDisplayItem new
public @org.jetbrains.annotations.Nullable Object getChangePayload(GroupDisplayItem oldItem, int oldPos, GroupDisplayItem newItem, int newPos) {
GroupItemBundle.Builder diffBundleBuilder = new GroupItemBundle.Builder();
- if (!newItem.getGroup().picture.isNull() && oldItem.getGroup().picture.getTargetId() != newItem.getGroup().picture.getTargetId()) {
- diffBundleBuilder.setCoverUri(newItem.getGroup().picture.getTarget().getUsableUri());
+ if (!newItem.getGroup().coverContent.isNull() && oldItem.getGroup().coverContent.getTargetId() != newItem.getGroup().coverContent.getTargetId()) {
+ diffBundleBuilder.setCoverUri(newItem.getGroup().coverContent.getTarget().getCover().getUsableUri());
}
if (oldItem.getGroup().isFavourite() != newItem.getGroup().isFavourite()) {
diffBundleBuilder.setFavourite(newItem.getGroup().isFavourite());
@@ -486,9 +486,9 @@ private void deleteSelectedItems() {
// Open prefs on the "storage" category
Intent intent = new Intent(requireActivity(), PrefsActivity.class);
- PrefsActivityBundle.Builder builder = new PrefsActivityBundle.Builder();
- builder.setIsStoragePrefs(true);
- intent.putExtras(builder.getBundle());
+ PrefsBundle prefsBundle = new PrefsBundle();
+ prefsBundle.setStoragePrefs(true);
+ intent.putExtras(prefsBundle.toBundle());
requireContext().startActivity(intent);
});
@@ -795,7 +795,7 @@ private void onSelectionChanged() {
selectExtension.setSelectOnLongClick(true);
} else {
long selectedLocalCount = Stream.of(selectedItems).map(GroupDisplayItem::getGroup).withoutNulls().count();
- activity.get().updateSelectionToolbar(selectedCount, selectedLocalCount, 0);
+ activity.get().updateSelectionToolbar(selectedCount, selectedLocalCount, 0, 0);
activity.get().getSelectionToolbar().setVisibility(View.VISIBLE);
}
}
diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/MergeDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/library/MergeDialogFragment.java
index ba4e137fe7..64e5b48cfa 100644
--- a/app/src/main/java/me/devsaki/hentoid/fragments/library/MergeDialogFragment.java
+++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/MergeDialogFragment.java
@@ -29,7 +29,9 @@
import me.devsaki.hentoid.database.ObjectBoxDAO;
import me.devsaki.hentoid.database.domains.Content;
import me.devsaki.hentoid.databinding.DialogLibraryMergeBinding;
+import me.devsaki.hentoid.enums.StatusContent;
import me.devsaki.hentoid.util.Helper;
+import me.devsaki.hentoid.util.Preferences;
import me.devsaki.hentoid.viewholders.IDraggableViewHolder;
import me.devsaki.hentoid.viewholders.TextItem;
@@ -110,6 +112,7 @@ public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstance
List contentList = loadContentList();
if (contentList.isEmpty()) return;
+ boolean isExternal = contentList.get(0).getStatus().equals(StatusContent.EXTERNAL);
itemAdapter.set(Stream.of(contentList).map(s -> new TextItem<>(s.getTitle(), s, true, false, false, touchHelper)).toList());
@@ -133,7 +136,13 @@ public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstance
newTitleTxt = binding.titleNew.getEditText();
if (newTitleTxt != null) newTitleTxt.setText(initialTitle);
- binding.mergeDeleteSwitch.setChecked(deleteDefault);
+ if (isExternal) {
+ binding.mergeDeleteSwitch.setEnabled(Preferences.isDeleteExternalLibrary());
+ binding.mergeDeleteSwitch.setChecked(Preferences.isDeleteExternalLibrary() && deleteDefault);
+ } else {
+ binding.mergeDeleteSwitch.setEnabled(true);
+ binding.mergeDeleteSwitch.setChecked(deleteDefault);
+ }
binding.actionButton.setOnClickListener(v -> onActionClick());
}
diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/queue/QueueFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/queue/QueueFragment.java
index 3648f01dc7..3864c2482c 100644
--- a/app/src/main/java/me/devsaki/hentoid/fragments/queue/QueueFragment.java
+++ b/app/src/main/java/me/devsaki/hentoid/fragments/queue/QueueFragment.java
@@ -62,11 +62,10 @@
import me.devsaki.hentoid.R;
import me.devsaki.hentoid.activities.PrefsActivity;
import me.devsaki.hentoid.activities.QueueActivity;
-import me.devsaki.hentoid.activities.bundles.PrefsActivityBundle;
+import me.devsaki.hentoid.activities.bundles.PrefsBundle;
import me.devsaki.hentoid.database.ObjectBoxDAO;
import me.devsaki.hentoid.database.domains.Content;
import me.devsaki.hentoid.database.domains.QueueRecord;
-import me.devsaki.hentoid.enums.StatusContent;
import me.devsaki.hentoid.events.DownloadEvent;
import me.devsaki.hentoid.events.DownloadPreparationEvent;
import me.devsaki.hentoid.events.ProcessEvent;
@@ -965,9 +964,9 @@ public void itemUnswiped(int position) {
private void onSettingsClick() {
Intent intent = new Intent(requireActivity(), PrefsActivity.class);
- PrefsActivityBundle.Builder builder = new PrefsActivityBundle.Builder();
- builder.setIsDownloaderPrefs(true);
- intent.putExtras(builder.getBundle());
+ PrefsBundle prefsBundle = new PrefsBundle();
+ prefsBundle.setDownloaderPrefs(true);
+ intent.putExtras(prefsBundle.toBundle());
requireContext().startActivity(intent);
}
diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerPrefsDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerPrefsDialogFragment.java
index 83ed0d75cb..771c526bb4 100644
--- a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerPrefsDialogFragment.java
+++ b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerPrefsDialogFragment.java
@@ -26,7 +26,7 @@
import me.devsaki.hentoid.R;
import me.devsaki.hentoid.activities.PrefsActivity;
-import me.devsaki.hentoid.activities.bundles.PrefsActivityBundle;
+import me.devsaki.hentoid.activities.bundles.PrefsBundle;
import me.devsaki.hentoid.util.Preferences;
public final class ViewerPrefsDialogFragment extends DialogFragment {
@@ -120,9 +120,9 @@ public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstance
appSettingsBtn.setOnClickListener(v -> {
Intent intent = new Intent(requireActivity(), PrefsActivity.class);
- PrefsActivityBundle.Builder builder = new PrefsActivityBundle.Builder();
- builder.setIsViewerPrefs(true);
- intent.putExtras(builder.getBundle());
+ PrefsBundle prefsBundle = new PrefsBundle();
+ prefsBundle.setViewerPrefs(true);
+ intent.putExtras(prefsBundle.toBundle());
requireContext().startActivity(intent);
});
diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleriesMetadata.java b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleriesMetadata.java
index b080de70a8..93e8c99093 100644
--- a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleriesMetadata.java
+++ b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleriesMetadata.java
@@ -71,6 +71,9 @@ public Content update(@NonNull Content content, @Nonnull String url, @NonNull Si
case "artist":
type = AttributeType.ARTIST;
break;
+ case "group":
+ type = AttributeType.CIRCLE;
+ break;
default:
type = AttributeType.TAG;
name = s;
diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/ParseHelper.java b/app/src/main/java/me/devsaki/hentoid/parsers/ParseHelper.java
index 41cdb5d602..4b43248708 100644
--- a/app/src/main/java/me/devsaki/hentoid/parsers/ParseHelper.java
+++ b/app/src/main/java/me/devsaki/hentoid/parsers/ParseHelper.java
@@ -13,6 +13,7 @@
import org.jsoup.nodes.Element;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -99,6 +100,9 @@ public static String removeTrailingNumbers(String s) {
return s;
}
+ /**
+ * See definition of the main method below
+ */
public static void parseAttributes(
@NonNull AttributeMap map,
@NonNull AttributeType type,
@@ -109,6 +113,9 @@ public static void parseAttributes(
for (Element a : elements) parseAttribute(map, type, a, removeTrailingNumbers, site);
}
+ /**
+ * See definition of the main method below
+ */
public static void parseAttributes(
@NonNull AttributeMap map,
@NonNull AttributeType type,
@@ -121,15 +128,21 @@ public static void parseAttributes(
parseAttribute(map, type, a, removeTrailingNumbers, childElementClass, site);
}
+ /**
+ * See definition of the main method below
+ */
public static void parseAttribute(
@NonNull AttributeMap map,
@NonNull AttributeType type,
@NonNull Element element,
boolean removeTrailingNumbers,
@NonNull Site site) {
- parseAttribute(map, type, element, removeTrailingNumbers, null, site, "");
+ parseAttribute(element, map, type, site, "", removeTrailingNumbers, null);
}
+ /**
+ * See definition of the main method below
+ */
public static void parseAttribute(
@NonNull AttributeMap map,
@NonNull AttributeType type,
@@ -137,17 +150,26 @@ public static void parseAttribute(
boolean removeTrailingNumbers,
@NonNull String childElementClass,
@NonNull Site site) {
- parseAttribute(map, type, element, removeTrailingNumbers, childElementClass, site, "");
+ parseAttribute(element, map, type, site, "", removeTrailingNumbers, childElementClass);
}
+ /**
+ * Extract Attributes from the given Element and put them into the given AttributeMap,
+ * using the given properties
+ *
+ * @param element Element to parse Attributes from
+ * @param map Output map where the detected attributes will be put
+ * @param type AttributeType to give to the detected Attributes
+ * @param site Site to give to the detected Attributes
+ * @param prefix If set, detected attributes will have this prefix added to their name
+ * @param removeTrailingNumbers If true trailing numbers will be removed from the attribute name
+ * @param childElementClass If set, the parser will look for sub-elements of the given class
+ */
public static void parseAttribute(
- @NonNull AttributeMap map,
+ @NonNull Element element, @NonNull AttributeMap map,
@NonNull AttributeType type,
- @NonNull Element element,
- boolean removeTrailingNumbers,
- @Nullable String childElementClass,
- @NonNull Site site,
- @NonNull final String prefix) {
+ @NonNull Site site, @NonNull final String prefix, boolean removeTrailingNumbers,
+ @Nullable String childElementClass) {
String name;
if (null == childElementClass) {
name = element.ownText();
@@ -167,29 +189,9 @@ public static void parseAttribute(
map.add(attribute);
}
- public static ImageFile urlToImageFile(
- @Nonnull String imgUrl,
- int order,
- int nbPages,
- @NonNull final StatusContent status) {
- return urlToImageFile(imgUrl, order, nbPages, status, null);
- }
-
- public static ImageFile urlToImageFile(
- @Nonnull String imgUrl,
- int order,
- int maxPages,
- @NonNull final StatusContent status,
- final Chapter chapter) {
- ImageFile result = new ImageFile();
-
- int nbMaxDigits = (int) (Math.floor(Math.log10(maxPages)) + 1);
- result.setOrder(order).setUrl(imgUrl).setStatus(status).computeName(nbMaxDigits);
- if (chapter != null) result.setChapter(chapter);
-
- return result;
- }
-
+ /**
+ * See definition of the main method below
+ */
public static List urlsToImageFiles(
@Nonnull List imgUrls,
@NonNull String coverUrl,
@@ -198,6 +200,9 @@ public static List urlsToImageFiles(
return urlsToImageFiles(imgUrls, coverUrl, status, null);
}
+ /**
+ * See definition of the main method below
+ */
public static List urlsToImageFiles(
@Nonnull List imgUrls,
@NonNull String coverUrl,
@@ -207,17 +212,26 @@ public static List urlsToImageFiles(
List result = new ArrayList<>();
result.add(ImageFile.newCover(coverUrl, status));
- result.addAll(urlsToImageFiles(imgUrls, 1, status, chapter, imgUrls.size()));
+ result.addAll(urlsToImageFiles(imgUrls, 1, status, imgUrls.size(), chapter));
return result;
}
+ /**
+ * Build a list of ImageFiles using the given properties
+ *
+ * @param imgUrls URLs of the images
+ * @param initialOrder Order of the 1st image to be generated
+ * @param status Status of the resulting ImageFiles
+ * @param totalBookPages Total number of pages of the corresponding book
+ * @param chapter Chapter to link to the resulting ImageFiles (optional)
+ * @return List of ImageFiles built using all given arguments
+ */
public static List urlsToImageFiles(
@Nonnull List imgUrls,
int initialOrder,
@NonNull final StatusContent status,
- final Chapter chapter,
- int maxPages
+ int totalBookPages, final Chapter chapter
) {
List result = new ArrayList<>();
@@ -225,16 +239,71 @@ public static List urlsToImageFiles(
// Remove duplicates before creationg the ImageFiles
List imgUrlsUnique = Stream.of(imgUrls).distinct().toList();
for (String s : imgUrlsUnique)
- result.add(urlToImageFile(s.trim(), order++, maxPages, status, chapter));
+ result.add(urlToImageFile(s.trim(), order++, totalBookPages, status, chapter));
return result;
}
+ /**
+ * Build an ImageFile using the given properties
+ *
+ * @param imgUrl URL of the image
+ * @param order Order of the image
+ * @param totalBookPages Total number of pages of the corresponding book
+ * @param status Status of the resulting ImageFile
+ * @return ImageFile built using all given arguments
+ */
+ public static ImageFile urlToImageFile(
+ @Nonnull String imgUrl,
+ int order,
+ int totalBookPages,
+ @NonNull final StatusContent status) {
+ return urlToImageFile(imgUrl, order, totalBookPages, status, null);
+ }
- public static void signalProgress(long contentId, long storedId, int current, int max) {
- EventBus.getDefault().post(new DownloadPreparationEvent(contentId, storedId, current, max));
+ /**
+ * Build an ImageFile using the given given properties
+ *
+ * @param imgUrl URL of the image
+ * @param order Order of the image
+ * @param totalBookPages Total number of pages of the corresponding book
+ * @param status Status of the resulting ImageFile
+ * @param chapter Chapter to link to the resulting ImageFile (optional)
+ * @return ImageFile built using all given arguments
+ */
+ public static ImageFile urlToImageFile(
+ @Nonnull String imgUrl,
+ int order,
+ int totalBookPages,
+ @NonNull final StatusContent status,
+ final Chapter chapter) {
+ ImageFile result = new ImageFile();
+
+ int nbMaxDigits = (int) (Math.floor(Math.log10(totalBookPages)) + 1);
+ result.setOrder(order).setUrl(imgUrl).setStatus(status).computeName(nbMaxDigits);
+ if (chapter != null) result.setChapter(chapter);
+
+ return result;
+ }
+
+ /**
+ * Signal download preparation event for the given processed elements
+ *
+ * @param contentId Online content ID being processed
+ * @param storedId Stored content ID being processed
+ * @param currentStep Current processing step
+ * @param maxSteps Maximum processing step
+ */
+ public static void signalProgress(long contentId, long storedId, int currentStep, int maxSteps) {
+ EventBus.getDefault().post(new DownloadPreparationEvent(contentId, storedId, currentStep, maxSteps));
}
+ /**
+ * Extract the cookie string, if it exists, from the given download parameters
+ *
+ * @param downloadParams Download parameters to extract the cookie string from
+ * @return Cookie string, if any in the given download parameters; empty string if none
+ */
public static String getSavedCookieStr(String downloadParams) {
Map downloadParamsMap = ContentHelper.parseDownloadParams(downloadParams);
if (downloadParamsMap.containsKey(HttpHelper.HEADER_COOKIE_KEY))
@@ -243,13 +312,25 @@ public static String getSavedCookieStr(String downloadParams) {
return "";
}
+ /**
+ * Copy the cookie string, if it exists, from the given download parameters to the given HTTP headers
+ *
+ * @param downloadParams Download parameters to extract the cookie string from
+ * @param headers HTTP headers to copy the cookie string to, if it exists
+ */
public static void addSavedCookiesToHeader(String downloadParams, @NonNull List> headers) {
String cookieStr = getSavedCookieStr(downloadParams);
if (!cookieStr.isEmpty())
headers.add(new Pair<>(HttpHelper.HEADER_COOKIE_KEY, cookieStr));
}
- // Save download params for future use during download
+ /**
+ * Save the given referrer and the relevant cookie string as download parameters
+ * to each image of the given list for future use during download
+ *
+ * @param imgs List of images to save download params to
+ * @param referrer Referrer to set
+ */
public static void setDownloadParams(@NonNull final List imgs, @NonNull final String referrer) {
Map params = new HashMap<>();
for (ImageFile img : imgs) {
@@ -261,8 +342,14 @@ public static void setDownloadParams(@NonNull final List imgs, @NonNu
}
}
- // TODO doc
- public static String getExtensionFromFormat(Map imgFormat, int i) {
+ /**
+ * Get the image extension from the given ImHentai / Hentaifox format code
+ *
+ * @param imgFormat Format map provided by the site
+ * @param i index to look up
+ * @return Image extension (without the dot), if found; empty string if not
+ */
+ public static String getExtensionFromFormat(@NonNull Map imgFormat, int i) {
String format = imgFormat.get((i + 1) + "");
if (format != null) {
switch (format.charAt(0)) {
@@ -278,31 +365,56 @@ public static String getExtensionFromFormat(Map imgFormat, int i
} else return "";
}
+ /**
+ * Extract a list of Chapters from the given list of links, for the given Content ID
+ *
+ * @param chapterLinks List of HTML links to extract Chapters from
+ * @param contentId Content ID to associate with all extracted Chapters
+ * @return Chapters detected from the given list of links, associated with the given Content ID
+ */
public static List getChaptersFromLinks(@NonNull List chapterLinks, long contentId) {
List result = new ArrayList<>();
Set urls = new HashSet<>();
- int order = 0;
+ // First extract data and filter URL duplicates
+ List> chapterData = new ArrayList<>();
for (Element e : chapterLinks) {
- String url = e.attr("href");
- String name = StringHelper.removeNonPrintableChars(e.ownText());
+ String url = e.attr("href").trim();
+ String name = e.attr("title").trim();
+ if (name.isEmpty())
+ name = StringHelper.removeNonPrintableChars(e.ownText()).trim();
// Make sure we're not adding duplicates
if (!urls.contains(url)) {
urls.add(url);
- Chapter chp = new Chapter(order++, url, name);
- chp.setContentId(contentId);
- result.add(chp);
+ chapterData.add(new Pair<>(url, name));
}
}
+ Collections.reverse(chapterData); // Put unique results in their chronological order
+
+ int order = 0;
+ // Build the final list
+ for (Pair chapter : chapterData) {
+ Chapter chp = new Chapter(order++, chapter.first, chapter.second);
+ chp.setContentId(contentId);
+ result.add(chp);
+ }
return result;
}
+ /**
+ * Extract the last useful part of the path of the given URL
+ * e.g. if the url is "http://aa.com/look/at/me" or "http://aa.com/look/at/me/", the result will be "me"
+ *
+ * @param url URL to extract from
+ * @return Last useful part of the path of the given URL
+ */
private static String getLastPathPart(@NonNull final String url) {
String[] parts = url.split("/");
return (parts[parts.length - 1].isEmpty()) ? parts[parts.length - 2] : parts[parts.length - 1];
}
+ // TODO doc
public static List getExtraChaptersbyUrl(
@NonNull List storedChapters,
@NonNull List detectedChapters
@@ -323,6 +435,7 @@ public static List getExtraChaptersbyUrl(
return Stream.of(result).sortBy(Chapter::getOrder).toList();
}
+ // TODO doc
public static List getExtraChaptersbyId(
@NonNull List storedChapters,
@NonNull List detectedIds
@@ -339,6 +452,7 @@ public static List getExtraChaptersbyId(
return result;
}
+ // TODO doc
public static int getMaxImageOrder(@NonNull List storedChapters) {
if (!storedChapters.isEmpty()) {
Optional optOrder = Stream.of(storedChapters)
@@ -352,6 +466,7 @@ public static int getMaxImageOrder(@NonNull List storedChapters) {
return 0;
}
+ // TODO doc
public static int getMaxChapterOrder(@NonNull List storedChapters) {
if (!storedChapters.isEmpty()) {
Optional optOrder = Stream.of(storedChapters)
@@ -363,6 +478,12 @@ public static int getMaxChapterOrder(@NonNull List storedChapters) {
return 0;
}
+ /**
+ * Extract the image URL from the given HTML element
+ *
+ * @param e HTML element to extract the URL from
+ * @return Image URL contained in the given HTML element
+ */
public static String getImgSrc(Element e) {
String result = e.attr("data-src").trim();
if (result.isEmpty()) result = e.attr("data-lazy-src").trim();
diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/HbrowseContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/HbrowseContent.java
index c4f5e2fa05..444a66b1d7 100644
--- a/app/src/main/java/me/devsaki/hentoid/parsers/content/HbrowseContent.java
+++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/HbrowseContent.java
@@ -86,9 +86,9 @@ private void addAttribute(@NonNull final Element metaContent, @NonNull Attribute
private void addAttribute(@NonNull final Element metaContent, @NonNull AttributeMap attributes, @NonNull AttributeType type, @NonNull final String prefix) {
if (!metaContent.children().isEmpty()) {
List links = metaContent.select("a");
- if (links != null && !links.isEmpty())
+ if (!links.isEmpty())
for (Element e : links)
- ParseHelper.parseAttribute(attributes, type, e, false, null, Site.HBROWSE, prefix);
+ ParseHelper.parseAttribute(e, attributes, type, Site.HBROWSE, prefix, false, null);
} else
attributes.add(new Attribute(type, prefix.isEmpty() ? "" : prefix + ":" + metaContent.childNode(0).toString(), metaContent.childNode(0).toString(), Site.HBROWSE));
}
diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/Hentai2ReadParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/images/Hentai2ReadParser.java
index 325bfaa723..9e5f8da87f 100644
--- a/app/src/main/java/me/devsaki/hentoid/parsers/images/Hentai2ReadParser.java
+++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/Hentai2ReadParser.java
@@ -53,7 +53,6 @@ public List parseImageListImpl(@NonNull Content onlineContent, @Nulla
if (null == doc) return result;
List chapterLinks = doc.select(".nav-chapters a[href^=" + onlineContent.getGalleryUrl() + "]");
- Collections.reverse(chapterLinks); // Put the chapters in the correct reading order
chapters = ParseHelper.getChaptersFromLinks(chapterLinks, onlineContent.getId());
// If the stored content has chapters already, save them for comparison
@@ -89,7 +88,7 @@ public List parseImageListImpl(@NonNull Content onlineContent, @Nulla
}
}
if (!imageUrls.isEmpty())
- result.addAll(ParseHelper.urlsToImageFiles(imageUrls, imgOffset + result.size() + 1, StatusContent.SAVED, chp, 1000));
+ result.addAll(ParseHelper.urlsToImageFiles(imageUrls, imgOffset + result.size() + 1, StatusContent.SAVED, 1000, chp));
else
Timber.i("Chapter parsing failed for %s : no pictures found", chp.getUrl());
} else {
diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/HitomiParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/images/HitomiParser.java
index 1c3a1d3459..de827c41f0 100644
--- a/app/src/main/java/me/devsaki/hentoid/parsers/images/HitomiParser.java
+++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/HitomiParser.java
@@ -29,6 +29,7 @@
import me.devsaki.hentoid.util.JsonHelper;
import me.devsaki.hentoid.util.Preferences;
import me.devsaki.hentoid.util.StringHelper;
+import me.devsaki.hentoid.util.exception.EmptyResultException;
import me.devsaki.hentoid.util.network.HttpHelper;
import me.devsaki.hentoid.views.HitomiBackgroundWebView;
import okhttp3.Response;
@@ -90,7 +91,11 @@ public List parseImageListWithWebview(@NonNull Content onlineContent,
} while (!done.get() && !processHalted.get() && remainingIterations-- > 0);
if (processHalted.get()) return result;
- String jsResult = imagesStr.get().replace("\"[", "[").replace("]\"", "]").replace("\\\"", "\"");
+ String jsResult = imagesStr.get();
+ if (null == jsResult)
+ throw new EmptyResultException("Unable to detect pages (empty result)");
+
+ jsResult = jsResult.replace("\"[", "[").replace("]\"", "]").replace("\\\"", "\"");
List imageUrls = JsonHelper.jsonToObject(jsResult, JsonHelper.LIST_STRINGS);
if (imageUrls != null && !imageUrls.isEmpty()) {
onlineContent.setCoverImageUrl(imageUrls.get(0));
diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/Manhwa18Parser.java b/app/src/main/java/me/devsaki/hentoid/parsers/images/Manhwa18Parser.java
index 7d70553ce1..78b71b1391 100644
--- a/app/src/main/java/me/devsaki/hentoid/parsers/images/Manhwa18Parser.java
+++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/Manhwa18Parser.java
@@ -45,7 +45,6 @@ public List parseImageListImpl(@NonNull Content onlineContent, @Nulla
List chapterLinks = doc.select("div ul a[href*=chap]");
if (chapterLinks.isEmpty()) chapterLinks = doc.select("div ul a[href*=ch-]");
- Collections.reverse(chapterLinks); // Put the chapters in the correct reading order
chapters = ParseHelper.getChaptersFromLinks(chapterLinks, onlineContent.getId());
// If the stored content has chapters already, save them for comparison
@@ -73,7 +72,7 @@ public List parseImageListImpl(@NonNull Content onlineContent, @Nulla
List images = doc.select("#chapter-content img");
List imageUrls = Stream.of(images).map(ParseHelper::getImgSrc).toList();
if (!imageUrls.isEmpty())
- result.addAll(ParseHelper.urlsToImageFiles(imageUrls, imgOffset + result.size() + 1, StatusContent.SAVED, chp, 1000));
+ result.addAll(ParseHelper.urlsToImageFiles(imageUrls, imgOffset + result.size() + 1, StatusContent.SAVED, 1000, chp));
else
Timber.i("Chapter parsing failed for %s : no pictures found", chp.getUrl());
} else {
@@ -83,6 +82,10 @@ public List parseImageListImpl(@NonNull Content onlineContent, @Nulla
}
progressComplete();
+ // Add cover if it's a first download
+ if (storedChapters.isEmpty())
+ result.add(ImageFile.newCover(onlineContent.getCoverImageUrl(), StatusContent.SAVED));
+
// If the process has been halted manually, the result is incomplete and should not be returned as is
if (processHalted.get()) throw new PreparationInterruptedException();
diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/ManhwaParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/images/ManhwaParser.java
index f7264311fc..d8fc49c580 100644
--- a/app/src/main/java/me/devsaki/hentoid/parsers/images/ManhwaParser.java
+++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/ManhwaParser.java
@@ -83,7 +83,6 @@ private List parseImageFiles(@NonNull Content onlineContent, @Nullabl
);
if (doc != null) {
List chapterLinks = doc.select("[class^=wp-manga-chapter] a");
- Collections.reverse(chapterLinks); // Put the chapters in the correct reading order
chapters = ParseHelper.getChaptersFromLinks(chapterLinks, onlineContent.getId());
} else {
reason = "Chapters page couldn't be downloaded @ " + canonicalUrl;
@@ -123,7 +122,7 @@ private List parseImageFiles(@NonNull Content onlineContent, @Nullabl
if (!url.isEmpty()) urls.add(url);
}
if (!urls.isEmpty())
- result.addAll(ParseHelper.urlsToImageFiles(urls, imgOffset + result.size() + 1, StatusContent.SAVED, chp, 1000));
+ result.addAll(ParseHelper.urlsToImageFiles(urls, imgOffset + result.size() + 1, StatusContent.SAVED, 1000, chp));
else
Timber.w("Chapter parsing failed for %s : no pictures found", chp.getUrl());
} else {
@@ -133,8 +132,9 @@ private List parseImageFiles(@NonNull Content onlineContent, @Nullabl
}
progressComplete();
- // Add cover
- result.add(ImageFile.newCover(onlineContent.getCoverImageUrl(), StatusContent.SAVED));
+ // Add cover if it's a first download
+ if (storedChapters.isEmpty())
+ result.add(ImageFile.newCover(onlineContent.getCoverImageUrl(), StatusContent.SAVED));
// If the process has been halted manually, the result is incomplete and should not be returned as is
if (processHalted.get()) throw new PreparationInterruptedException();
diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/ToonilyParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/images/ToonilyParser.java
index 2376a366c0..e5476ca46f 100644
--- a/app/src/main/java/me/devsaki/hentoid/parsers/images/ToonilyParser.java
+++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/ToonilyParser.java
@@ -80,7 +80,6 @@ private List parseImageFiles(@NonNull Content onlineContent, @Nullabl
);
if (doc != null) {
List chapterLinks = doc.select("[class^=wp-manga-chapter] a");
- Collections.reverse(chapterLinks); // Put the chapters in the correct reading order
chapters = ParseHelper.getChaptersFromLinks(chapterLinks, onlineContent.getId());
} else {
reason = "Chapters page couldn't be downloaded @ " + canonicalUrl;
@@ -120,7 +119,7 @@ private List parseImageFiles(@NonNull Content onlineContent, @Nullabl
if (!url.isEmpty()) imageUrls.add(url);
}
if (!imageUrls.isEmpty())
- result.addAll(ParseHelper.urlsToImageFiles(imageUrls, imgOffset + result.size() + 1, StatusContent.SAVED, chp, 1000));
+ result.addAll(ParseHelper.urlsToImageFiles(imageUrls, imgOffset + result.size() + 1, StatusContent.SAVED, 1000, chp));
else
Timber.i("Chapter parsing failed for %s : no pictures found", chp.getUrl());
} else {
@@ -130,8 +129,9 @@ private List parseImageFiles(@NonNull Content onlineContent, @Nullabl
}
progressComplete();
- // Add cover
- result.add(ImageFile.newCover(onlineContent.getCoverImageUrl(), StatusContent.SAVED));
+ // Add cover if it's a first download
+ if (storedChapters.isEmpty())
+ result.add(ImageFile.newCover(onlineContent.getCoverImageUrl(), StatusContent.SAVED));
// If the process has been halted manually, the result is incomplete and should not be returned as is
if (processHalted.get()) throw new PreparationInterruptedException();
diff --git a/app/src/main/java/me/devsaki/hentoid/util/BundleX.kt b/app/src/main/java/me/devsaki/hentoid/util/BundleX.kt
new file mode 100644
index 0000000000..7c323af394
--- /dev/null
+++ b/app/src/main/java/me/devsaki/hentoid/util/BundleX.kt
@@ -0,0 +1,121 @@
+package me.devsaki.hentoid.util
+
+import android.os.Bundle
+import android.os.Parcelable
+import android.util.Size
+import android.util.SizeF
+import java.io.Serializable
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+fun Bundle.boolean(default: Boolean) = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getBoolean(property.name, default)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: Boolean) =
+ putBoolean(property.name, value)
+}
+
+fun Bundle.byte(default: Byte) = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getByte(property.name, default)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: Byte) =
+ putByte(property.name, value)
+}
+
+fun Bundle.char(default: Char) = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getChar(property.name, default)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: Char) =
+ putChar(property.name, value)
+}
+
+fun Bundle.short(default: Short) = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getShort(property.name, default)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: Short) =
+ putShort(property.name, value)
+}
+
+fun Bundle.int(default: Int) = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getInt(property.name, default)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) =
+ putInt(property.name, value)
+}
+
+fun Bundle.long(default: Long) = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getLong(property.name, default)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: Long) =
+ putLong(property.name, value)
+}
+
+fun Bundle.float(default: Float) = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getFloat(property.name, default)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: Float) =
+ putFloat(property.name, value)
+}
+
+fun Bundle.string(default: String?) = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getString(property.name, default)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: String?) =
+ putString(property.name, value)
+}
+
+fun Bundle.size() = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getSize(property.name)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: Size?) =
+ putSize(property.name, value)
+}
+
+fun Bundle.sizeF() = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getSizeF(property.name)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: SizeF?) =
+ putSizeF(property.name, value)
+}
+
+fun Bundle.parcelable() = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getParcelable(property.name)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) =
+ putParcelable(property.name, value)
+}
+
+fun Bundle.serializable() = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getSerializable(property.name)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: Serializable?) =
+ putSerializable(property.name, value)
+}
+
+fun Bundle.intArray() = object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getIntArray(property.name)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: IntArray?) =
+ putIntArray(property.name, value)
+}
+
+fun Bundle.intArrayList() = object : ReadWriteProperty?> {
+ override fun getValue(thisRef: Any, property: KProperty<*>) =
+ getIntegerArrayList(property.name)
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: ArrayList?) =
+ putIntegerArrayList(property.name, value)
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.java b/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.java
index 3a5c69d719..1015ee616a 100644
--- a/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.java
+++ b/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.java
@@ -158,9 +158,9 @@ public static void viewContentGalleryPage(@NonNull final Context context, @NonNu
if (content.getSite().equals(Site.NONE)) return;
Intent intent = new Intent(context, Content.getWebActivityClass(content.getSite()));
- BaseWebActivityBundle.Builder builder = new BaseWebActivityBundle.Builder();
- builder.setUrl(content.getGalleryUrl());
- intent.putExtras(builder.getBundle());
+ BaseWebActivityBundle bundle = new BaseWebActivityBundle();
+ bundle.setUrl(content.getGalleryUrl());
+ intent.putExtras(bundle.toBundle());
if (wrapPin) intent = UnlockActivity.wrapIntent(context, intent);
context.startActivity(intent);
}
@@ -441,7 +441,7 @@ public static long addContent(
group = new Group(Grouping.ARTIST, a.getName(), ++nbGroups);
group.setSubtype(a.getType().equals(AttributeType.ARTIST) ? Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS : Preferences.Constant.ARTIST_GROUP_VISIBILITY_GROUPS);
if (!a.contents.isEmpty())
- group.picture.setTarget(a.contents.get(0).getCover());
+ group.coverContent.setTarget(a.contents.get(0));
}
GroupHelper.addContentToAttributeGroup(dao, group, a, content);
}
@@ -931,9 +931,9 @@ public static void launchBrowserFor(@NonNull final Context context, @NonNull fin
Intent intent = new Intent(context, Content.getWebActivityClass(targetSite));
- BaseWebActivityBundle.Builder builder = new BaseWebActivityBundle.Builder();
- builder.setUrl(targetUrl);
- intent.putExtras(builder.getBundle());
+ BaseWebActivityBundle bundle = new BaseWebActivityBundle();
+ bundle.setUrl(targetUrl);
+ intent.putExtras(bundle.toBundle());
context.startActivity(intent);
}
diff --git a/app/src/main/java/me/devsaki/hentoid/util/GroupHelper.java b/app/src/main/java/me/devsaki/hentoid/util/GroupHelper.java
index 7fb93b4720..787d65a8cc 100644
--- a/app/src/main/java/me/devsaki/hentoid/util/GroupHelper.java
+++ b/app/src/main/java/me/devsaki/hentoid/util/GroupHelper.java
@@ -129,9 +129,8 @@ public static Content moveContentToCustomGroup(@NonNull final Content content, @
// Update the cover of the old groups if they used a picture from the book that is being moved
for (GroupItem gi : groupItems) {
Group g = gi.group.getTarget();
- if (g != null && !g.picture.isNull()) {
- ImageFile groupCover = g.picture.getTarget();
- if (groupCover.getContent().getTargetId() == content.getId()) {
+ if (g != null && !g.coverContent.isNull()) {
+ if (g.coverContent.getTargetId() == content.getId()) {
updateGroupCover(g, content.getId(), dao);
}
}
@@ -150,8 +149,8 @@ public static Content moveContentToCustomGroup(@NonNull final Content content, @
content.groupItems.applyChangesToDb();
// Add a picture to the target group if it didn't have one
- if (group.picture.isNull())
- group.picture.setAndPutTarget(content.getCover());
+ if (group.coverContent.isNull())
+ group.coverContent.setAndPutTarget(content);
}
return content;
@@ -170,7 +169,7 @@ private static void updateGroupCover(@NonNull final Group g, long contentIdToRem
// Empty group cover if there's just one content inside
if (1 == groupsContents.size() && groupsContents.get(0).getId() == contentIdToRemove) {
- g.picture.setAndPutTarget(null);
+ g.coverContent.setAndPutTarget(null);
return;
}
@@ -179,7 +178,7 @@ private static void updateGroupCover(@NonNull final Group g, long contentIdToRem
if (c.getId() != contentIdToRemove) {
ImageFile cover = c.getCover();
if (cover.getId() > -1) {
- g.picture.setAndPutTarget(cover);
+ g.coverContent.setAndPutTarget(c);
return;
}
}
diff --git a/app/src/main/java/me/devsaki/hentoid/util/download/RequestQueueManager.java b/app/src/main/java/me/devsaki/hentoid/util/download/RequestQueueManager.java
index 32e09f43e0..56b1d8b458 100644
--- a/app/src/main/java/me/devsaki/hentoid/util/download/RequestQueueManager.java
+++ b/app/src/main/java/me/devsaki/hentoid/util/download/RequestQueueManager.java
@@ -193,7 +193,8 @@ private int getAllowedNewRequests(long now) {
do {
polled = false;
Long earliestRequestTimestamp = previousRequestsTimestamps.peek();
- if (null != earliestRequestTimestamp && now - earliestRequestTimestamp > 1000) {
+ if (null == earliestRequestTimestamp) break; // Empty collection
+ if (now - earliestRequestTimestamp > 1000) {
previousRequestsTimestamps.poll();
polled = true;
}
diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.java b/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.java
index bd511565ba..6a4f70466c 100644
--- a/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.java
+++ b/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.java
@@ -579,8 +579,8 @@ private void attachButtons(@NonNull final ContentItem item) {
}
}
- public static void updateProgress(@NonNull final Content content, @NonNull View rootCardView, int position, boolean isPausedEvent, boolean isQueueActive) {
- boolean isQueueReady = !ContentQueueManager.getInstance().isQueuePaused() && !isPausedEvent;
+ public static void updateProgress(@NonNull final Content content, @NonNull View rootCardView, int position, boolean isPausedEvent, boolean isContentQueueActive) {
+ boolean isQueueReady = isContentQueueActive && !ContentQueueManager.getInstance().isQueuePaused() && !isPausedEvent;
boolean isFirstItem = (0 == position);
ProgressBar pb = rootCardView.findViewById(R.id.pbDownload);
diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.java b/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.java
index a31aaacf3f..6155dc1cc1 100644
--- a/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.java
+++ b/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.java
@@ -190,15 +190,16 @@ public void bindView(@NotNull GroupDisplayItem item, @NotNull List> payloads)
}
if (ivCover != null) {
- ImageFile cover = null;
- if (!item.group.picture.isNull()) cover = item.group.picture.getTarget();
+ Content coverContent = null;
+ if (!item.group.coverContent.isNull())
+ coverContent = item.group.coverContent.getTarget();
else if (!item.group.items.isEmpty()) {
if (item.group.items.get(0).content.isResolved()) {
Content c = item.group.items.get(0).content.getTarget();
- if (c != null) cover = c.getCover();
+ if (c != null) coverContent = c;
}
}
- if (cover != null) attachCover(cover);
+ if (coverContent != null) attachCover(coverContent.getCover());
}
List items = item.group.items;
title.setText(String.format("%s%s", item.group.name, (null == items || items.isEmpty()) ? "" : " (" + items.size() + ")"));
diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/LibraryViewModel.java b/app/src/main/java/me/devsaki/hentoid/viewmodels/LibraryViewModel.java
index 39dc06616f..acda1728fd 100644
--- a/app/src/main/java/me/devsaki/hentoid/viewmodels/LibraryViewModel.java
+++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/LibraryViewModel.java
@@ -582,7 +582,7 @@ public void deleteItems(
WorkManager workManager = WorkManager.getInstance(getApplication());
workManager.enqueue(new OneTimeWorkRequest.Builder(DeleteWorker.class).setInputData(builder.getData()).build());
- // TODO update isCustomGroupingAvailable when the whole delete job is complete
+ // TODO update isCustomGroupingAvailable when the whole delete chain is complete
}
public void purgeItem(@NonNull final Content content) {
@@ -649,9 +649,9 @@ public Content doArchiveContent(@NonNull final Content content) throws IOExcepti
return null;
}
- public void setGroupCover(long groupId, ImageFile cover) {
+ public void setGroupCoverContent(long groupId, @NonNull Content coverContent) {
Group localGroup = dao.selectGroup(groupId);
- if (localGroup != null) localGroup.picture.setAndPutTarget(cover);
+ if (localGroup != null) localGroup.coverContent.setAndPutTarget(coverContent);
}
public void saveContentPositions(@NonNull final List orderedContent,
diff --git a/app/src/main/res/layout/dialog_library_merge.xml b/app/src/main/res/layout/dialog_library_merge.xml
index d5f8c4bff8..b5f601a350 100644
--- a/app/src/main/res/layout/dialog_library_merge.xml
+++ b/app/src/main/res/layout/dialog_library_merge.xml
@@ -38,10 +38,10 @@
+ android:layout_height="wrap_content"
+ android:paddingStart="8dp"
+ android:text="@string/merge_delete_after_merging"
+ app:layout_constraintTop_toBottomOf="@id/title_new" />