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" />