diff --git a/.gitignore b/.gitignore index bf1f8a5739..98fd9d22b3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ app/**/output.json # Gradle files .gradle/ build/ -/gradle.properties # Generated files bin/ diff --git a/app/build.gradle b/app/build.gradle index 64a7b1a540..535370a2ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,20 +8,18 @@ if (file('signing.gradle').exists()) { } android { - compileSdkVersion 27 + compileSdkVersion 28 compileOptions { targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 } defaultConfig { applicationId "me.devsaki.hentoid" - minSdkVersion 15 - // Hold updating targetSdkVersion until implementing an image viewer - // or Perfect Viewer implements Content URI + minSdkVersion 19 //noinspection ExpiringTargetSdkVersion - targetSdkVersion 23 - versionCode 108 - versionName '1.7.6' + targetSdkVersion 28 + versionCode 110 // is updated automatically by BitRise; only used when building locally + versionName '1.8.4' def fkToken = '\"' + (System.getenv("FK_TOKEN")?: "")+'\"' def includeObjectBoxBrowser = System.getenv("INCLUDE_OBJECTBOX_BROWSER")?:"false" @@ -66,26 +64,29 @@ dependencies { implementation project(':fakkuLib') testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' // Firebase - implementation 'com.google.firebase:firebase-core:16.0.6' + implementation 'com.google.firebase:firebase-core:17.0.0' // Crashlytics - implementation 'com.crashlytics.sdk.android:crashlytics:2.9.8' + implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' // Stetho core // implementation 'com.facebook.stetho:stetho:1.5.0' // Support libraries - implementation 'com.android.support:recyclerview-v7:27.1.1' - implementation 'com.android.support:exifinterface:27.1.1' - implementation 'com.android.support:appcompat-v7:27.1.1' - implementation 'com.android.support:cardview-v7:27.1.1' - implementation 'com.android.support:design:27.1.1' - implementation 'com.android.support:preference-v7:27.1.1' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' - implementation 'com.google.android.gms:play-services-safetynet:16.0.0' + implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'androidx.exifinterface:exifinterface:1.0.0' + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.cardview:cardview:1.0.0' + //implementation 'com.google.android.material:material:1.0.0' + implementation 'com.google.android.material:material:1.1.0-alpha08' + implementation 'androidx.preference:preference:1.0.0' + implementation 'androidx.media:media:1.0.1' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.legacy:legacy-support-v13:1.0.0' + implementation 'com.google.android.gms:play-services-safetynet:17.0.0' // <-- this one to fix the SSLHandshake error with okhttp on Android 4.1-4.4 when server only supports TLS1.2 @@ -95,7 +96,7 @@ dependencies { implementation 'com.github.bumptech.glide:okhttp3-integration:4.8.0' // Intro screens or slides: github.com/apl-devs/AppIntro - implementation 'com.github.apl-devs:appintro:v4.2.3' + implementation 'com.github.paolorotolo:appintro:v5.1.0' // Java serialization/deserialization (Java Objects into JSON and back): github.com/google/gson implementation 'com.google.code.gson:gson:2.8.5' @@ -110,9 +111,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:3.11.0' // For downloading images : github.com/google/volley - implementation 'com.android.volley:volley:1.1.1' - - // <-- if this evolves, please examine RequestQueueManager.getRequestQueue for evolutions + implementation 'com.android.volley:volley:1.1.1' // <-- if this evolves, please examine RequestQueueManager.getRequestQueue for evolutions // EventBus: github.com/greenrobot/EventBus implementation 'org.greenrobot:eventbus:3.1.1' @@ -132,9 +131,7 @@ dependencies { */ // Specific UI layout for tag mosaic : github.com/google/flexbox-layout - implementation 'com.google.android:flexbox:1.0.0' - - // <- dont upgrade that until the entire app switches to AndroidX + implementation 'com.google.android:flexbox:1.1.0' // https://mvnrepository.com/artifact/commons-io/commons-io implementation 'commons-io:commons-io:2.6' @@ -153,8 +150,8 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' // For ViewModel to work - implementation "android.arch.lifecycle:extensions:1.1.1" - implementation "android.arch.lifecycle:viewmodel:1.1.1" + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.0.0' // 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"). @@ -165,6 +162,11 @@ dependencies { // https://github.com/davemorrissey/subsampling-scale-image-view implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' + //implementation 'com.github.hereisderek:subsampling-scale-image-view:v3.10.2' // Compatible with AndroidX; crashes at resetScaleAndCenter + + // https://github.com/davideas/FlexibleAdapter + implementation 'eu.davidea:flexible-adapter:5.1.0' + implementation 'eu.davidea:flexible-adapter-ui:1.0.0' } sonarqube { diff --git a/app/objectbox-models/default.json b/app/objectbox-models/default.json index 83590f33f0..b78e491c34 100644 --- a/app/objectbox-models/default.json +++ b/app/objectbox-models/default.json @@ -27,7 +27,7 @@ }, { "id": "2:5880334030341287801", - "lastPropertyId": "18:9117635469382092615", + "lastPropertyId": "19:326862264253525250", "name": "Content", "properties": [ { @@ -101,6 +101,10 @@ { "id": "18:9117635469382092615", "name": "isBeingDeleted" + }, + { + "id": "19:326862264253525250", + "name": "jsonUri" } ], "relations": [ @@ -112,7 +116,7 @@ }, { "id": "3:2849837771881495731", - "lastPropertyId": "7:8627441942081468691", + "lastPropertyId": "9:3938664550318760597", "name": "ImageFile", "properties": [ { @@ -143,6 +147,10 @@ { "id": "7:8627441942081468691", "name": "downloadParams" + }, + { + "id": "9:3938664550318760597", + "name": "favourite" } ], "relations": [] @@ -252,7 +260,8 @@ 7592738185335406663, 2516142253958987500, 7452586865244450379, - 7159239942191071387 + 7159239942191071387, + 6300283326491520978 ], "retiredRelationUids": [ 4182320255043105081, diff --git a/app/src/androidTest/java/me/devsaki/hentoid/ApplicationTest.java b/app/src/androidTest/java/me/devsaki/hentoid/ApplicationTest.java deleted file mode 100644 index 0d6a905bd8..0000000000 --- a/app/src/androidTest/java/me/devsaki/hentoid/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package me.devsaki.hentoid; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89d395e787..bc5c11b141 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,7 +22,7 @@ + android:theme="@style/AppTheme.NoActionBar.Splash"> @@ -32,41 +32,45 @@ + android:theme="@style/AppTheme" /> + + android:theme="@style/AppTheme.NoActionBar.Overlay"> @@ -84,7 +88,7 @@ android:configChanges="orientation|screenSize" android:label="@string/title_activity_mikan" android:parentActivityName=".activities.DownloadsActivity" - android:theme="@style/AppTheme.Mikan" + android:theme="@style/AppTheme" tools:ignore="UnusedAttribute"> + android:theme="@style/AppTheme.NoActionBar" /> + android:theme="@style/AppTheme.NoActionBar" /> @@ -236,10 +240,10 @@ - + @@ -247,10 +251,9 @@ - @@ -258,10 +261,19 @@ - + + + + + + + @@ -269,7 +281,7 @@ + android:theme="@style/AppTheme.NoActionBar.Monochrome"> @@ -293,7 +305,7 @@ diff --git a/app/src/main/assets/licenses.html b/app/src/main/assets/licenses.html index f6b837e1e6..e55ebb9ab5 100644 --- a/app/src/main/assets/licenses.html +++ b/app/src/main/assets/licenses.html @@ -2928,7 +2928,7 @@

Notices for files:

The MIT License
 
-Copyright (c) 2009-2018 Jonathan Hedley 
+Copyright (c) 2009-2018 Jonathan Hedley jonathan@hedley.net
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/app/src/main/java/me/devsaki/hentoid/HentoidApp.java b/app/src/main/java/me/devsaki/hentoid/HentoidApp.java
index 689daf37af..4964bb3b4e 100644
--- a/app/src/main/java/me/devsaki/hentoid/HentoidApp.java
+++ b/app/src/main/java/me/devsaki/hentoid/HentoidApp.java
@@ -9,6 +9,8 @@
 import android.os.Bundle;
 import android.os.StrictMode;
 
+import androidx.appcompat.app.AppCompatDelegate;
+
 import com.crashlytics.android.Crashlytics;
 import com.google.android.gms.security.ProviderInstaller;
 import com.google.firebase.analytics.FirebaseAnalytics;
@@ -17,6 +19,7 @@
 import me.devsaki.hentoid.database.DatabaseMaintenance;
 import me.devsaki.hentoid.database.HentoidDB;
 import me.devsaki.hentoid.notification.download.DownloadNotificationChannel;
+import me.devsaki.hentoid.notification.maintenance.MaintenanceNotificationChannel;
 import me.devsaki.hentoid.notification.update.UpdateNotificationChannel;
 import me.devsaki.hentoid.services.DatabaseMaintenanceService;
 import me.devsaki.hentoid.services.UpdateCheckService;
@@ -33,7 +36,8 @@
 public class HentoidApp extends Application {
 
     private static boolean beginImport;
-    @SuppressLint("StaticFieldLeak") // A context leak happening at app level isn't _really_ a leak, right ? ;-)
+    @SuppressLint("StaticFieldLeak")
+    // A context leak happening at app level isn't _really_ a leak, right ? ;-)
     private static Context instance;
 
     public static Context getAppContext() {
@@ -106,7 +110,13 @@ public void onCreate() {
         // Init notifications
         UpdateNotificationChannel.init(this);
         DownloadNotificationChannel.init(this);
-        startService(UpdateCheckService.makeIntent(this, false));
+        MaintenanceNotificationChannel.init(this);
+        Intent intent = UpdateCheckService.makeIntent(this, false);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            startForegroundService(intent);
+        } else {
+            startService(intent);
+        }
 
         // Clears all previous notifications
         NotificationManager manager = (NotificationManager) instance.getSystemService(Context.NOTIFICATION_SERVICE);
@@ -115,11 +125,17 @@ public void onCreate() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
             ShortcutHelper.buildShortcuts(this);
         }
+
+        // Set Night mode
+        int darkMode = Preferences.getDarkMode();
+        AppCompatDelegate.setDefaultNightMode(darkModeFromPrefs(darkMode));
+        FirebaseAnalytics.getInstance(this).setUserProperty("night_mode", Integer.toString(darkMode));
     }
 
     /**
      * Clean up and upgrade database
      */
+    @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"})
     private void performDatabaseHousekeeping() {
         HentoidDB oldDB = HentoidDB.getInstance(this);
 
@@ -128,6 +144,23 @@ private void performDatabaseHousekeeping() {
 
         // Launch a service that will perform non-structural DB housekeeping tasks
         Intent intent = DatabaseMaintenanceService.makeIntent(this);
-        startService(intent);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            startForegroundService(intent);
+        } else {
+            startService(intent);
+        }
+    }
+
+    public static int darkModeFromPrefs(int prefsMode) {
+        switch (prefsMode) {
+            case Preferences.Constant.DARK_MODE_ON:
+                return AppCompatDelegate.MODE_NIGHT_YES;
+            case Preferences.Constant.DARK_MODE_OFF:
+                return AppCompatDelegate.MODE_NIGHT_NO;
+            case Preferences.Constant.DARK_MODE_BATTERY:
+                return AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY;
+            default:
+                return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
+        }
     }
 }
diff --git a/app/src/main/java/me/devsaki/hentoid/abstracts/BaseActivity.java b/app/src/main/java/me/devsaki/hentoid/abstracts/BaseActivity.java
index f13a844c0a..29dc6bde40 100644
--- a/app/src/main/java/me/devsaki/hentoid/abstracts/BaseActivity.java
+++ b/app/src/main/java/me/devsaki/hentoid/abstracts/BaseActivity.java
@@ -1,9 +1,8 @@
 package me.devsaki.hentoid.abstracts;
 
-import android.support.v7.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatActivity;
 
 import me.devsaki.hentoid.R;
-import me.devsaki.hentoid.util.Helper;
 import me.devsaki.hentoid.util.ToastUtil;
 
 /**
diff --git a/app/src/main/java/me/devsaki/hentoid/abstracts/BaseFragment.java b/app/src/main/java/me/devsaki/hentoid/abstracts/BaseFragment.java
index 5cf3a1213a..98dda9b384 100644
--- a/app/src/main/java/me/devsaki/hentoid/abstracts/BaseFragment.java
+++ b/app/src/main/java/me/devsaki/hentoid/abstracts/BaseFragment.java
@@ -1,8 +1,8 @@
 package me.devsaki.hentoid.abstracts;
 
 import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
 
 import org.greenrobot.eventbus.EventBus;
 
@@ -49,7 +49,7 @@ public void onStart() {
 
     @Override
     public void onDestroy() {
-        EventBus.getDefault().unregister(this);
+        if (EventBus.getDefault().isRegistered(this)) EventBus.getDefault().unregister(this);
         super.onDestroy();
     }
 
diff --git a/app/src/main/java/me/devsaki/hentoid/abstracts/DownloadsFragment.java b/app/src/main/java/me/devsaki/hentoid/abstracts/DownloadsFragment.java
index aaa66cf026..d0a289cff6 100644
--- a/app/src/main/java/me/devsaki/hentoid/abstracts/DownloadsFragment.java
+++ b/app/src/main/java/me/devsaki/hentoid/abstracts/DownloadsFragment.java
@@ -6,21 +6,12 @@
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.StringRes;
-import android.support.design.widget.Snackbar;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.widget.SwipeRefreshLayout;
-import android.support.v7.app.AlertDialog;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.SearchView;
 import android.view.ActionMode;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -32,7 +23,17 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.widget.SearchView;
+import androidx.fragment.app.FragmentActivity;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
 import com.annimon.stream.Stream;
+import com.google.android.material.snackbar.Snackbar;
 
 import org.greenrobot.eventbus.EventBus;
 import org.greenrobot.eventbus.Subscribe;
@@ -42,6 +43,9 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.annotation.Nonnull;
+
+import me.devsaki.hentoid.BuildConfig;
 import me.devsaki.hentoid.HentoidApp;
 import me.devsaki.hentoid.R;
 import me.devsaki.hentoid.activities.ImportActivity;
@@ -51,17 +55,16 @@
 import me.devsaki.hentoid.collection.CollectionAccessor;
 import me.devsaki.hentoid.collection.mikan.MikanCollectionAccessor;
 import me.devsaki.hentoid.database.ObjectBoxCollectionAccessor;
-import me.devsaki.hentoid.database.ObjectBoxDB;
-import me.devsaki.hentoid.database.domains.Attribute;
 import me.devsaki.hentoid.database.domains.Content;
-import me.devsaki.hentoid.enums.Language;
 import me.devsaki.hentoid.enums.Site;
 import me.devsaki.hentoid.events.DownloadEvent;
 import me.devsaki.hentoid.events.ImportEvent;
-import me.devsaki.hentoid.fragments.AboutMikanDialogFragment;
+import me.devsaki.hentoid.fragments.downloads.AboutMikanDialogFragment;
+import me.devsaki.hentoid.fragments.downloads.PagerFragment;
 import me.devsaki.hentoid.fragments.downloads.SearchBookIdDialogFragment;
-import me.devsaki.hentoid.listener.ContentListener;
+import me.devsaki.hentoid.fragments.downloads.UpdateSuccessDialogFragment;
 import me.devsaki.hentoid.listener.ContentClickListener.ItemSelectListener;
+import me.devsaki.hentoid.listener.PagedResultListener;
 import me.devsaki.hentoid.services.ContentQueueManager;
 import me.devsaki.hentoid.util.ConstsImport;
 import me.devsaki.hentoid.util.FileHelper;
@@ -70,6 +73,7 @@
 import me.devsaki.hentoid.util.Preferences;
 import me.devsaki.hentoid.util.RandomSeedSingleton;
 import me.devsaki.hentoid.util.ToastUtil;
+import me.devsaki.hentoid.widget.ContentSearchManager;
 import timber.log.Timber;
 
 import static com.annimon.stream.Collectors.toCollection;
@@ -78,35 +82,31 @@
  * Created by avluis on 08/27/2016. Common elements for use by EndlessFragment and PagerFragment
  * 

* todo issue: After requesting for permission, the app is reset using {@link #resetApp()} instead - * of implementing {@link #onRequestPermissionsResult(int, String[], int[])} to receive permission + * of implementing {@link #onRequestPermissionsResult} to receive permission * request result */ -public abstract class DownloadsFragment extends BaseFragment implements ContentListener, +public abstract class DownloadsFragment extends BaseFragment implements PagedResultListener, ItemSelectListener { // ======== CONSTANTS - protected static final int SHOW_LOADING = 1; - protected static final int SHOW_BLANK = 2; + private static final int SHOW_LOADING = 1; + private static final int SHOW_BLANK = 2; protected static final int SHOW_RESULT = 3; - public final static int MODE_LIBRARY = 0; - public final static int MODE_MIKAN = 1; + public static final int MODE_LIBRARY = 0; + public static final int MODE_MIKAN = 1; // Save state constants - - private static final String KEY_SELECTED_TAGS = "selected_tags"; - private static final String KEY_FILTER_FAVOURITES = "filter_favs"; - private static final String KEY_CURRENT_PAGE = "current_page"; - private static final String KEY_QUERY = "query"; private static final String KEY_MODE = "mode"; + private static final String KEY_PLANNED_REFRESH = "planned_refresh"; // ======== UI ELEMENTS // Top tooltip appearing when a download has been completed - protected LinearLayout newContentToolTip; + private LinearLayout newContentToolTip; // "Search" button on top menu private MenuItem searchMenu; // "Toggle favourites" button on top menu @@ -116,13 +116,13 @@ public abstract class DownloadsFragment extends BaseFragment implements ContentL // Action view associated with search menu button private SearchView mainSearchView; // Search pane that shows up on top when using search function - protected View advancedSearchPane; + private View advancedSearchPane; // Layout containing the list of books private SwipeRefreshLayout refreshLayout; // List containing all books protected RecyclerView mListView; // Layout manager associated with the above list view - protected LinearLayoutManager llm; + private LinearLayoutManager llm; // Pane saying "Loading up~" private TextView loadingText; // Pane saying "Why am I empty ?" @@ -147,12 +147,10 @@ public abstract class DownloadsFragment extends BaseFragment implements ContentL // === MISC. USAGE protected Context mContext; - // Current page of collection view (NB : In EndlessFragment, a "page" is a group of loaded books. Last page is reached when scrolling reaches the very end of the book list) - protected int currentPage = 1; // Adapter in charge of book list display protected ContentAdapter mAdapter; // True if a new download is ready; used to display / hide "New Content" tooltip when scrolling - protected boolean isNewContentAvailable; + private boolean isNewContentAvailable; // True if book list is being loaded; used for synchronization between threads protected boolean isLoading; // Indicates whether or not one of the books has been selected @@ -160,36 +158,27 @@ public abstract class DownloadsFragment extends BaseFragment implements ContentL // Records the system time (ms) when back button has been last pressed (to detect "double back button" event) private long backButtonPressed; // True if bottom toolbar visibility is fixed and should not change regardless of scrolling; false if bottom toolbar visibility changes according to scrolling - protected boolean overrideBottomToolbarVisibility; + private boolean overrideBottomToolbarVisibility; // True if storage permissions have been checked at least once private boolean storagePermissionChecked = false; // Mode : show library or show Mikan search private int mode = MODE_LIBRARY; - // Collection accessor (DB or external, depending on mode) - private CollectionAccessor collectionAccessor; // Total count of book in entire selected/queried collection (Adapter is in charge of updating it) private long mTotalSelectedCount = -1; // -1 = uninitialized (no query done yet) // Total count of book in entire collection (Adapter is in charge of updating it) private long mTotalCount = -1; // -1 = uninitialized (no query done yet) // Used to ignore native calls to onQueryTextChange - boolean invalidateNextQueryTextChange = false; - // Used to detect if the library has been refreshed - boolean libraryHasBeenRefreshed = false; - // If library has been refreshed, indicated new content count - int refreshedContentCount = 0; + private boolean invalidateNextQueryTextChange = false; + // A library display refresh has been planned + private boolean plannedRefresh = false; // === SEARCH - // Favourite filter active - private boolean filterFavourites = false; - // Expression typed in the search bar - protected String query = ""; - // Current search tags - private List selectedSearchTags = new ArrayList<>(); + protected ContentSearchManager searchManager; // Last search parameters; used to determine whether or not page number should be reset to 1 + // NB : populated by getCurrentSearchParams private String lastSearchParams = ""; - // To be documented private ActionMode mActionMode; private boolean selectTrigger = false; @@ -200,9 +189,9 @@ private static int getIconFromSortOrder(int sortOrder) { case Preferences.Constant.ORDER_CONTENT_LAST_DL_DATE_FIRST: return R.drawable.ic_menu_sort_321; case Preferences.Constant.ORDER_CONTENT_LAST_DL_DATE_LAST: - return R.drawable.ic_menu_sort_by_date; + return R.drawable.ic_menu_sort_123; case Preferences.Constant.ORDER_CONTENT_TITLE_ALPHA: - return R.drawable.ic_menu_sort_alpha; + return R.drawable.ic_menu_sort_az; case Preferences.Constant.ORDER_CONTENT_TITLE_ALPHA_INVERTED: return R.drawable.ic_menu_sort_za; case Preferences.Constant.ORDER_CONTENT_LEAST_READ: @@ -228,7 +217,7 @@ private static int getIconFromSortOrder(int sortOrder) { public boolean onCreateActionMode(ActionMode mode, Menu menu) { // Inflate a menu resource providing context menu items MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.menu_context_menu, menu); + inflater.inflate(R.menu.downloads_context_menu, menu); return true; } @@ -255,6 +244,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return true; case R.id.action_delete: + case R.id.action_delete_sweep: mAdapter.purgeSelectedItems(); mode.finish(); @@ -263,11 +253,6 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { mAdapter.archiveSelectedItems(); mode.finish(); - return true; - case R.id.action_delete_sweep: - mAdapter.purgeSelectedItems(); - mode.finish(); - return true; default: return false; @@ -286,11 +271,11 @@ public void onDestroyActionMode(ActionMode mode) { public void onResume() { super.onResume(); - int currentViewer = Preferences.getContentReadAction(); - if (Preferences.Constant.PREF_READ_CONTENT_HENTOID_VIEWER != currentViewer) { - if (!Preferences.hasViewerChoiceBeenDisplayed()) showViewerChoiceDialog(); - } else { - Preferences.setViewerChoiceDisplayed(true); + // Display the "update success" dialog when an update is detected + if (Preferences.getLastKnownAppVersionCode() > 0 && + Preferences.getLastKnownAppVersionCode() < BuildConfig.VERSION_CODE) { + UpdateSuccessDialogFragment.invoke(requireFragmentManager()); + Preferences.setLastKnownAppVersionCode(BuildConfig.VERSION_CODE); } defaultLoad(); @@ -300,23 +285,24 @@ public void onResume() { * Check write permissions on target storage and load library */ private void defaultLoad() { - if (MODE_LIBRARY == mode) { if (PermissionUtil.requestExternalStoragePermission(requireActivity(), ConstsImport.RQST_STORAGE_PERMISSION)) { boolean shouldUpdate = queryPrefs(); - if (shouldUpdate || -1 == mTotalSelectedCount) - searchLibrary(); // If prefs changes detected or first run (-1 = uninitialized) + + // Run a search if prefs changes detected or first run (-1 = uninitialized) + if (shouldUpdate || -1 == mTotalSelectedCount || 0 == mAdapter.getItemCount()) + searchLibrary(); + if (ContentQueueManager.getInstance().getDownloadCount() > 0) showReloadToolTip(); showToolbar(true); } else { Timber.d("Storage permission denied!"); - if (storagePermissionChecked) { - resetApp(); - } + if (storagePermissionChecked) resetApp(); storagePermissionChecked = true; } } else if (MODE_MIKAN == mode) { - if (-1 == mTotalSelectedCount) searchLibrary(); + if (-1 == mTotalSelectedCount || 0 == mAdapter.getItemCount()) searchLibrary(); + showToolbar(true); } } @@ -325,11 +311,32 @@ private void defaultLoad() { public void onImportEvent(ImportEvent event) { if (ImportEvent.EV_COMPLETE == event.eventType) { EventBus.getDefault().removeStickyEvent(event); - libraryHasBeenRefreshed = true; - refreshedContentCount = event.booksOK; + plannedRefresh = true; } } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onDownloadEvent(DownloadEvent event) { + if (event.eventType == DownloadEvent.EV_COMPLETE && !isLoading) { + if (MODE_LIBRARY == mode) showReloadToolTip(); + else mAdapter.switchStateToDownloaded(event.content); + } + } + + private void openBook(Content content) { + // The list order might change when viewing books when certain sort orders are activated + // "unread" status might also change + // => plan a refresh next time DownloadsFragment is called + plannedRefresh = true; + Bundle bundle = new Bundle(); + searchManager.saveToBundle(bundle); + int pageOffset = 0; + if (this instanceof PagerFragment) + pageOffset = (searchManager.getCurrentPage() - 1) * Preferences.getContentPageQuantity(); + bundle.putInt("contentIndex", pageOffset + mAdapter.getContentPosition(content) + 1); + FileHelper.openContent(requireContext(), content, bundle); + } + /** * Updates class variables with Hentoid user preferences */ @@ -346,27 +353,21 @@ protected boolean queryPrefs() { activity.finish(); } - if (libraryHasBeenRefreshed && mTotalCount > -1) { - Timber.d("Library has been refreshed ! %s -> %s books", mTotalCount, refreshedContentCount); - - if (refreshedContentCount > mTotalCount) { // More books added - showReloadToolTip(); - } else { // Library cleaned up - shouldUpdate = true; - } - libraryHasBeenRefreshed = false; - refreshedContentCount = 0; + if (plannedRefresh && mTotalCount > -1) { + Timber.d("A library display refresh has been planned"); + shouldUpdate = true; + plannedRefresh = false; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { checkStorage(); } - int booksPerPage = Preferences.getContentPageQuantity(); + int settingsBooksPerPage = Preferences.getContentPageQuantity(); - if (this.booksPerPage != booksPerPage) { + if (this.booksPerPage != settingsBooksPerPage) { Timber.d("booksPerPage updated."); - this.booksPerPage = booksPerPage; + this.booksPerPage = settingsBooksPerPage; setQuery(""); shouldUpdate = true; } @@ -421,18 +422,9 @@ public void onPause() { @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - - outState.putBoolean(KEY_FILTER_FAVOURITES, filterFavourites); - outState.putString(KEY_QUERY, query); - outState.putInt(KEY_CURRENT_PAGE, currentPage); outState.putInt(KEY_MODE, mode); - - long[] selectedTagIds = new long[selectedSearchTags.size()]; - int index = 0; - for (Attribute a : selectedSearchTags) { - selectedTagIds[index++] = a.getId(); - } - outState.putLongArray(KEY_SELECTED_TAGS, selectedTagIds); + outState.putBoolean(KEY_PLANNED_REFRESH, plannedRefresh); + searchManager.saveToBundle(outState); } @Override @@ -440,21 +432,9 @@ public void onViewStateRestored(@Nullable Bundle state) { super.onViewStateRestored(state); if (state != null) { - filterFavourites = state.getBoolean(KEY_FILTER_FAVOURITES, false); - query = state.getString(KEY_QUERY, ""); - currentPage = state.getInt(KEY_CURRENT_PAGE); mode = state.getInt(KEY_MODE); - - long[] selectedTagIds = state.getLongArray(KEY_SELECTED_TAGS); - ObjectBoxDB db = ObjectBoxDB.getInstance(requireContext()); - if (selectedTagIds != null) { - for (long i : selectedTagIds) { - Attribute a = db.selectAttributeById(i); - if (a != null) { - selectedSearchTags.add(a); - } - } - } + plannedRefresh = state.getBoolean(KEY_PLANNED_REFRESH, false); + searchManager.loadFromBundle(state); } } @@ -471,7 +451,7 @@ public void onCreate(Bundle savedInstanceState) { @Override public void onDestroy() { - collectionAccessor.dispose(); + searchManager.dispose(); mAdapter.dispose(); super.onDestroy(); } @@ -480,36 +460,33 @@ public void onDestroy() { public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { if (this.getArguments() != null) mode = this.getArguments().getInt("mode"); - collectionAccessor = (MODE_LIBRARY == mode) ? new ObjectBoxCollectionAccessor(mContext) : new MikanCollectionAccessor(mContext); + CollectionAccessor collectionAccessor = (MODE_LIBRARY == mode) ? new ObjectBoxCollectionAccessor(mContext) : new MikanCollectionAccessor(mContext); + searchManager = new ContentSearchManager(collectionAccessor); View rootView = inflater.inflate(R.layout.fragment_downloads, container, false); - initUI(rootView); + initUI(rootView, collectionAccessor); attachScrollListener(); attachOnClickListeners(rootView); return rootView; } - protected void initUI(View rootView) { + protected void initUI(View rootView, CollectionAccessor accessor) { loadingText = rootView.findViewById(R.id.loading); emptyText = rootView.findViewById(R.id.empty); emptyText.setText((MODE_LIBRARY == mode) ? R.string.downloads_empty_library : R.string.downloads_empty_mikan); - int contentSortOrder = Preferences.getContentSortOrder(); - - if (MODE_MIKAN == mode) - contentSortOrder = Preferences.Constant.ORDER_CONTENT_LAST_UL_DATE_FIRST; - llm = new LinearLayoutManager(mContext); mAdapter = new ContentAdapter.Builder() .setContext(mContext) - .setCollectionAccessor(collectionAccessor) + .setCollectionAccessor(accessor) .setDisplayMode(mode) - .setSortComparator(Content.getComparator(contentSortOrder)) + .setSortComparator(Content.getComparator()) .setItemSelectListener(this) .setOnContentRemovedListener(this::onContentRemoved) + .setOpenBookAction(this::openBook) .build(); // Main view @@ -537,7 +514,7 @@ protected void initUI(View rootView) { protected void attachScrollListener() { mListView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + public void onScrolled(@Nonnull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // Show toolbar: @@ -583,7 +560,7 @@ protected void attachOnClickListeners(View rootView) { filterClearButton.setOnClickListener(v -> { setQuery(""); - selectedSearchTags.clear(); + searchManager.clearSelectedSearchTags(); filterBar.setVisibility(View.GONE); searchLibrary(); }); @@ -621,8 +598,8 @@ public boolean onBackPressed() { /** * Clear search query and hide the search view if asked so */ - protected void clearQuery() { - setQuery(query = ""); + private void clearQuery() { + setQuery(""); searchLibrary(); } @@ -630,7 +607,7 @@ protected void clearQuery() { * Refresh the whole screen - Called by pressing the "New Content" button that appear on new * downloads - Called by scrolling up when being on top of the list ("force reload" command) */ - protected void commitRefresh() { + private void commitRefresh() { newContentToolTip.setVisibility(View.GONE); refreshLayout.setRefreshing(false); refreshLayout.setEnabled(false); @@ -650,17 +627,9 @@ private void resetCount() { if (manager != null) manager.cancel(0); } - @Subscribe(threadMode = ThreadMode.MAIN) - public void onDownloadEvent(DownloadEvent event) { - if (event.eventType == DownloadEvent.EV_COMPLETE && !isLoading) { - if (MODE_LIBRARY == mode) showReloadToolTip(); - else mAdapter.switchStateToDownloaded(event.content); - } - } - @Override - public void onCreateOptionsMenu(final Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.menu_content_list, menu); + public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.downloads_menu, menu); MenuItem aboutMikanMenu = menu.findItem(R.id.action_about_mikan); aboutMikanMenu.setVisible(MODE_MIKAN == mode); @@ -681,12 +650,12 @@ public boolean onMenuItemActionExpand(MenuItem item) { setSearchPaneVisibility(true); // Re-sets the query on screen, since default behaviour removes it right after collapse _and_ expand - if (query != null && !query.isEmpty()) + if (!searchManager.getQuery().isEmpty()) // Use of handler allows to set the value _after_ the UI has auto-cleared it // Without that handler the view displays with an empty value new Handler().postDelayed(() -> { invalidateNextQueryTextChange = true; - mainSearchView.setQuery(query, false); + mainSearchView.setQuery(searchManager.getQuery(), false); }, 100); return true; @@ -742,8 +711,8 @@ private void onAdvancedSearchClick() { SearchActivityBundle.Builder builder = new SearchActivityBundle.Builder(); builder.setMode(mode); - if (!selectedSearchTags.isEmpty()) - builder.setUri(Helper.buildSearchUri(selectedSearchTags)); + if (!searchManager.getTags().isEmpty()) + builder.setUri(SearchActivityBundle.Builder.buildSearchUri(searchManager.getTags())); search.putExtras(builder.getBundle()); startActivityForResult(search, 999); @@ -758,7 +727,7 @@ private void onAdvancedSearchClick() { * @return true if the order has been successfully processed */ @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(@NonNull MenuItem item) { int contentSortOrder; switch (item.getItemId()) { @@ -791,7 +760,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } - mAdapter.setSortComparator(Content.getComparator(contentSortOrder)); + mAdapter.setSortComparator(Content.getComparator()); orderMenu.setIcon(getIconFromSortOrder(contentSortOrder)); Preferences.setContentSortOrder(contentSortOrder); searchLibrary(); @@ -813,7 +782,7 @@ private void setSearchPaneVisibility(boolean visible) { * Toggles favourite filter on a book and updates the UI accordingly */ private void toggleFavouriteFilter() { - filterFavourites = !filterFavourites; + searchManager.setFilterFavourites(!searchManager.isFilterFavourites()); updateFavouriteFilter(); searchLibrary(); } @@ -822,12 +791,11 @@ private void toggleFavouriteFilter() { * Update favourite filter button appearance (icon and color) on a book */ private void updateFavouriteFilter() { - favsMenu.setIcon(filterFavourites ? R.drawable.ic_fav_full : R.drawable.ic_fav_empty); + favsMenu.setIcon(searchManager.isFilterFavourites() ? R.drawable.ic_fav_full : R.drawable.ic_fav_empty); } private void submitContentSearchQuery(final String s) { - query = s; - selectedSearchTags.clear(); // If user searches in main toolbar, universal search takes over advanced search + searchManager.clearSelectedSearchTags(); // If user searches in main toolbar, universal search takes over advanced search setQuery(s); searchLibrary(); } @@ -841,19 +809,8 @@ private void showReloadToolTip() { } private void setQuery(String query) { - this.query = query; - currentPage = 1; - } - - /** - * Returns the current value of the query typed in the search toolbar; empty string if no query - * typed - * - * @return Current value of the query typed in the search toolbar; empty string if no query - * typed - */ - private String getQuery() { - return query == null ? "" : query; + searchManager.setQuery(query); + searchManager.setCurrentPage(1); } private void clearSelection() { @@ -873,7 +830,6 @@ protected void toggleUI(int mode) { mListView.setVisibility(View.GONE); emptyText.setVisibility(View.GONE); loadingText.setVisibility(View.VISIBLE); - //showToolbar(false); startLoadingTextAnimation(); break; case SHOW_BLANK: @@ -921,14 +877,13 @@ private void stopLoadingTextAnimation() { } } - /** * Indicates whether a search query is active (using universal search or advanced search) or not * * @return True if a search query is is active (using universal search or advanced search); false if not (=whole unfiltered library selected) */ private boolean isSearchQueryActive() { - return (getQuery().length() > 0 || selectedSearchTags.size() > 0); + return (!searchManager.getQuery().isEmpty() || !searchManager.getTags().isEmpty()); } /** @@ -936,15 +891,13 @@ private boolean isSearchQueryActive() { * * @return Search parameters thumbprint */ - private String getCurrentSearchParams(int contentSortOrder) { - StringBuilder result = new StringBuilder(mode == MODE_LIBRARY ? "L" : "M"); - result.append(".").append(query); - for (Attribute a : selectedSearchTags) result.append(".").append(a.getName()); - result.append(".").append(booksPerPage); - result.append(".").append(contentSortOrder); - result.append(".").append(filterFavourites); - - return result.toString(); + private String getCurrentSearchParams() { + return (mode == MODE_LIBRARY ? "L" : "M") + + "|" + searchManager.getQuery() + + "|" + SearchActivityBundle.Builder.buildSearchUri(searchManager.getTags()) + + "|" + booksPerPage + + "|" + searchManager.getContentSortOrder() + + "|" + searchManager.isFilterFavourites(); } protected abstract boolean forceSearchFromPageOne(); @@ -952,6 +905,7 @@ private String getCurrentSearchParams(int contentSortOrder) { protected void searchLibrary() { searchLibrary(true); } + /** * Loads the library applying current search parameters * @@ -959,24 +913,19 @@ protected void searchLibrary() { */ protected void searchLibrary(boolean showLoadingPanel) { isLoading = true; - int contentSortOrder = Preferences.getContentSortOrder(); + searchManager.setContentSortOrder(Preferences.getContentSortOrder()); if (showLoadingPanel) toggleUI(SHOW_LOADING); // Searches start from page 1 if they are new or if the fragment implementation forces it - String currentSearchParams = getCurrentSearchParams(contentSortOrder); + String currentSearchParams = getCurrentSearchParams(); if (!currentSearchParams.equals(lastSearchParams) || forceSearchFromPageOne()) { - currentPage = 1; + searchManager.setCurrentPage(1); mListView.scrollToPosition(0); } lastSearchParams = currentSearchParams; - if (!getQuery().isEmpty()) - collectionAccessor.searchBooksUniversal(getQuery(), currentPage, booksPerPage, contentSortOrder, filterFavourites, this); // Universal search - else if (!selectedSearchTags.isEmpty()) - collectionAccessor.searchBooks("", selectedSearchTags, currentPage, booksPerPage, contentSortOrder, filterFavourites, this); // Advanced search - else - collectionAccessor.getRecentBooks(Site.HITOMI, Language.ANY, currentPage, booksPerPage, contentSortOrder, filterFavourites, this); // Default search (display recent) + searchManager.searchLibraryForContent(booksPerPage, this); } protected abstract void showToolbar(boolean show); @@ -989,11 +938,11 @@ else if (!selectedSearchTags.isEmpty()) * @return true if last page has been reached */ protected boolean isLastPage() { - return (currentPage * booksPerPage >= mTotalSelectedCount); + return (searchManager.getCurrentPage() * booksPerPage >= mTotalSelectedCount); } private void displayNoResults() { - if (!query.isEmpty()) { + if (!searchManager.getQuery().isEmpty()) { emptyText.setText(R.string.search_entry_not_found); } else { emptyText.setText((MODE_LIBRARY == mode) ? R.string.downloads_empty_library : R.string.downloads_empty_mikan); @@ -1019,10 +968,10 @@ private void updateTitle() { } /* - ContentListener implementation + PagedResultListener implementation */ @Override - public void onContentReady(List results, long totalSelectedContent, long totalContent) { + public void onPagedResultReady(List results, long totalSelectedContent, long totalContent) { Timber.d("Content results have loaded : %s results; %s total selected count, %s total count", results.size(), totalSelectedContent, totalContent); isLoading = false; @@ -1033,11 +982,10 @@ public void onContentReady(List results, long totalSelectedContent, lon isNewContentAvailable = false; } - @StringRes int textRes = totalSelectedContent > 1 ? - R.string.downloads_filter_book_count_plural : - R.string.downloads_filter_book_count; + Resources res = getResources(); + String textRes = res.getQuantityString(R.plurals.downloads_filter_book_count_plural, (int)totalSelectedContent, (int)totalSelectedContent); - filterBookCount.setText(getString(textRes, totalSelectedContent)); + filterBookCount.setText(textRes); filterBar.setVisibility(View.VISIBLE); if (totalSelectedContent > 0 && searchMenu != null) searchMenu.collapseActionView(); } else { @@ -1045,13 +993,15 @@ public void onContentReady(List results, long totalSelectedContent, lon } // User searches a book ID - if (Helper.isNumeric(query)) { + // => Suggests searching through all sources except those where the selected book ID is already in the collection + if (Helper.isNumeric(searchManager.getQuery())) { ArrayList siteCodes = Stream.of(results) + .filter(content -> searchManager.getQuery().equals(content.getUniqueSiteId())) .map(Content::getSite) .map(Site::getCode) .collect(toCollection(ArrayList::new)); - SearchBookIdDialogFragment.invoke(requireFragmentManager(), query, siteCodes); + SearchBookIdDialogFragment.invoke(requireFragmentManager(), searchManager.getQuery(), siteCodes); } if (0 == totalSelectedContent) { @@ -1067,7 +1017,7 @@ public void onContentReady(List results, long totalSelectedContent, lon } @Override - public void onContentFailed(Content content, String message) { + public void onPagedResultFailed(Content content, String message) { Timber.w(message); isLoading = false; @@ -1141,18 +1091,15 @@ public void onItemClear(int itemCount) { public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == 999) { - if (resultCode == Activity.RESULT_OK) { - if (data != null && data.getExtras() != null) { - Uri searchUri = new SearchActivityBundle.Parser(data.getExtras()).getUri(); - - if (searchUri != null) { - setQuery(searchUri.getPath()); - selectedSearchTags = Helper.parseSearchUri(searchUri); + if (requestCode == 999 + && resultCode == Activity.RESULT_OK + && data != null && data.getExtras() != null) { + Uri searchUri = new SearchActivityBundle.Parser(data.getExtras()).getUri(); - searchLibrary(); - } - } + if (searchUri != null) { + setQuery(searchUri.getPath()); + searchManager.setTags(SearchActivityBundle.Parser.parseSearchUri(searchUri)); + searchLibrary(); } } } @@ -1166,7 +1113,7 @@ private void onContentRemoved(int i) { mTotalSelectedCount = mTotalSelectedCount - i; mTotalCount = mTotalCount - i; - if (0 == mTotalCount) currentPage = 1; + if (0 == mTotalCount) searchManager.setCurrentPage(1); if (0 == mTotalSelectedCount) { displayNoResults(); @@ -1175,18 +1122,4 @@ private void onContentRemoved(int i) { updateTitle(); } - - private void showViewerChoiceDialog() { - new AlertDialog.Builder(requireContext(), R.style.Theme_AppCompat_Dialog_Alert) - .setTitle(R.string.downloads_suggest_image_viewer_title) - .setMessage(R.string.downloads_suggest_image_viewer) - .setPositiveButton(R.string.try_it, - (dialog, which) -> { - Preferences.setViewerChoiceDisplayed(true); - Preferences.setContentReadAction(Preferences.Constant.PREF_READ_CONTENT_HENTOID_VIEWER); - }) - .setNegativeButton(R.string.no, - (dialog, which) -> Preferences.setViewerChoiceDisplayed(true)) - .show(); - } } diff --git a/app/src/main/java/me/devsaki/hentoid/abstracts/DrawerActivity.java b/app/src/main/java/me/devsaki/hentoid/abstracts/DrawerActivity.java deleted file mode 100644 index 472953068b..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/abstracts/DrawerActivity.java +++ /dev/null @@ -1,226 +0,0 @@ -package me.devsaki.hentoid.abstracts; - -import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityOptionsCompat; -import android.support.v4.app.FragmentManager; -import android.support.v4.content.ContextCompat; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v7.widget.Toolbar; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListView; - -import me.devsaki.hentoid.R; -import me.devsaki.hentoid.ui.CompoundAdapter; -import me.devsaki.hentoid.ui.DrawerMenuContents; -import me.devsaki.hentoid.util.Preferences; -import timber.log.Timber; - -/** - * Created by avluis on 4/11/2016. - * Abstract activity with toolbar and navigation drawer. - * Needs to be extended by any activity that wants to be shown as a top level activity. - * Subclasses must have these layout elements: - * - {@link android.support.v4.widget.DrawerLayout} with id 'drawer_layout'. - * - {@link android.widget.ListView} with id 'drawer_list'. - */ -public abstract class DrawerActivity extends BaseActivity implements DrawerLayout.DrawerListener { - - private DrawerLayout mDrawerLayout; - private ListView mDrawerList; - private DrawerMenuContents mDrawerMenuContents; - private ActionBarDrawerToggle mDrawerToggle; - private int itemToOpen = -1; - private int currentPos = -1; - private boolean itemTapped; - - protected abstract String getToolbarTitle(); - - protected void initializeNavigationDrawer(Toolbar toolbar) { - mDrawerLayout = findViewById(R.id.drawer_layout); - mDrawerList = findViewById(R.id.drawer_list); - - mDrawerToggle = new ActionBarDrawerToggle( - this, - mDrawerLayout, - toolbar, - R.string.drawer_open, - R.string.drawer_close - ); - mDrawerLayout.addDrawerListener(this); - populateDrawerItems(); - updateDrawerToggle(); - - // When the user runs the app for the first time, we want to land them with the - // navigation drawer open. But just the first time. - if (!Preferences.isFirstRunProcessComplete()) { - // first run of the app starts with the nav drawer open - mDrawerLayout.openDrawer(GravityCompat.START); - Preferences.setIsFirstRunProcessComplete(true); - } - } - - @Override - public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { - if (mDrawerToggle != null) mDrawerToggle.onDrawerSlide(drawerView, slideOffset); - } - - @Override - public void onDrawerOpened(@NonNull View drawerView) { - if (mDrawerToggle != null) mDrawerToggle.onDrawerOpened(drawerView); - if (getSupportActionBar() != null) { - getSupportActionBar().setTitle(getToolbarTitle()); - } - } - - @Override - public void onDrawerClosed(@NonNull View drawerView) { - if (mDrawerToggle != null) mDrawerToggle.onDrawerClosed(drawerView); - - int position = itemToOpen; - if (position >= 0 && itemTapped) { - itemTapped = false; - Class activityClass = mDrawerMenuContents.getActivity(position); - Intent intent = new Intent(this, activityClass); - Bundle bundle = ActivityOptionsCompat - .makeCustomAnimation(this, R.anim.fade_in, R.anim.fade_out) - .toBundle(); - ContextCompat.startActivity(this, intent, bundle); - overridePendingTransition(R.anim.fade_in, R.anim.fade_out); - } - } - - @Override - public void onDrawerStateChanged(int newState) { - if (mDrawerToggle != null) mDrawerToggle.onDrawerStateChanged(newState); - } - - private void populateDrawerItems() { - mDrawerMenuContents = new DrawerMenuContents(); - updateDrawerPosition(); - final int selectedPosition = currentPos; - final int unselectedColor = ContextCompat.getColor(getApplicationContext(), - R.color.drawer_item_unselected_background); - final int selectedColor = ContextCompat.getColor(getApplicationContext(), - R.color.drawer_item_selected_background); - final CompoundAdapter adapter = new CompoundAdapter(this, mDrawerMenuContents.getItems(), - R.layout.drawer_list_item, - new String[]{DrawerMenuContents.FIELD_TITLE}, new int[]{R.id.drawer_item_title}) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = super.getView(position, convertView, parent); - int color = unselectedColor; - if (position == selectedPosition) { - color = selectedColor; - } - view.setBackgroundColor(color); - return view; - } - }; - - mDrawerList.setOnItemClickListener((parent, view, position, id) -> { - if (position != selectedPosition) { - mDrawerList.setItemChecked(position, true); - itemToOpen = position; - itemTapped = true; - } - mDrawerLayout.closeDrawers(); - }); - mDrawerList.setAdapter(adapter); - } - - protected void updateDrawerPosition() { - final int selectedPosition = mDrawerMenuContents.getPosition(this.getClass()); - updateSelected(selectedPosition); - } - - private void updateSelected(int position) { - currentPos = position; - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - // Sync the toggle state after onRestoreInstanceState has occurred. - mDrawerToggle.syncState(); - } - - @Override - protected void onResume() { - super.onResume(); - // Whenever the fragment back stack changes, we may need to update the - // action bar toggle: only top level screens show the hamburger-like icon, inner - // screens - either Activities or fragments - show the "Up" icon instead. - getSupportFragmentManager().addOnBackStackChangedListener(this::updateDrawerToggle); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if (mDrawerToggle != null) { - mDrawerToggle.onConfigurationChanged(newConfig); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Pass the event to {@link android.support.v7.app.ActionBarDrawerToggle}, if it returns - // true, then it has handled the app icon touch event - if (mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item)) { - return true; - } - // If not handled by drawerToggle, home needs to be handled by returning to previous - if (item != null && item.getItemId() == android.R.id.home) { - Timber.d("sent home"); - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onBackPressed() { - // If the drawer is open, back will close it - if (mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START)) { - mDrawerLayout.closeDrawers(); - return; - } - // Otherwise, it may return to the previous fragment stack - FragmentManager fragmentManager = getSupportFragmentManager(); - if (fragmentManager.getBackStackEntryCount() > 0) { - fragmentManager.popBackStack(); - } else { - // Lastly, it will rely on the system behavior for back - updateDrawerPosition(); - super.onBackPressed(); - } - } - - @Override - public void onPause() { - super.onPause(); - getSupportFragmentManager().removeOnBackStackChangedListener(this::updateDrawerToggle); - } - - private void updateDrawerToggle() { - if (mDrawerToggle == null) { - return; - } - boolean isRoot = getSupportFragmentManager().getBackStackEntryCount() == 0; - mDrawerToggle.setDrawerIndicatorEnabled(isRoot); - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayShowHomeEnabled(!isRoot); - getSupportActionBar().setDisplayHomeAsUpEnabled(!isRoot); - getSupportActionBar().setHomeButtonEnabled(!isRoot); - } - if (isRoot) { - mDrawerToggle.syncState(); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/AboutActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/AboutActivity.java index 533d427ed0..304d64548a 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/AboutActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/AboutActivity.java @@ -1,16 +1,20 @@ package me.devsaki.hentoid.activities; import android.os.Bundle; -import android.support.annotation.IdRes; -import android.support.v7.app.AlertDialog; +import androidx.annotation.IdRes; import android.view.View; -import android.webkit.WebView; -import android.widget.Button; import android.widget.TextView; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + import me.devsaki.hentoid.BuildConfig; import me.devsaki.hentoid.R; import me.devsaki.hentoid.abstracts.BaseActivity; +import me.devsaki.hentoid.events.UpdateEvent; +import me.devsaki.hentoid.fragments.about.ChangelogFragment; +import me.devsaki.hentoid.fragments.about.LicensesFragment; import me.devsaki.hentoid.util.Consts; import me.devsaki.hentoid.util.Helper; @@ -19,6 +23,8 @@ */ public class AboutActivity extends BaseActivity { + private TextView btnChangelog; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -33,24 +39,44 @@ protected void onCreate(Bundle savedInstanceState) { TextView tvVersionName = findViewById(R.id.tv_version_name); tvVersionName.setText(String.format("Hentoid ver: %s (%s)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); - WebView webView = new WebView(this); - webView.loadUrl("file:///android_asset/licenses.html"); - webView.setInitialScale(95); + btnChangelog = findViewById(R.id.about_changelog_button); + btnChangelog.setOnClickListener(v -> showChangelogFragment()); - AlertDialog licensesDialog = new AlertDialog.Builder(this) - .setTitle("Licenses") - .setView(webView) - .setPositiveButton(android.R.string.ok, null) - .create(); + View btnLicenses = findViewById(R.id.about_licenses_button); + btnLicenses.setOnClickListener(v -> showLicenseFragment()); - // TODO: dialog should not show large content or a no-op button - // replace with activity instead - Button btnLicenses = findViewById(R.id.btn_about_licenses); - btnLicenses.setOnClickListener(view -> licensesDialog.show()); + if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this); } private void bindTextViewLink(@IdRes int tvId, String url) { View linkableView = findViewById(tvId); linkableView.setOnClickListener(v -> Helper.openUrl(this, url)); } + + private void showLicenseFragment() { + getSupportFragmentManager() + .beginTransaction() + .add(android.R.id.content, new LicensesFragment()) + .addToBackStack(null) // This triggers a memory leak in LeakCanary but is _not_ a leak : see https://stackoverflow.com/questions/27913009/memory-leak-in-fragmentmanager + .commit(); + } + + private void showChangelogFragment() { + getSupportFragmentManager() + .beginTransaction() + .add(android.R.id.content, new ChangelogFragment()) + .addToBackStack(null) // This triggers a memory leak in LeakCanary but is _not_ a leak : see https://stackoverflow.com/questions/27913009/memory-leak-in-fragmentmanager + .commit(); + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onUpdateEvent(UpdateEvent event) { + if (event.hasNewVersion) btnChangelog.setText(R.string.view_changelog_flagged); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (EventBus.getDefault().isRegistered(this)) EventBus.getDefault().unregister(this); + } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/DownloadsActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/DownloadsActivity.java index 932e7da7c6..ef1833e2e7 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/DownloadsActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/DownloadsActivity.java @@ -3,19 +3,20 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.widget.Toolbar; import android.view.WindowManager; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import me.devsaki.hentoid.R; +import me.devsaki.hentoid.abstracts.BaseActivity; import me.devsaki.hentoid.abstracts.BaseFragment; import me.devsaki.hentoid.abstracts.BaseFragment.BackInterface; import me.devsaki.hentoid.abstracts.DownloadsFragment; -import me.devsaki.hentoid.abstracts.DrawerActivity; import me.devsaki.hentoid.fragments.downloads.EndlessFragment; import me.devsaki.hentoid.fragments.downloads.PagerFragment; import me.devsaki.hentoid.util.Helper; @@ -26,7 +27,9 @@ * Created by avluis on 08/26/2016. * DownloadsActivity: In charge of hosting EndlessFragment & PagerFragment */ -public class DownloadsActivity extends DrawerActivity implements BackInterface { +public class DownloadsActivity extends BaseActivity implements BackInterface { + + private DrawerLayout drawerLayout; private BaseFragment baseFragment; @@ -71,9 +74,21 @@ protected void onCreate(Bundle savedInstanceState) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); } + drawerLayout = findViewById(R.id.drawer_layout); + toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - initializeNavigationDrawer(toolbar); + toolbar.setNavigationIcon(R.drawable.ic_drawer); + toolbar.setNavigationOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START)); + + // When the user runs the app for the first time, we want to land them with the + // navigation drawer open. But just the first time. + if (!Preferences.isFirstRunProcessComplete()) { + // first run of the app starts with the nav drawer open + drawerLayout.openDrawer(GravityCompat.START); + Preferences.setIsFirstRunProcessComplete(true); + } + setTitle(""); } @@ -95,7 +110,6 @@ protected void onNewIntent(Intent intent) { @Override public void onBackPressed() { - DrawerLayout drawerLayout = findViewById(R.id.drawer_layout); if (drawerLayout != null && drawerLayout.isDrawerOpen(GravityCompat.START)) { drawerLayout.closeDrawers(); return; @@ -109,7 +123,7 @@ public void onBackPressed() { @Override public void setTitle(CharSequence subtitle) { - String title = getToolbarTitle() + " " + subtitle; + String title = getString(R.string.title_activity_downloads) + " " + subtitle; super.setTitle(title); toolbar.setTitle(title); } @@ -125,7 +139,6 @@ protected void onResume() { super.onResume(); updateSelectedFragment(); - updateDrawerPosition(); } private void updateSelectedFragment() { @@ -133,10 +146,6 @@ private void updateSelectedFragment() { Fragment fragment = manager.findFragmentById(R.id.content_frame); if (fragment != null) { - /* - Fragment selectedFragment = buildFragment(); - String selectedFragmentTag = selectedFragment.getClass().getSimpleName(); - */ String selectedFragmentTag = getFragment().getSimpleName(); if (!selectedFragmentTag.equals(fragment.getTag())) { @@ -166,13 +175,12 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis } } - @Override - protected String getToolbarTitle() { - return getString(R.string.title_activity_downloads); - } - @Override public void addBackInterface(BaseFragment fragment) { this.baseFragment = fragment; } + + public void onNavigationDrawerItemClicked() { + drawerLayout.closeDrawer(GravityCompat.START); + } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/ImageViewerActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/ImageViewerActivity.java index 874ed22360..e4f5d6de7c 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/ImageViewerActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/ImageViewerActivity.java @@ -1,26 +1,26 @@ package me.devsaki.hentoid.activities; -import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; import android.os.Build; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; import android.view.WindowManager; -import java.util.List; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProviders; + +import java.security.AccessControlException; import me.devsaki.hentoid.activities.bundles.ImageViewerActivityBundle; import me.devsaki.hentoid.fragments.viewer.ImagePagerFragment; import me.devsaki.hentoid.util.ConstsImport; import me.devsaki.hentoid.util.PermissionUtil; import me.devsaki.hentoid.util.Preferences; +import me.devsaki.hentoid.util.ToastUtil; import me.devsaki.hentoid.viewmodels.ImageViewerViewModel; public class ImageViewerActivity extends AppCompatActivity { - private ImageViewerViewModel viewModel; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -29,29 +29,28 @@ protected void onCreate(Bundle savedInstanceState) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); Intent intent = getIntent(); - if (intent != null && intent.getExtras() != null) { - ImageViewerActivityBundle.Parser parser = new ImageViewerActivityBundle.Parser(intent.getExtras()); - List uris = parser.getUrisStr(); + if (null == intent || null == intent.getExtras()) + throw new IllegalArgumentException("Required init arguments not found"); - if (null == uris) { - throw new RuntimeException("Initialization failed"); - } + ImageViewerActivityBundle.Parser parser = new ImageViewerActivityBundle.Parser(intent.getExtras()); + long contentId = parser.getContentId(); + if (0 == contentId) throw new IllegalArgumentException("Incorrect ContentId"); - viewModel = ViewModelProviders.of(this).get(ImageViewerViewModel.class); - viewModel.setImages(uris); - viewModel.setContentId(parser.getContentId()); - } + ImageViewerViewModel viewModel = ViewModelProviders.of(this).get(ImageViewerViewModel.class); + Bundle searchParams = parser.getSearchParams(); + if (searchParams != null) viewModel.loadFromSearchParams(contentId, searchParams); + else viewModel.loadFromContent(contentId); - PermissionUtil.requestExternalStoragePermission(this, ConstsImport.RQST_STORAGE_PERMISSION); + if (!PermissionUtil.requestExternalStoragePermission(this, ConstsImport.RQST_STORAGE_PERMISSION)) { + ToastUtil.toast("Storage permission denied - cannot open the viewer"); + throw new AccessControlException("Storage permission denied - cannot open the viewer"); + } // Allows an full recolor of the status bar with the custom color defined in the activity's theme if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN); - } if (null == savedInstanceState) { getSupportFragmentManager() @@ -60,11 +59,4 @@ protected void onCreate(Bundle savedInstanceState) { .commit(); } } - - @Override - public void onBackPressed() { - viewModel.saveCurrentPosition(); - super.onBackPressed(); - } - } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/ImportActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/ImportActivity.java index 89a43b21b5..4e03375e83 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/ImportActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/ImportActivity.java @@ -13,18 +13,19 @@ import android.os.Environment; import android.os.Handler; import android.provider.DocumentsContract; -import android.support.annotation.NonNull; -import android.support.annotation.RequiresApi; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.FragmentTransaction; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.InputType; import android.view.View; import android.widget.EditText; import android.widget.ImageView; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentTransaction; + import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -248,7 +249,7 @@ private void pickDownloadDirectory(File dir) { if (FileHelper.isOnExtSdCard(dir) && !FileHelper.isWritable(dir)) { Timber.d("Inaccessible: moving back to default directory."); downloadDir = currentRootDir = new File(Environment.getExternalStorageDirectory() + - "/" + Consts.DEFAULT_LOCAL_DIRECTORY + "/"); + File.separator + Consts.DEFAULT_LOCAL_DIRECTORY + File.separator); } if (useDefaultFolder) { prevRootDir = currentRootDir; @@ -301,7 +302,7 @@ public void onManualInput(OnTextViewClickedEvent event) { if (event.isLongClick()) { Timber.d("Resetting directory back to default."); currentRootDir = new File(Environment.getExternalStorageDirectory() + - "/" + Consts.DEFAULT_LOCAL_DIRECTORY + "/"); + File.separator + Consts.DEFAULT_LOCAL_DIRECTORY + File.separator); dirChooserFragment.dismiss(); pickDownloadDirectory(currentRootDir); } else { @@ -388,7 +389,7 @@ private void resolveDirs(String[] externalDirs, List writeableDirs) { } else { if (writeableDirs.size() == 1) { // If we get exactly one write-able path returned, attempt to make use of it - String sdDir = writeableDirs.get(0) + "/" + Consts.DEFAULT_LOCAL_DIRECTORY + "/"; + String sdDir = writeableDirs.get(0) + File.separator + Consts.DEFAULT_LOCAL_DIRECTORY + File.separator; if (!FileHelper.isOnExtSdCard(writeableDirs.get(0)) && FileHelper.checkAndSetRootFolder(sdDir)) { // TODO - dirChooserFragment can't actually browse SD card : to fix later ? Timber.d("Got access to SD Card."); currentRootDir = new File(sdDir); @@ -402,7 +403,7 @@ private void resolveDirs(String[] externalDirs, List writeableDirs) { PackageManager manager = this.getPackageManager(); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); List handlers = manager.queryIntentActivities(intent, 0); - if (handlers != null && handlers.size() > 0) { + if (handlers != null && !handlers.isEmpty()) { Timber.d("Device should be able to handle the SAF request"); ToastUtil.toast("Attempting SAF"); requestWritePermission(); @@ -481,7 +482,7 @@ private void revokePermission() { getContentResolver().releasePersistableUriPermission(p.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } - if (getContentResolver().getPersistedUriPermissions().size() == 0) { + if (getContentResolver().getPersistedUriPermissions().isEmpty()) { Timber.d("Permissions revoked successfully."); } else { Timber.d("Permissions failed to be revoked."); @@ -491,6 +492,9 @@ private void revokePermission() { /* Return from SAF system dialog + + NB : Right now, this method _assumes_ the selected folder is on the first SD card + => Even if SAF actually selects internal phone memory or another SD card / an external USB storage device, it won't be processed properly */ @RequiresApi(api = KITKAT) @Override @@ -503,6 +507,8 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { Uri treeUri = data.getData(); if (treeUri != null && treeUri.getPath() != null) { // Persist selected folder URI in shared preferences + // NB : calling saveUri populates the preference used by FileHelper.isSAF, which indicates the library storage is on an SD card / an external USB storage device + // => this should be managed if SAF dialog is used to select folders on the internal phone memory FileHelper.saveUri(treeUri); // Persist access permissions @@ -564,7 +570,7 @@ public void onImportEventComplete(ImportEvent event) { private boolean hasBooks() { List downloadDirs = new ArrayList<>(); for (Site s : Site.values()) { - downloadDirs.add(FileHelper.getSiteDownloadDir(this, s)); + downloadDirs.add(FileHelper.getOrCreateSiteDownloadDir(this, s)); } for (File downloadDir : downloadDirs) { @@ -641,7 +647,11 @@ private void runImport() { builder.setRefreshCleanUnreadable(isCleanUnreadable); intent.putExtras(builder.getBundle()); - startService(intent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent); + } else { + startService(intent); + } } private void cleanUpDB() { diff --git a/app/src/main/java/me/devsaki/hentoid/activities/IntentActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/IntentActivity.java index 937ba2de39..e93c03edac 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/IntentActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/IntentActivity.java @@ -3,7 +3,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import me.devsaki.hentoid.abstracts.BaseActivity; import me.devsaki.hentoid.database.domains.Content; diff --git a/app/src/main/java/me/devsaki/hentoid/activities/IntroActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/IntroActivity.java index 52ef871873..d0101eecc9 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/IntroActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/IntroActivity.java @@ -5,29 +5,31 @@ import android.os.Build; import android.os.Bundle; import android.provider.Settings; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; +import androidx.annotation.Nullable; +import com.google.android.material.snackbar.Snackbar; -import com.github.paolorotolo.appintro.AppIntro2; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.fragment.app.Fragment; -import java.security.InvalidParameterException; +import com.github.paolorotolo.appintro.AppIntro2; import me.devsaki.hentoid.BuildConfig; import me.devsaki.hentoid.HentoidApp; import me.devsaki.hentoid.R; -import me.devsaki.hentoid.fragments.BaseSlide; +import me.devsaki.hentoid.fragments.intro.BaseSlide; import me.devsaki.hentoid.fragments.intro.DoneIntroFragment; import me.devsaki.hentoid.fragments.intro.ImportIntroFragment; import me.devsaki.hentoid.fragments.intro.PermissionIntroFragment; +import me.devsaki.hentoid.fragments.intro.ThemeIntroFragment; import me.devsaki.hentoid.fragments.intro.WelcomeIntroFragment; import me.devsaki.hentoid.util.ConstsImport; import me.devsaki.hentoid.util.Preferences; import timber.log.Timber; -import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE; -import static android.support.design.widget.Snackbar.LENGTH_LONG; -import static android.support.design.widget.Snackbar.LENGTH_SHORT; +import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE; +import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; +import static com.google.android.material.snackbar.Snackbar.LENGTH_SHORT; +import static me.devsaki.hentoid.HentoidApp.darkModeFromPrefs; import static me.devsaki.hentoid.util.ConstsImport.RESULT_KEY; /** @@ -49,6 +51,7 @@ protected void onCreate(Bundle savedInstanceState) { } addSlide(BaseSlide.newInstance(R.layout.intro_slide_04)); addSlide(new ImportIntroFragment()); + addSlide(new ThemeIntroFragment()); addSlide(new DoneIntroFragment()); setTitle(R.string.app_name); @@ -86,6 +89,12 @@ public void onCustomStorageSelected() { HentoidApp.setBeginImport(true); } + public void setThemePrefs(int pref) { + Preferences.setDarkMode(pref); + AppCompatDelegate.setDefaultNightMode(darkModeFromPrefs(Preferences.getDarkMode())); + getPager().goToNextSlide(); + } + @Override public void onDonePressed(Fragment currentFragment) { Preferences.setIsFirstRun(false); diff --git a/app/src/main/java/me/devsaki/hentoid/activities/MikanSearchActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/MikanSearchActivity.java index 89a79c9ec0..b92c85686f 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/MikanSearchActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/MikanSearchActivity.java @@ -3,9 +3,9 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import me.devsaki.hentoid.R; import me.devsaki.hentoid.abstracts.BaseActivity; diff --git a/app/src/main/java/me/devsaki/hentoid/activities/PinPreferenceActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/PinPreferenceActivity.java index bccc05d44a..d6e5b7fca6 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/PinPreferenceActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/PinPreferenceActivity.java @@ -1,10 +1,10 @@ package me.devsaki.hentoid.activities; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import me.devsaki.hentoid.R; import me.devsaki.hentoid.fragments.pin.ActivatedPinPreferenceFragment; diff --git a/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.java index 25ea4a9d92..6dfb07d248 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/PrefsActivity.java @@ -1,17 +1,23 @@ package me.devsaki.hentoid.activities; import android.content.Intent; +import android.os.Build; import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceFragmentCompat; -import android.support.v7.preference.PreferenceScreen; import android.view.MenuItem; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.google.android.material.snackbar.Snackbar; + import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.R; import me.devsaki.hentoid.abstracts.BaseActivity; import me.devsaki.hentoid.events.ImportEvent; @@ -23,6 +29,8 @@ import me.devsaki.hentoid.util.Preferences; import me.devsaki.hentoid.util.ToastUtil; +import static me.devsaki.hentoid.HentoidApp.darkModeFromPrefs; + /** * Created by DevSaki on 20/05/2015. * Set up and present preferences. @@ -82,11 +90,11 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { findPreference(Preferences.Key.PREF_ANALYTICS_TRACKING) .setOnPreferenceChangeListener((preference, newValue) -> onPrefRequiringRestartChanged()); - findPreference(Preferences.Key.PREF_USE_SFW) - .setOnPreferenceChangeListener((preference, newValue) -> onPrefRequiringRestartChanged()); - findPreference(Preferences.Key.PREF_APP_LOCK) .setOnPreferenceClickListener(preference -> onAppLockPreferenceClick()); + + findPreference(Preferences.Key.DARK_MODE) + .setOnPreferenceChangeListener((preference, newValue) -> onPrefDarkModeChanged(newValue)); } } @@ -131,7 +139,11 @@ public void onNavigateToScreen(PreferenceScreen preferenceScreen) { private boolean onCheckUpdatePrefClick() { if (!UpdateDownloadService.isRunning()) { Intent intent = UpdateCheckService.makeIntent(requireContext(), true); - requireContext().startService(intent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + requireContext().startForegroundService(intent); + } else { + requireContext().startService(intent); + } } return true; } @@ -141,6 +153,11 @@ private boolean onPrefRequiringRestartChanged() { return true; } + private boolean onPrefDarkModeChanged(@Nonnull Object value) { + AppCompatDelegate.setDefaultNightMode(darkModeFromPrefs(Integer.parseInt(value.toString()))); + return true; + } + private boolean onAppLockPreferenceClick() { Intent intent = new Intent(requireContext(), PinPreferenceActivity.class); startActivity(intent); diff --git a/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.java index 2bbec2264c..5f8fcc0a3a 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.activities; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import android.view.MenuItem; import me.devsaki.hentoid.R; @@ -19,10 +19,7 @@ public class QueueActivity extends BaseActivity implements BackInterface { private BaseFragment baseFragment; private Fragment fragment; - private QueueFragment buildFragment() { - return QueueFragment.newInstance(); - } - + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -37,7 +34,7 @@ protected void onCreate(Bundle savedInstanceState) { fragment = manager.findFragmentById(R.id.content_frame); if (fragment == null) { - fragment = buildFragment(); + fragment = new QueueFragment(); manager.beginTransaction() .add(R.id.content_frame, fragment, getFragmentTag()) @@ -62,14 +59,11 @@ public void onBackPressed() { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - // Respond to the action bar's Up/Home button - case android.R.id.home: - super.onBackPressed(); - return true; - default: - return super.onOptionsItemSelected(item); + if (item.getItemId() == android.R.id.home) { + super.onBackPressed(); + return true; } + return super.onOptionsItemSelected(item); } @Override diff --git a/app/src/main/java/me/devsaki/hentoid/activities/SearchActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/SearchActivity.java index def5d56492..ab219979f9 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/SearchActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/SearchActivity.java @@ -1,14 +1,14 @@ package me.devsaki.hentoid.activities; import android.app.Activity; -import android.arch.lifecycle.ViewModelProviders; +import androidx.lifecycle.ViewModelProviders; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.Toolbar; +import com.google.android.material.snackbar.Snackbar; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.appcompat.widget.Toolbar; import android.util.SparseIntArray; import android.view.View; import android.widget.TextView; @@ -61,7 +61,7 @@ protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); SearchActivityBundle.Builder builder = new SearchActivityBundle.Builder(); - builder.setUri(Helper.buildSearchUri(viewModel.getSelectedAttributesData().getValue())); + builder.setUri(SearchActivityBundle.Builder.buildSearchUri(viewModel.getSelectedAttributesData().getValue())); outState.putAll(builder.getBundle()); } @@ -71,7 +71,7 @@ protected void onRestoreInstanceState(Bundle savedInstanceState) { Uri searchUri = new SearchActivityBundle.Parser(savedInstanceState).getUri(); if (searchUri != null) { - List preSelectedAttributes = Helper.parseSearchUri(searchUri); + List preSelectedAttributes = SearchActivityBundle.Parser.parseSearchUri(searchUri); if (preSelectedAttributes != null) viewModel.setSelectedAttributes(preSelectedAttributes); } @@ -88,7 +88,7 @@ protected void onCreate(Bundle savedInstanceState) { SearchActivityBundle.Parser parser = new SearchActivityBundle.Parser(intent.getExtras()); mode = parser.getMode(); Uri searchUri = parser.getUri(); - if (searchUri != null) preSelectedAttributes = Helper.parseSearchUri(searchUri); + if (searchUri != null) preSelectedAttributes = SearchActivityBundle.Parser.parseSearchUri(searchUri); } setContentView(R.layout.activity_search); @@ -200,7 +200,7 @@ private void onBooksReady(SearchViewModel.ContentSearchResult result) { } private void validateForm() { - Uri searchUri = Helper.buildSearchUri(viewModel.getSelectedAttributesData().getValue()); + Uri searchUri = SearchActivityBundle.Builder.buildSearchUri(viewModel.getSelectedAttributesData().getValue()); Timber.d("URI :%s", searchUri); SearchActivityBundle.Builder builder = new SearchActivityBundle.Builder().setUri(searchUri); diff --git a/app/src/main/java/me/devsaki/hentoid/activities/SplashActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/SplashActivity.java index 0021fe40c3..e0894fd6a6 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/SplashActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/SplashActivity.java @@ -2,8 +2,10 @@ import android.app.ProgressDialog; import android.content.Intent; +import android.os.Build; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; + +import androidx.appcompat.app.AppCompatActivity; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -82,6 +84,10 @@ private void handleDatabaseMigration() { progressDialog.show(); Intent intent = DatabaseMigrationService.makeIntent(this); - startService(intent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent); + } else { + startService(intent); + } } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.java index 6083b9e4f3..0e4237b24c 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.java @@ -2,15 +2,11 @@ import android.os.Bundle; -import java.util.ArrayList; -import java.util.List; - import javax.annotation.Nonnull; -import javax.annotation.Nullable; public class ImageViewerActivityBundle { - private static final String KEY_URIS_STR = "urisStr"; private static final String KEY_CONTENT_ID = "contentId"; + private static final String KEY_SEARCH_PARAMS = "searchParams"; private ImageViewerActivityBundle() { throw new UnsupportedOperationException(); @@ -24,9 +20,8 @@ public void setContentId(long contentId) { bundle.putLong(KEY_CONTENT_ID, contentId); } - public void setUrisStr(List uris) { - ArrayList uriList = new ArrayList<>(uris); - bundle.putStringArrayList(KEY_URIS_STR, uriList); + public void setSearchParams(Bundle params) { + bundle.putBundle(KEY_SEARCH_PARAMS, params); } public Bundle getBundle() { @@ -42,13 +37,12 @@ public Parser(@Nonnull Bundle bundle) { this.bundle = bundle; } - @Nullable - public List getUrisStr() { - return bundle.getStringArrayList(KEY_URIS_STR); - } - public long getContentId() { return bundle.getLong(KEY_CONTENT_ID, 0); } + + public Bundle getSearchParams() { + return bundle.getBundle(KEY_SEARCH_PARAMS); + } } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.java index 58cbdf9315..ea205dd8c0 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.java @@ -9,7 +9,9 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import me.devsaki.hentoid.database.domains.Attribute; import me.devsaki.hentoid.enums.AttributeType; +import me.devsaki.hentoid.util.AttributeMap; public class SearchActivityBundle { private static final String KEY_ATTRIBUTE_TYPES = "attributeTypes"; @@ -40,6 +42,21 @@ public Builder setUri(Uri uri) { return this; } + public static Uri buildSearchUri(List attributes) { + AttributeMap metadataMap = new AttributeMap(); + metadataMap.addAll(attributes); + + Uri.Builder searchUri = new Uri.Builder() + .scheme("search") + .authority("hentoid"); + for (AttributeType attrType : metadataMap.keySet()) { + List attrs = metadataMap.get(attrType); + for (Attribute attr : attrs) + searchUri.appendQueryParameter(attrType.name(), attr.getId() + ";" + attr.getName()); + } + return searchUri.build(); + } + public Bundle getBundle() { return bundle; } @@ -76,5 +93,25 @@ public Uri getUri() { return result; } + + public static List parseSearchUri(Uri uri) { + List result = new ArrayList<>(); + + if (uri != null) + for (String typeStr : uri.getQueryParameterNames()) { + AttributeType type = AttributeType.searchByName(typeStr); + if (type != null) + for (String attrStr : uri.getQueryParameters(typeStr)) { + String[] attrParams = attrStr.split(";"); + if (2 == attrParams.length) { + result.add(new Attribute(type, attrParams[1]).setId(Long.parseLong(attrParams[0]))); + } + } + } + + return result; + } } + + } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/ASMHentaiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/ASMHentaiActivity.java new file mode 100644 index 0000000000..a0c52f049e --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/ASMHentaiActivity.java @@ -0,0 +1,27 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +/** + * Created by avluis on 07/21/2016. + * Implements ASMHentai source + */ +public class ASMHentaiActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "asmhentai.com"; + private static final String GALLERY_FILTER = "asmhentai.com/g/"; + private static final String[] blockedContent = {"f.js"}; + + Site getStartSite() { + return Site.ASMHENTAI; + } + + + @Override + protected CustomWebViewClient getWebClient() { + addContentBlockFilter(blockedContent); + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/BaseWebActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java similarity index 68% rename from app/src/main/java/me/devsaki/hentoid/activities/websites/BaseWebActivity.java rename to app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java index 326ac5b747..7fd24c8776 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/BaseWebActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java @@ -1,19 +1,18 @@ -package me.devsaki.hentoid.activities.websites; +package me.devsaki.hentoid.activities.sources; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.Matrix; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.support.annotation.DrawableRes; -import android.support.annotation.NonNull; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.widget.SwipeRefreshLayout; +import android.util.Pair; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; +import android.webkit.CookieManager; import android.webkit.WebBackForwardList; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; @@ -22,15 +21,33 @@ import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.Nonnull; + +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; import me.devsaki.hentoid.HentoidApp; import me.devsaki.hentoid.R; import me.devsaki.hentoid.abstracts.BaseActivity; @@ -43,14 +60,21 @@ import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.listener.ResultListener; +import me.devsaki.hentoid.parsers.ContentParserFactory; +import me.devsaki.hentoid.parsers.content.ContentParser; import me.devsaki.hentoid.services.ContentQueueManager; import me.devsaki.hentoid.util.Consts; import me.devsaki.hentoid.util.FileHelper; import me.devsaki.hentoid.util.Helper; +import me.devsaki.hentoid.util.HttpHelper; +import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.PermissionUtil; import me.devsaki.hentoid.util.Preferences; import me.devsaki.hentoid.util.ToastUtil; import me.devsaki.hentoid.views.ObservableWebView; +import okhttp3.Response; +import pl.droidsonroids.jspoon.HtmlAdapter; +import pl.droidsonroids.jspoon.Jspoon; import timber.log.Timber; /** @@ -59,32 +83,35 @@ * The source itself should contain every method it needs to function. *

* todo issue: - * {@link #checkPermissions()} causes the app to reset unexpectedly. If permission is integral to - * this activity's function, it is recommended to request for this permission and show rationale if - * permission request is denied + * {@link #checkPermissions()} causes the app to reset unexpectedly. If permission is integral to + * this activity's function, it is recommended to request for this permission and show rationale if + * permission request is denied */ public abstract class BaseWebActivity extends BaseActivity implements ResultListener { protected static final int MODE_DL = 0; - protected static final int MODE_QUEUE = 1; - protected static final int MODE_READ = 2; + private static final int MODE_QUEUE = 1; + private static final int MODE_READ = 2; // UI - protected ObservableWebView webView; // Associated webview - private FloatingActionButton fabAction, fabRefreshOrStop, fabHome; // Action buttons + // Associated webview + protected ObservableWebView webView; + // Action buttons + private FloatingActionButton fabAction; + private FloatingActionButton fabRefreshOrStop; + private FloatingActionButton fabHome; + // Swipe layout private SwipeRefreshLayout swipeLayout; // Content currently viewed private Content currentContent; // Database private ObjectBoxDB db; - // Indicates if webView is loading - private boolean webViewIsLoading; // Indicated which mode the download FAB is in protected int fabActionMode; private boolean fabActionEnabled; - protected CustomWebViewClient webClient; + private CustomWebViewClient webClient; // List of blocked content (ads or annoying images) -- will be replaced by a blank stream private static final List universalBlockedContent = new ArrayList<>(); // Universal list (applied to all sites) @@ -110,6 +137,8 @@ public abstract class BaseWebActivity extends BaseActivity implements ResultList universalBlockedContent.add("adsco.re"); universalBlockedContent.add("s24hc8xzag.com"); universalBlockedContent.add("/nutaku/"); + universalBlockedContent.add("trafficjunky"); + universalBlockedContent.add("traffichaus"); } protected abstract CustomWebViewClient getWebClient(); @@ -211,9 +240,9 @@ public void onProgressChanged(WebView view, int newProgress) { } } }); - webView.setOnScrollChangedCallback((l, t) -> { - if (!webViewIsLoading) { - if (webView.canScrollVertically(1) || t == 0) { + webView.setOnScrollChangedCallback((deltaX, deltaY) -> { + if (!webClient.isLoading()) { + if (deltaY <= 0) { fabRefreshOrStop.show(); fabHome.show(); if (fabActionEnabled) fabAction.show(); @@ -255,7 +284,7 @@ public void onProgressChanged(WebView view, int newProgress) { private void initSwipeLayout() { swipeLayout = findViewById(R.id.swipe_container); swipeLayout.setOnRefreshListener(() -> { - if (!swipeLayout.isRefreshing() || !webViewIsLoading) { + if (!swipeLayout.isRefreshing() || !webClient.isLoading()) { webView.reload(); } }); @@ -267,7 +296,7 @@ private void initSwipeLayout() { } public void onRefreshStopFabClick(View view) { - if (webViewIsLoading) { + if (webClient.isLoading()) { webView.stopLoading(); } else { webView.reload(); @@ -308,39 +337,31 @@ public void onHomeFabClick(View view) { public void onActionFabClick(View view) { if (MODE_DL == fabActionMode) processDownload(); else if (MODE_QUEUE == fabActionMode) goToQueue(); - else if (MODE_READ == fabActionMode) - { + else if (MODE_READ == fabActionMode && currentContent != null) { + currentContent = db.selectContentByUrl(currentContent.getUrl()); if (currentContent != null) { - currentContent = db.selectContentByUrl(currentContent.getUrl()); - if (currentContent != null) { - if (StatusContent.DOWNLOADED == currentContent.getStatus() - || StatusContent.ERROR == currentContent.getStatus() - || StatusContent.MIGRATED == currentContent.getStatus()) - { - FileHelper.openContent(this, currentContent); - } else { - fabAction.hide(); - } + if (StatusContent.DOWNLOADED == currentContent.getStatus() + || StatusContent.ERROR == currentContent.getStatus() + || StatusContent.MIGRATED == currentContent.getStatus()) { + FileHelper.openContent(this, currentContent); + } else { + fabAction.hide(); } } } } - private void changeFabActionMode(int mode) - { + private void changeFabActionMode(int mode) { @DrawableRes int resId = R.drawable.ic_menu_about; if (MODE_DL == mode) { resId = R.drawable.ic_action_download; - } - else if (MODE_QUEUE == mode) { - resId = R.drawable.ic_queued; - } - else if (MODE_READ == mode) - { + } else if (MODE_QUEUE == mode) { + resId = R.drawable.ic_action_queue; + } else if (MODE_READ == mode) { resId = R.drawable.ic_action_play; } fabActionMode = mode; - fabAction.setImageResource(resId); + setFabIcon(fabAction, resId); fabActionEnabled = true; fabAction.show(); } @@ -351,7 +372,8 @@ else if (MODE_READ == mode) void processDownload() { if (null == currentContent) return; - if (currentContent.getId() > 0) currentContent = db.selectContentById(currentContent.getId()); + if (currentContent.getId() > 0) + currentContent = db.selectContentById(currentContent.getId()); if (null == currentContent) return; @@ -368,7 +390,7 @@ void processDownload() { List queue = db.selectQueue(); int lastIndex = 1; - if (queue.size() > 0) { + if (!queue.isEmpty()) { lastIndex = queue.get(queue.size() - 1).rank + 1; } db.insertQueue(currentContent.getId(), lastIndex); @@ -415,7 +437,7 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { * * @param content Currently displayed content */ - void processContent(Content content) { + private void processContent(Content content) { if (null == content || null == content.getUrl()) { return; } @@ -450,6 +472,11 @@ void processContent(Content content) { currentContent = content; } + private void hideActionFab() { + fabAction.hide(); + fabActionEnabled = false; + } + public void onResultReady(Content results, long totalContent) { processContent(results); } @@ -458,21 +485,50 @@ public void onResultFailed(String message) { runOnUiThread(() -> ToastUtil.toast(HentoidApp.getAppContext(), R.string.web_unparsable)); } + /** + * Indicates if the given URL is forbidden by the current content filters + * + * @param url URL to be examinated + * @return True if URL is forbidden according to current filters; false if not + */ + private boolean isUrlForbidden(String url) { + for (String s : universalBlockedContent) { + if (url.contains(s)) return true; + } + if (localBlockedContent != null) + for (String s : localBlockedContent) { + if (url.contains(s)) return true; + } + + return false; + } - abstract class CustomWebViewClient extends WebViewClient { + /** + * Analyze loaded HTML to display download button + * Override blocked content with empty content + */ + class CustomWebViewClient extends WebViewClient { + + private final Jspoon jspoon = Jspoon.create(); protected final CompositeDisposable compositeDisposable = new CompositeDisposable(); - final ByteArrayInputStream nothing = new ByteArrayInputStream("".getBytes()); + private final ByteArrayInputStream nothing = new ByteArrayInputStream("".getBytes()); protected final ResultListener listener; private final Pattern filteredUrlPattern; + private final HtmlAdapter htmlAdapter; - private String domainName = ""; - - protected abstract void onGalleryFound(String url); + private String restrictedDomainName = ""; + private boolean isPageLoading = false; + private boolean isHtmlLoaded = false; + @SuppressWarnings("unchecked") CustomWebViewClient(String filteredUrl, ResultListener listener) { this.listener = listener; + + Class c = ContentParserFactory.getInstance().getContentParserClass(getStartSite()); + htmlAdapter = jspoon.adapter(c); // Unchecked but alright + if (filteredUrl.length() > 0) filteredUrlPattern = Pattern.compile(filteredUrl); else filteredUrlPattern = null; } @@ -483,7 +539,7 @@ void destroy() { } void restrictTo(String s) { - domainName = s; + restrictedDomainName = s; } private boolean isPageFiltered(String url) { @@ -494,43 +550,45 @@ private boolean isPageFiltered(String url) { } @Override + @Deprecated public boolean shouldOverrideUrlLoading(WebView view, String url) { String hostStr = Uri.parse(url).getHost(); - return hostStr != null && !hostStr.contains(domainName); + return hostStr != null && !hostStr.contains(restrictedDomainName); } @TargetApi(Build.VERSION_CODES.N) @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { String hostStr = Uri.parse(request.getUrl().toString()).getHost(); - return hostStr != null && !hostStr.contains(domainName); + return hostStr != null && !hostStr.contains(restrictedDomainName); } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - webViewIsLoading = true; - fabRefreshOrStop.setImageResource(R.drawable.ic_action_clear); + setFabIcon(fabRefreshOrStop, R.drawable.ic_action_clear); fabRefreshOrStop.show(); fabHome.show(); - - fabAction.hide(); - fabActionEnabled = false; - - if (isPageFiltered(url)) onGalleryFound(url); + isPageLoading = true; + if (!isHtmlLoaded) hideActionFab(); } @Override public void onPageFinished(WebView view, String url) { - webViewIsLoading = false; - fabRefreshOrStop.setImageResource(R.drawable.ic_action_refresh); + isPageLoading = false; + setFabIcon(fabRefreshOrStop, R.drawable.ic_action_refresh); } @Override + @Deprecated public WebResourceResponse shouldInterceptRequest(@NonNull WebView view, @NonNull String url) { if (isUrlForbidden(url)) { return new WebResourceResponse("text/plain", "utf-8", nothing); } else { + if (!isPageLoading) { + isHtmlLoaded = false; + if (isPageFiltered(url)) return parseResponse(url, null); + } return super.shouldInterceptRequest(view, url); } } @@ -543,26 +601,81 @@ public WebResourceResponse shouldInterceptRequest(@NonNull WebView view, if (isUrlForbidden(url)) { return new WebResourceResponse("text/plain", "utf-8", nothing); } else { + if (!isPageLoading) { + isHtmlLoaded = false; + if (isPageFiltered(url)) return parseResponse(url, request.getRequestHeaders()); + } return super.shouldInterceptRequest(view, request); } } - } - /** - * Indicates if the given URL is forbidden by the current content filters - * - * @param url URL to be examinated - * @return True if URL is forbidden according to current filters; false if not - */ - protected boolean isUrlForbidden(String url) { - for (String s : universalBlockedContent) { - if (url.contains(s)) return true; - } - if (localBlockedContent != null) - for (String s : localBlockedContent) { - if (url.contains(s)) return true; + protected WebResourceResponse parseResponse(@NonNull String urlStr, @Nullable Map headers) { + List> headersList = new ArrayList<>(); + + if (headers != null) + for (String key : headers.keySet()) + headersList.add(new Pair<>(key, headers.get(key))); + + String cookie = CookieManager.getInstance().getCookie(urlStr); + if (cookie != null) headersList.add(new Pair<>(HttpHelper.HEADER_COOKIE_KEY, cookie)); + + try { + Response response = HttpHelper.getOnlineResource(urlStr, headersList, getStartSite().canKnowHentoidAgent()); + if (null == response.body()) throw new IOException("Empty body"); + + // Response body bytestream needs to be duplicated + // because Jsoup closes it, which makes it unavailable for the WebView to use + List is = Helper.duplicateInputStream(response.body().byteStream(), 2); + + compositeDisposable.add( + Single.fromCallable(() -> htmlAdapter.fromInputStream(is.get(0), new URL(urlStr)).toContent(urlStr)) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> processContent(result, headersList), + throwable -> { + Timber.e(throwable, "Error parsing content."); + isHtmlLoaded = true; + listener.onResultFailed(""); + }) + ); + + return HttpHelper.okHttpResponseToWebResourceResponse(response, is.get(1)); + } catch (MalformedURLException e) { + Timber.e(e, "Malformed URL : %s", urlStr); + } catch (IOException e) { + Timber.e(e); } + return null; + } - return false; + private void processContent(@Nonnull Content content, @Nonnull List> headersList) { + if (content.getStatus() != null && content.getStatus().equals(StatusContent.IGNORED)) + return; + + // Save cookies for future calls during download + Map params = new HashMap<>(); + for (Pair p : headersList) + if (p.first.equals(HttpHelper.HEADER_COOKIE_KEY)) params.put(HttpHelper.HEADER_COOKIE_KEY, p.second); + + content.setDownloadParams(JsonHelper.serializeToJson(params)); + isHtmlLoaded = true; + listener.onResultReady(content, 1); + } + + /** + * Indicated whether the current webpage is still loading or not + * + * @return True if current webpage is being loaded; false if not + */ + public boolean isLoading() { + return isPageLoading; + } + } + + // Workaround for https://issuetracker.google.com/issues/111316656 + private void setFabIcon(@Nonnull FloatingActionButton btn, @DrawableRes int resId) { + btn.setImageResource(resId); + btn.setImageMatrix(new Matrix()); } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/EHentaiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/EHentaiActivity.java similarity index 74% rename from app/src/main/java/me/devsaki/hentoid/activities/websites/EHentaiActivity.java rename to app/src/main/java/me/devsaki/hentoid/activities/sources/EHentaiActivity.java index 4db2bdfe2c..0503f53db3 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/EHentaiActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/EHentaiActivity.java @@ -1,13 +1,18 @@ -package me.devsaki.hentoid.activities.websites; +package me.devsaki.hentoid.activities.sources; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.webkit.CookieManager; +import android.webkit.WebResourceResponse; + +import java.util.Map; import io.reactivex.android.schedulers.AndroidSchedulers; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.listener.ResultListener; import me.devsaki.hentoid.parsers.content.EHentaiGalleryQuery; -import me.devsaki.hentoid.retrofit.EHentaiServer; +import me.devsaki.hentoid.retrofit.sources.EHentaiServer; import timber.log.Timber; /** @@ -37,18 +42,21 @@ private class EHentaiWebClient extends CustomWebViewClient { super(filter, listener); } + // We keep calling the API without using BaseWebActivity.parseResponse @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); + protected WebResourceResponse parseResponse(@NonNull String urlStr, @Nullable Map headers) { + String[] galleryUrlParts = urlStr.split("/"); EHentaiGalleryQuery query = new EHentaiGalleryQuery(galleryUrlParts[4], galleryUrlParts[5]); compositeDisposable.add(EHentaiServer.API.getGalleryMetadata(query) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { + metadata -> listener.onResultReady(metadata.toContent(urlStr), 1), + throwable -> { Timber.e(throwable, "Error parsing content."); listener.onResultFailed(""); }) ); + return null; } } } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/FakkuActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/FakkuActivity.java new file mode 100644 index 0000000000..8a782a3d28 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/FakkuActivity.java @@ -0,0 +1,20 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +public class FakkuActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "fakku.net"; + private static final String GALLERY_FILTER = "fakku.net/hentai/"; + + Site getStartSite() { + return Site.FAKKU2; + } + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/HentaiCafeActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/HentaiCafeActivity.java new file mode 100644 index 0000000000..ad1041300a --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/HentaiCafeActivity.java @@ -0,0 +1,53 @@ +package me.devsaki.hentoid.activities.sources; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.webkit.WebResourceResponse; + +import java.util.Map; + +import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.listener.ResultListener; + +import static me.devsaki.hentoid.enums.Site.HENTAICAFE; + +/** + * Created by avluis on 07/21/2016. + * Implements Hentai Cafe source + */ +public class HentaiCafeActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "hentai.cafe"; + private static final String GALLERY_FILTER = "//hentai.cafe/[^/]+/$"; + + Site getStartSite() { + return Site.HENTAICAFE; + } + + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new HentaiCafeWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } + + private class HentaiCafeWebViewClient extends CustomWebViewClient { + + HentaiCafeWebViewClient(String filteredUrl, ResultListener listener) { + super(filteredUrl, listener); + } + + @Override + protected WebResourceResponse parseResponse(@NonNull String urlStr, @Nullable Map headers) { + if (urlStr.startsWith(HENTAICAFE.getUrl() + "/78-2/") // ignore tags page + || urlStr.startsWith(HENTAICAFE.getUrl() + "/artists/") // ignore artist page + || urlStr.startsWith(HENTAICAFE.getUrl() + "/?s=") // ignore text search results + ) { + return null; + } + return super.parseResponse(urlStr, headers); + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/HitomiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/HitomiActivity.java new file mode 100644 index 0000000000..83b5327cb2 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/HitomiActivity.java @@ -0,0 +1,26 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +/** + * Created by Shiro on 1/20/2016. + * Implements Hitomi.la source + */ +public class HitomiActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "hitomi.la"; + private static final String GALLERY_FILTER = "//hitomi.la/galleries/"; + private static final String[] blockedContent = {"hitomi-horizontal.js", "hitomi-vertical.js"}; + + Site getStartSite() { + return Site.HITOMI; + } + + @Override + protected CustomWebViewClient getWebClient() { + addContentBlockFilter(blockedContent); + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/MusesActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/MusesActivity.java new file mode 100644 index 0000000000..c1f29555f4 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/MusesActivity.java @@ -0,0 +1,20 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +public class MusesActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "8muses.com"; + private static final String GALLERY_FILTER = "//www.8muses.com/comics/album/"; + + Site getStartSite() { + return Site.MUSES; + } + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/NexusActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/NexusActivity.java new file mode 100644 index 0000000000..0fd80d8352 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/NexusActivity.java @@ -0,0 +1,20 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +public class NexusActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "hentainexus.com"; + private static final String GALLERY_FILTER = "//hentainexus.com/view/"; + + Site getStartSite() { + return Site.NEXUS; + } + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/NhentaiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/NhentaiActivity.java new file mode 100644 index 0000000000..0b80906551 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/NhentaiActivity.java @@ -0,0 +1,25 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +/** + * Created by Shiro on 1/20/2016. + * Implements nhentai source + */ +public class NhentaiActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "nhentai.net"; + private static final String GALLERY_FILTER = "nhentai.net/g/"; + + Site getStartSite() { + return Site.NHENTAI; + } + + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/PururinActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/PururinActivity.java new file mode 100644 index 0000000000..6e77fcf0c2 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/PururinActivity.java @@ -0,0 +1,20 @@ +package me.devsaki.hentoid.activities.sources; + +import me.devsaki.hentoid.enums.Site; + +public class PururinActivity extends BaseWebActivity { + + private static final String DOMAIN_FILTER = "pururin.io"; + private static final String GALLERY_FILTER = "//pururin.io/gallery/"; + + Site getStartSite() { + return Site.PURURIN; + } + + @Override + protected CustomWebViewClient getWebClient() { + CustomWebViewClient client = new CustomWebViewClient(GALLERY_FILTER, this); + client.restrictTo(DOMAIN_FILTER); + return client; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/TsuminoActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/TsuminoActivity.java similarity index 77% rename from app/src/main/java/me/devsaki/hentoid/activities/websites/TsuminoActivity.java rename to app/src/main/java/me/devsaki/hentoid/activities/sources/TsuminoActivity.java index 26b0ebc163..0b41f1deef 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/TsuminoActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/TsuminoActivity.java @@ -1,15 +1,12 @@ -package me.devsaki.hentoid.activities.websites; +package me.devsaki.hentoid.activities.sources; import android.graphics.Bitmap; import android.view.View; import android.webkit.WebView; -import io.reactivex.android.schedulers.AndroidSchedulers; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.TsuminoServer; -import timber.log.Timber; /** * Created by Shiro on 1/22/2016. @@ -70,21 +67,6 @@ private class TsuminoWebViewClient extends CustomWebViewClient { super(galleryFilter, listener); } - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - // Tsumino books can be called through two different URLs : "book ID" and "book ID/book-name" - // -> need to get the book ID only - compositeDisposable.add(TsuminoServer.API.getGalleryMetadata(galleryUrlParts[5]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/ASMHentaiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/ASMHentaiActivity.java deleted file mode 100644 index c4726c745f..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/ASMHentaiActivity.java +++ /dev/null @@ -1,64 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.ASMComicsServer; -import me.devsaki.hentoid.retrofit.ASMHentaiServer; -import timber.log.Timber; - -/** - * Created by avluis on 07/21/2016. - * Implements ASMHentai source - */ -public class ASMHentaiActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "asmhentai.com"; - private static final String GALLERY_FILTER = "asmhentai.com/g/"; - private static final String[] blockedContent = {"f.js"}; - - Site getStartSite() { - return Site.ASMHENTAI; - } - - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new ASMViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class ASMViewClient extends CustomWebViewClient { - - ASMViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - addContentBlockFilter(blockedContent); - } - - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - if (url.contains("comics.asm")) { - compositeDisposable.add(ASMComicsServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 1]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } else { - compositeDisposable.add(ASMHentaiServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 1]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/FakkuActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/FakkuActivity.java deleted file mode 100644 index 2b10002752..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/FakkuActivity.java +++ /dev/null @@ -1,62 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import android.webkit.CookieManager; - -import java.util.HashMap; -import java.util.Map; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.FakkuServer; -import me.devsaki.hentoid.util.JsonHelper; -import timber.log.Timber; - -public class FakkuActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "fakku.net"; - private static final String GALLERY_FILTER = "fakku.net/hentai/"; - - Site getStartSite() { - return Site.FAKKU2; - } - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new FakkuViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class FakkuViewClient extends CustomWebViewClient { - - FakkuViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - } - - @Override - protected void onGalleryFound(String url) { - String cookie = CookieManager.getInstance().getCookie(url); - String[] galleryUrlParts = url.split("/"); - compositeDisposable.add(FakkuServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 1], cookie) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> { - Content content = metadata.toContent(); - if (content != null) { - // Save cookies for future calls during download - Map params = new HashMap<>(); - params.put("cookie", cookie); - content.setDownloadParams(JsonHelper.serializeToJson(params)); - } - - listener.onResultReady(content, 1); - }, throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/HentaiCafeActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/HentaiCafeActivity.java deleted file mode 100644 index 3d991e2008..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/HentaiCafeActivity.java +++ /dev/null @@ -1,61 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import android.net.Uri; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.HentaiCafeServer; -import timber.log.Timber; - -import static me.devsaki.hentoid.enums.Site.HENTAICAFE; - -/** - * Created by avluis on 07/21/2016. - * Implements Hentai Cafe source - */ -public class HentaiCafeActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "hentai.cafe"; - private static final String GALLERY_FILTER = "//hentai.cafe/[^/]+/$"; - - Site getStartSite() { - return Site.HENTAICAFE; - } - - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new HentaiCafeWebViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class HentaiCafeWebViewClient extends CustomWebViewClient { - - HentaiCafeWebViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - } - - @Override - protected void onGalleryFound(String url) { - if ( url.startsWith(HENTAICAFE.getUrl() + "/78-2/") // ignore tags page - || url.startsWith(HENTAICAFE.getUrl() + "/artists/") // ignore artist page - || url.startsWith(HENTAICAFE.getUrl() + "/?s=") // ignore text search results - ) { - return; - } - - String[] galleryUrlParts = url.split("/"); - compositeDisposable.add(HentaiCafeServer.API.getGalleryMetadata(Uri.decode(galleryUrlParts[galleryUrlParts.length - 1])) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/HitomiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/HitomiActivity.java deleted file mode 100644 index eb2a72d5a5..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/HitomiActivity.java +++ /dev/null @@ -1,52 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.HitomiServer; -import timber.log.Timber; - -/** - * Created by Shiro on 1/20/2016. - * Implements Hitomi.la source - */ -public class HitomiActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "hitomi.la"; - private static final String GALLERY_FILTER = "//hitomi.la/galleries/"; - private static final String[] blockedContent = {"hitomi-horizontal.js", "hitomi-vertical.js"}; - - Site getStartSite() { - return Site.HITOMI; - } - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new HitomiWebViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - - private class HitomiWebViewClient extends CustomWebViewClient { - - HitomiWebViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - addContentBlockFilter(blockedContent); - } - - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - compositeDisposable.add(HitomiServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 1]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/NhentaiActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/NhentaiActivity.java deleted file mode 100644 index 9cd5650ae3..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/NhentaiActivity.java +++ /dev/null @@ -1,62 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.NhentaiServer; -import timber.log.Timber; - -/** - * Created by Shiro on 1/20/2016. - * Implements nhentai source - */ -public class NhentaiActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "nhentai.net"; - private static final String GALLERY_FILTER = "nhentai.net/g/"; - - Site getStartSite() { - return Site.NHENTAI; - } - - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new NhentaiWebViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class NhentaiWebViewClient extends CustomWebViewClient { - - NhentaiWebViewClient(String galleryUrl, ResultListener listener) { - super(galleryUrl, listener); - } - - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - - boolean gFound = false; - String bookId = ""; - for (String s : galleryUrlParts) { - if (gFound) { - bookId = s; - break; - } - if (s.equals("g")) gFound = true; - } - - compositeDisposable.add(NhentaiServer.API.getGalleryMetadata(bookId) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), - throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/PandaActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/PandaActivity.java deleted file mode 100644 index a782ceac96..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/PandaActivity.java +++ /dev/null @@ -1,50 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.PandaServer; -import timber.log.Timber; - -/** - * Created by Robb_w on 2018/04 - * Implements MangaPanda source - */ -public class PandaActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "mangapanda.com"; - private static final String GALLERY_FILTER = "mangapanda.com/[A-Za-z0-9\\-_]+/[0-9]+"; - - Site getStartSite() { - return Site.PANDA; - } - - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new PandaWebViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class PandaWebViewClient extends CustomWebViewClient { - - PandaWebViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - } - - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - compositeDisposable.add(PandaServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 2], galleryUrlParts[galleryUrlParts.length - 1]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/websites/PururinActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/websites/PururinActivity.java deleted file mode 100644 index 39bfed026e..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/websites/PururinActivity.java +++ /dev/null @@ -1,45 +0,0 @@ -package me.devsaki.hentoid.activities.websites; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.PururinServer; -import timber.log.Timber; - -public class PururinActivity extends BaseWebActivity { - - private static final String DOMAIN_FILTER = "pururin.io"; - private static final String GALLERY_FILTER = "//pururin.io/gallery/"; - - Site getStartSite() { - return Site.PURURIN; - } - - @Override - protected CustomWebViewClient getWebClient() { - CustomWebViewClient client = new PururinViewClient(GALLERY_FILTER, this); - client.restrictTo(DOMAIN_FILTER); - return client; - } - - private class PururinViewClient extends CustomWebViewClient { - - PururinViewClient(String filteredUrl, ResultListener listener) { - super(filteredUrl, listener); - } - - @Override - protected void onGalleryFound(String url) { - String[] galleryUrlParts = url.split("/"); - compositeDisposable.add(PururinServer.API.getGalleryMetadata(galleryUrlParts[galleryUrlParts.length - 2], galleryUrlParts[galleryUrlParts.length - 1]) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - metadata -> listener.onResultReady(metadata.toContent(), 1), throwable -> { - Timber.e(throwable, "Error parsing content."); - listener.onResultFailed(""); - }) - ); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/AvailableAttributeAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/AvailableAttributeAdapter.java index bc2693822f..eb2d9bb0b1 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/AvailableAttributeAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/AvailableAttributeAdapter.java @@ -1,7 +1,7 @@ package me.devsaki.hentoid.adapters; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ContentAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/ContentAdapter.java index 1879973919..461053ce65 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ContentAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ContentAdapter.java @@ -2,26 +2,27 @@ import android.content.Context; import android.content.Intent; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AlertDialog; -import android.support.v7.util.SortedList; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.util.SortedListAdapterCallback; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SortedList; +import androidx.recyclerview.widget.SortedListAdapterCallback; + +import com.annimon.stream.function.Consumer; import com.annimon.stream.function.IntConsumer; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.RequestOptions; import com.crashlytics.android.Crashlytics; +import com.google.android.material.snackbar.Snackbar; import java.io.File; -import java.io.IOException; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Comparator; @@ -44,17 +45,15 @@ import me.devsaki.hentoid.database.domains.QueueRecord; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.StatusContent; -import me.devsaki.hentoid.listener.ContentListener; import me.devsaki.hentoid.listener.ContentClickListener; import me.devsaki.hentoid.listener.ContentClickListener.ItemSelectListener; +import me.devsaki.hentoid.listener.PagedResultListener; import me.devsaki.hentoid.services.ContentQueueManager; import me.devsaki.hentoid.ui.BlinkAnimation; import me.devsaki.hentoid.util.ContentNotRemovedException; import me.devsaki.hentoid.util.FileHelper; import me.devsaki.hentoid.util.Helper; -import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.LogUtil; -import me.devsaki.hentoid.util.Preferences; import me.devsaki.hentoid.util.ToastUtil; import timber.log.Timber; @@ -62,7 +61,7 @@ * Created by avluis on 04/23/2016. RecyclerView based Content Adapter * TODO - Consider replacing with https://github.com/davideas/FlexibleAdapter */ -public class ContentAdapter extends RecyclerView.Adapter implements ContentListener { +public class ContentAdapter extends RecyclerView.Adapter implements PagedResultListener { private static final int VISIBLE_THRESHOLD = 10; @@ -74,6 +73,7 @@ public class ContentAdapter extends RecyclerView.Adapter implemen private final int displayMode; private final RequestOptions glideRequestOptions; private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + private final Consumer openBookAction; private RecyclerView libraryView; // Kept as reference for querying by Content through ID private Runnable onScrollToEndListener; @@ -86,6 +86,7 @@ private ContentAdapter(Builder builder) { collectionAccessor = builder.collectionAccessor; sortComparator = builder.sortComparator; displayMode = builder.displayMode; + openBookAction = builder.openBookAction; glideRequestOptions = new RequestOptions() .centerInside() .error(R.drawable.ic_placeholder); @@ -334,7 +335,7 @@ private void attachButtons(ContentHolder holder, final Content content) { } compositeDisposable.add( - Single.fromCallable(() -> toggleFavourite(content.getId())) + Single.fromCallable(() -> toggleFavourite(context, content.getId())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( @@ -380,7 +381,7 @@ else if (status == StatusContent.DOWNLOADING || status == StatusContent.PAUSED) // "In library" icon else if (status == StatusContent.DOWNLOADED || status == StatusContent.MIGRATED || status == StatusContent.ERROR) { holder.ivDownload.setImageResource(R.drawable.ic_action_play); - holder.ivDownload.setOnClickListener(v -> FileHelper.openContent(context, content)); + holder.ivDownload.setOnClickListener(v -> openBookAction.accept(content)); } } @@ -389,6 +390,7 @@ else if (status == StatusContent.DOWNLOADED || status == StatusContent.MIGRATED } } + // Mikan mode only private void tryDownloadPages(Content content) { ContentHolder holder = getHolderByContent(content); if (holder != null) { @@ -402,7 +404,7 @@ private void attachOnClickListeners(final ContentHolder holder, Content content, // Simple click = open book (library mode only) if (DownloadsFragment.MODE_LIBRARY == displayMode) { - holder.itemView.setOnClickListener(new ContentClickListener(context, content, pos, itemSelectListener) { + holder.itemView.setOnClickListener(new ContentClickListener(content, pos, itemSelectListener) { @Override public void onClick(View v) { @@ -416,13 +418,7 @@ public void onClick(View v) { } else { clearSelections(); setSelected(false, 0); - - super.onClick(v); - - if (sortComparator.equals(Content.READ_DATE_INV_COMPARATOR) - || sortComparator.equals(Content.READS_ORDER_COMPARATOR) - || sortComparator.equals(Content.READS_ORDER_INV_COMPARATOR)) - mSortedList.recalculatePositionOfItemAt(pos); // Reading the book has an effect on its position + openBookAction.accept(content); } } }); @@ -430,7 +426,7 @@ public void onClick(View v) { // Long click = select item (library mode only) if (DownloadsFragment.MODE_LIBRARY == displayMode) { - holder.itemView.setOnLongClickListener(new ContentClickListener(context, content, pos, itemSelectListener) { + holder.itemView.setOnLongClickListener(new ContentClickListener(content, pos, itemSelectListener) { @Override public boolean onLongClick(View v) { @@ -483,10 +479,9 @@ private void downloadAgain(final Content item) { private void downloadContent(Content item) { ObjectBoxDB db = ObjectBoxDB.getInstance(context); - if (StatusContent.ONLINE == item.getStatus()) - if (item.getImageFiles() != null) - for (ImageFile im : item.getImageFiles()) - db.updateImageFileStatusAndParams(im.setStatus(StatusContent.SAVED)); + if (StatusContent.ONLINE == item.getStatus() && item.getImageFiles() != null) + for (ImageFile im : item.getImageFiles()) + db.updateImageFileStatusAndParams(im.setStatus(StatusContent.SAVED)); item.setDownloadDate(new Date().getTime()); item.setStatus(StatusContent.DOWNLOADING); @@ -494,7 +489,7 @@ private void downloadContent(Content item) { List queue = db.selectQueue(); int lastIndex = 1; - if (queue.size() > 0) { + if (!queue.isEmpty()) { lastIndex = queue.get(queue.size() - 1).rank + 1; } db.insertQueue(item.getId(), lastIndex); @@ -514,7 +509,7 @@ private void showErrorLog(final Content content) { errorLogInfo.noDataMessage = "No error detected."; if (errorLog != null) { - log.add("Error log for " + content.getTitle() + " : " + errorLog.size() + " errors"); + log.add("Error log for " + content.getTitle() + " [" + content.getUniqueSiteId() + "@" + content.getSite().getDescription() + "] : " + errorLog.size() + " errors"); for (ErrorRecord e : errorLog) log.add(e.toString()); } @@ -572,7 +567,7 @@ private void askDeleteItems(final List items) { .create().show(); } - private Content toggleFavourite(long contentId) { + private static Content toggleFavourite(Context context, long contentId) { ObjectBoxDB db = ObjectBoxDB.getInstance(context); Content content = db.selectContentById(contentId); @@ -584,13 +579,8 @@ private Content toggleFavourite(long contentId) { db.insertContent(content); // Persist in it JSON - String rootFolderName = Preferences.getRootFolderName(); - File dir = new File(rootFolderName, content.getStorageFolder()); - try { - JsonHelper.saveJson(content.preJSONExport(), dir); - } catch (IOException e) { - Timber.e(e, "Error while writing to %s", dir.getAbsolutePath()); - } + if (!content.getJsonUri().isEmpty()) FileHelper.updateJson(context, content); + else FileHelper.createJson(content); } return content; } @@ -610,7 +600,7 @@ public void switchStateToDownloaded(Content content) { if (holder != null) { holder.ivDownload.setImageResource(R.drawable.ic_action_play); holder.ivDownload.clearAnimation(); - holder.ivDownload.setOnClickListener(v -> FileHelper.openContent(context, content)); + holder.ivDownload.setOnClickListener(v -> openBookAction.accept(content)); } } @@ -619,6 +609,12 @@ private ContentHolder getHolderByContent(Content content) { return (ContentHolder) libraryView.findViewHolderForItemId(content.getId()); } + public int getContentPosition(Content content) { + ContentHolder holder = getHolderByContent(content); + if (holder != null) return holder.getLayoutPosition(); + else return -1; + } + @Override public long getItemId(int position) { return mSortedList.get(position).getId(); @@ -808,10 +804,10 @@ public void addAll(List contents) { mSortedList.endBatchedUpdates(); } - // ContentListener implementation -- Mikan mode only + // PagedResultListener implementation -- Mikan mode only // Listener for pages retrieval (Mikan mode only) @Override - public void onContentReady(List results, long totalSelectedContent, long totalContent) { + public void onPagedResultReady(List results, long totalSelectedContent, long totalContent) { if (1 == results.size()) // 1 content with pages { downloadContent(results.get(0)); @@ -820,7 +816,7 @@ public void onContentReady(List results, long totalSelectedContent, lon // Listener for error visual feedback (Mikan mode only) @Override - public void onContentFailed(Content content, String message) { + public void onPagedResultFailed(Content content, String message) { Timber.w(message); Snackbar snackbar = Snackbar.make(libraryView, message, Snackbar.LENGTH_LONG); @@ -864,6 +860,7 @@ public static class Builder { private CollectionAccessor collectionAccessor; private Comparator sortComparator; private int displayMode; + private Consumer openBookAction; public Builder setContext(Context context) { this.context = context; @@ -895,6 +892,11 @@ public Builder setOnContentRemovedListener(IntConsumer onContentRemovedListener) return this; } + public Builder setOpenBookAction(Consumer action) { + this.openBookAction = action; + return this; + } + public ContentAdapter build() { return new ContentAdapter(this); } diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ContentHolder.java b/app/src/main/java/me/devsaki/hentoid/adapters/ContentHolder.java index 411ec985fb..99e376c25c 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ContentHolder.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ContentHolder.java @@ -1,6 +1,6 @@ package me.devsaki.hentoid.adapters; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView; import android.view.View; import android.widget.ImageView; import android.widget.TextView; diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImageGalleryAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/ImageGalleryAdapter.java new file mode 100644 index 0000000000..c1da1c1fab --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImageGalleryAdapter.java @@ -0,0 +1,32 @@ +package me.devsaki.hentoid.adapters; + +import androidx.annotation.Nullable; + +import com.annimon.stream.function.Consumer; + +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.IFlexible; +import me.devsaki.hentoid.database.domains.ImageFile; +import me.devsaki.hentoid.viewholders.ImageFileFlex; + +public class ImageGalleryAdapter extends FlexibleAdapter { + private final Consumer onFavouriteClickListener; + + public ImageGalleryAdapter(@Nullable List items, Consumer onFavouriteClickListener) { + super(items); + this.onFavouriteClickListener = onFavouriteClickListener; + } + + public Consumer getOnFavouriteClickListener() { + return onFavouriteClickListener; + } + + public boolean isFavouritePresent() { + for (IFlexible img : getCurrentItems()) + if (((ImageFileFlex)img).isFavourite()) return true; + + return false; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.java new file mode 100644 index 0000000000..d5e648e08b --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.java @@ -0,0 +1,165 @@ +package me.devsaki.hentoid.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.davemorrissey.labs.subscaleview.ImageSource; +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; + +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.database.domains.ImageFile; +import me.devsaki.hentoid.util.FileHelper; +import me.devsaki.hentoid.util.ImageLoaderThreadExecutor; +import me.devsaki.hentoid.util.Preferences; +import timber.log.Timber; + + +public final class ImagePagerAdapter extends RecyclerView.Adapter { + + private static final int TYPE_OTHER = 0; + private static final int TYPE_GIF = 1; + + private static final Executor executor = new ImageLoaderThreadExecutor(); + private final RequestOptions glideRequestOptions = new RequestOptions().centerInside(); + + private View.OnTouchListener itemTouchListener; + private RecyclerView recyclerView; + + private List images = new ArrayList<>(); + + + @Override + public int getItemCount() { + return images.size(); + } + + public void setImages(List images) { + this.images = Collections.unmodifiableList(images); + } + + public void setRecyclerView(RecyclerView v) { + recyclerView = v; + } + + public void setItemTouchListener(View.OnTouchListener itemTouchListener) { + this.itemTouchListener = itemTouchListener; + } + + public boolean isFavouritePresent() { + for (ImageFile img : images) + if (img.isFavourite()) return true; + + return false; + } + + @Override + public int getItemViewType(int position) { + if ("gif".equalsIgnoreCase(FileHelper.getExtension(images.get(position).getAbsolutePath()))) { + return TYPE_GIF; + } + return TYPE_OTHER; + } + + + @NonNull + @Override + public ImageViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); + View view; + if (TYPE_GIF == viewType) { + view = inflater.inflate(R.layout.item_viewer_image_glide, viewGroup, false); + } else if (Preferences.Constant.PREF_VIEWER_ORIENTATION_VERTICAL == Preferences.getViewerOrientation()) { + view = inflater.inflate(R.layout.item_viewer_image_subsampling_muted, viewGroup, false); + } else { + view = inflater.inflate(R.layout.item_viewer_image_subsampling, viewGroup, false); + } + return new ImageViewHolder(view, viewType); + } + + @Override + public void onBindViewHolder(@NonNull ImageViewHolder viewHolder, int pos) { + viewHolder.setImageUri(images.get(pos).getAbsolutePath()); + + int layoutStyle = (Preferences.Constant.PREF_VIEWER_ORIENTATION_VERTICAL == Preferences.getViewerOrientation()) ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT; + + ViewGroup.LayoutParams layoutParams = viewHolder.imgView.getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.height = layoutStyle; + viewHolder.imgView.setLayoutParams(layoutParams); + } + + @Nullable + public ImageFile getImageAt(int position) { + return (position >= 0 && position < images.size()) ? images.get(position) : null; + } + + public void resetPosition(int position) { + if (recyclerView != null) { + ImageViewHolder holder = (ImageViewHolder) recyclerView.findViewHolderForAdapterPosition(position); + if (holder != null) holder.resetScale(); + } + } + + final class ImageViewHolder extends RecyclerView.ViewHolder { + + private final int imgType; + private final View imgView; + + private ImageViewHolder(@NonNull View itemView, int imageType) { + super(itemView); + imgType = imageType; + imgView = itemView; + + if (TYPE_OTHER == imgType) { + ((SubsamplingScaleImageView) imgView).setExecutor(executor); + imgView.setOnTouchListener(itemTouchListener); + } + } + + void setImageUri(String uri) { + Timber.i(">>>>IMG %s %s", imgType, uri); + if (TYPE_GIF == imgType) { + ImageView view = (ImageView) imgView; + Glide.with(imgView.getContext().getApplicationContext()) + .load(uri) + .apply(glideRequestOptions) + .into(view); + + } else { + SubsamplingScaleImageView ssView = (SubsamplingScaleImageView) imgView; + ssView.recycle(); + ssView.setMinimumScaleType(getScaleType()); + ssView.setImage(ImageSource.uri(uri)); + } + } + + private int getScaleType() { + if (Preferences.Constant.PREF_VIEWER_DISPLAY_FILL == Preferences.getViewerResizeMode()) { + return SubsamplingScaleImageView.SCALE_TYPE_START; + } else { + return SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE; + } + } + + void resetScale() { + if (TYPE_GIF != imgType) { + SubsamplingScaleImageView ssView = (SubsamplingScaleImageView) imgView; + if (ssView.isImageLoaded() && ssView.isReady() && ssView.isLaidOut()) + ssView.resetScaleAndCenter(); + } + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImageRecyclerAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/ImageRecyclerAdapter.java deleted file mode 100644 index a58f72f092..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImageRecyclerAdapter.java +++ /dev/null @@ -1,93 +0,0 @@ -package me.devsaki.hentoid.adapters; - -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.davemorrissey.labs.subscaleview.ImageSource; -import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Executor; - -import me.devsaki.hentoid.R; -import me.devsaki.hentoid.util.ImageLoaderThreadExecutor; -import me.devsaki.hentoid.util.Preferences; -import timber.log.Timber; - - -public final class ImageRecyclerAdapter extends RecyclerView.Adapter { - - // TODO : SubsamplingScaleImageView does _not_ support animated GIFs -> use pl.droidsonroids.gif:android-gif-drawable when serving a GIF ? - - private static final Executor executor = new ImageLoaderThreadExecutor(); - - - private View.OnTouchListener itemTouchListener; - - private List imageUris; - - - @Override - public int getItemCount() { - return imageUris.size(); - } - - public void setImageUris(List imageUris) { - this.imageUris = Collections.unmodifiableList(imageUris); - } - - public void setItemTouchListener(View.OnTouchListener itemTouchListener) { - this.itemTouchListener = itemTouchListener; - } - - @NonNull - @Override - public ImageViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { - LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); - View view = inflater.inflate(R.layout.item_viewer_image, viewGroup, false); - return new ImageViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull ImageViewHolder viewHolder, int pos) { - viewHolder.setImageUri(imageUris.get(pos)); - - int layoutStyle = (Preferences.Constant.PREF_VIEWER_ORIENTATION_VERTICAL == Preferences.getViewerOrientation()) ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT; - - ViewGroup.LayoutParams layoutParams = viewHolder.imgView.getLayoutParams(); - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; - layoutParams.height = layoutStyle; - viewHolder.imgView.setLayoutParams(layoutParams); - } - - final class ImageViewHolder extends RecyclerView.ViewHolder { - - private final SubsamplingScaleImageView imgView; - - private ImageViewHolder(@NonNull View itemView) { - super(itemView); - imgView = (SubsamplingScaleImageView) itemView; - imgView.setExecutor(executor); - imgView.setOnTouchListener(itemTouchListener); - } - - void setImageUri(String uri) { - imgView.recycle(); - imgView.setMinimumScaleType(getScaleType()); -Timber.i(">>>>IMG %s", uri); - imgView.setImage(ImageSource.uri(uri)); - } - - private int getScaleType() { - if (Preferences.Constant.PREF_VIEWER_DISPLAY_FILL == Preferences.getViewerResizeMode()) { - return SubsamplingScaleImageView.SCALE_TYPE_START; - } else { - return SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE; - } - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/QueueContentAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/QueueContentAdapter.java index ec10c76654..3c800195ff 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/QueueContentAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/QueueContentAdapter.java @@ -1,17 +1,17 @@ package me.devsaki.hentoid.adapters; import android.content.Context; -import android.support.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.ImageView; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; + import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; @@ -124,13 +124,13 @@ private void attachTitle(ViewHolder holder, Content content) { */ private void attachCover(ViewHolder holder, Content content) { String coverFile = FileHelper.getThumb(content); - Glide.with(context).clear(holder.ivCover); + Glide.with(context.getApplicationContext()).clear(holder.ivCover); RequestOptions myOptions = new RequestOptions() .fitCenter() .error(R.drawable.ic_placeholder); - Glide.with(context) + Glide.with(context.getApplicationContext()) .load(coverFile) .apply(myOptions) .into(holder.ivCover); @@ -263,7 +263,7 @@ private void attachButtons(View view, final Content content, boolean isFirstItem btnDown.setVisibility(isLastItem ? View.INVISIBLE : View.VISIBLE); btnDown.setOnClickListener(v -> moveDown(content.getId())); - Button btnCancel = view.findViewById(R.id.btnCancel); + View btnCancel = view.findViewById(R.id.btnCancel); btnCancel.setOnClickListener(v -> cancel(content)); } @@ -440,10 +440,7 @@ private void cancel(Content content) { Completable.fromRunnable(() -> doCancel(content.getId())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> { - // Remove the content from the in-memory list and the UI - super.remove(content); - })); + .subscribe(() -> super.remove(content))); // Remove the content from the in-memory list and the UI } private void doCancel(long contentId) { diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/SelectedAttributeAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/SelectedAttributeAdapter.java index a7c8819b7b..d85b168522 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/SelectedAttributeAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/SelectedAttributeAdapter.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.adapters; -import android.support.annotation.NonNull; -import android.support.v7.recyclerview.extensions.ListAdapter; -import android.support.v7.util.DiffUtil; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.DiffUtil; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/SiteAdapter.java b/app/src/main/java/me/devsaki/hentoid/adapters/SiteAdapter.java index c65ac2e1c6..1fd5685f3a 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/SiteAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/adapters/SiteAdapter.java @@ -1,7 +1,7 @@ package me.devsaki.hentoid.adapters; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -26,7 +26,7 @@ public void setOnClickListener(View.OnClickListener listener) { @NonNull @Override public SiteAdapterViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_picker, parent, false); + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_text_sites, parent, false); return new SiteAdapterViewHolder(view); } @@ -56,7 +56,7 @@ static class SiteAdapterViewHolder extends RecyclerView.ViewHolder { private SiteAdapterViewHolder(View itemView) { super(itemView); - textView = itemView.findViewById(R.id.picker_item_name); + textView = itemView.findViewById(R.id.drawer_item_txt); } void bindTo(Site site) { diff --git a/app/src/main/java/me/devsaki/hentoid/collection/CollectionAccessor.java b/app/src/main/java/me/devsaki/hentoid/collection/CollectionAccessor.java index 684faa6730..25bb3e9c67 100644 --- a/app/src/main/java/me/devsaki/hentoid/collection/CollectionAccessor.java +++ b/app/src/main/java/me/devsaki/hentoid/collection/CollectionAccessor.java @@ -9,26 +9,36 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Language; import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ContentListener; +import me.devsaki.hentoid.listener.PagedResultListener; import me.devsaki.hentoid.listener.ResultListener; public interface CollectionAccessor { - void getRecentBooks(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener); + // BOOKS - void getPages(Content content, ContentListener listener); + void getRecentBooksPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); - void searchBooks(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener); + void getRecentBookIdsPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); - void countBooks(String query, List metadata, boolean favouritesOnly, ContentListener listener); + void getPages(Content content, PagedResultListener listener); - void searchBooksUniversal(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener); + void searchBooksPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); - void countBooksUniversal(String query, boolean favouritesOnly, ContentListener listener); + void searchBookIdsPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); + + void countBooks(String query, List metadata, boolean favouritesOnly, PagedResultListener listener); + + void searchBooksUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); + + void searchBookIdsUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener); + + void countBooksUniversal(String query, boolean favouritesOnly, PagedResultListener listener); + + // ATTRIBUTES void getAttributeMasterData(List types, String filter, int sortOrder, ResultListener> listener); - void getPagedAttributeMasterData(List types, String filter, int page, int booksPerPage, int orderStyle, ResultListener> listener); + void getAttributeMasterDataPaged(List types, String filter, int page, int booksPerPage, int orderStyle, ResultListener> listener); boolean supportsAvailabilityFilter(); @@ -36,7 +46,7 @@ public interface CollectionAccessor { void getAttributeMasterData(List types, String filter, List attrs, boolean filterFavourites, int sortOrder, ResultListener> listener); - void getPagedAttributeMasterData(List types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener> listener); + void getAttributeMasterDataPaged(List types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener> listener); void getAvailableAttributes(List types, List attrs, boolean filterFavourites, ResultListener> listener); diff --git a/app/src/main/java/me/devsaki/hentoid/collection/LibraryMatcher.java b/app/src/main/java/me/devsaki/hentoid/collection/LibraryMatcher.java index ac5faccbcc..d739b4163c 100644 --- a/app/src/main/java/me/devsaki/hentoid/collection/LibraryMatcher.java +++ b/app/src/main/java/me/devsaki/hentoid/collection/LibraryMatcher.java @@ -20,7 +20,7 @@ public class LibraryMatcher { public List matchContentToLibrary(List list) { - if (list != null && list.size() > 0) { + if (list != null && !list.isEmpty()) { Site site = list.get(0).getSite(); List uniqueIds = new ArrayList<>(); diff --git a/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanAttribute.java b/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanAttribute.java index cb3949ce7e..776ba1611d 100644 --- a/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanAttribute.java +++ b/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanAttribute.java @@ -24,24 +24,24 @@ public class MikanAttribute { public String type; Attribute toAttribute() { - AttributeType type; - switch (this.type) { + AttributeType attrType; + switch (type) { case "language": - type = AttributeType.LANGUAGE; + attrType = AttributeType.LANGUAGE; break; case "character": - type = AttributeType.CHARACTER; + attrType = AttributeType.CHARACTER; break; case "artist": - type = AttributeType.ARTIST; + attrType = AttributeType.ARTIST; break; case "group": - type = AttributeType.CIRCLE; + attrType = AttributeType.CIRCLE; break; default: - type = AttributeType.TAG; + attrType = AttributeType.TAG; } - Attribute result = new Attribute(type, name, url, Site.HITOMI); + Attribute result = new Attribute(attrType, name, url, Site.HITOMI); result.setCount(count); result.setExternalId(id); diff --git a/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanCollectionAccessor.java b/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanCollectionAccessor.java index dd348095ea..833afc9637 100644 --- a/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanCollectionAccessor.java +++ b/app/src/main/java/me/devsaki/hentoid/collection/mikan/MikanCollectionAccessor.java @@ -18,6 +18,8 @@ import java.util.Map; import io.reactivex.disposables.CompositeDisposable; +import me.devsaki.hentoid.HentoidApp; +import me.devsaki.hentoid.R; import me.devsaki.hentoid.collection.CollectionAccessor; import me.devsaki.hentoid.collection.LibraryMatcher; import me.devsaki.hentoid.database.domains.Attribute; @@ -25,9 +27,9 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Language; import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ContentListener; +import me.devsaki.hentoid.listener.PagedResultListener; import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.retrofit.MikanServer; +import me.devsaki.hentoid.retrofit.sources.MikanServer; import me.devsaki.hentoid.util.AttributeCache; import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.IllegalTags; @@ -41,7 +43,7 @@ public class MikanCollectionAccessor implements CollectionAccessor { private final LibraryMatcher libraryMatcher; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); // == CONSTRUCTOR @@ -110,7 +112,7 @@ private static String getEndpointPath(AttributeType attr) { // === ACCESSORS @Override - public void getRecentBooks(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { + public void getRecentBooksPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { boolean showMostRecentFirst = Preferences.Constant.ORDER_CONTENT_LAST_UL_DATE_FIRST == orderStyle; if (isSiteUnsupported(site)) { @@ -124,22 +126,31 @@ public void getRecentBooks(Site site, Language language, int page, int booksPerP compositeDisposable.add(MikanServer.API.getRecent(getMikanCodeForSite(site), params) .observeOn(mainThread()) - .subscribe((result) -> onContentSuccess(result, listener), (throwable) -> listener.onContentFailed(null, "Recent books failed to load - " + throwable.getMessage()))); + .subscribe(result -> onContentSuccess(result, listener), + throwable -> listener.onPagedResultFailed(null, "Recent books failed to load - " + throwable.getMessage()))); } @Override - public void getPages(Content content, ContentListener listener) { + public void getRecentBookIdsPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); + } + + @Override + public void getPages(Content content, PagedResultListener listener) { if (isSiteUnsupported(content.getSite())) { throw new UnsupportedOperationException("Site " + content.getSite().getDescription() + " not supported yet by Mikan search"); } compositeDisposable.add(MikanServer.API.getPages(getMikanCodeForSite(content.getSite()), content.getUniqueSiteId()) .observeOn(mainThread()) - .subscribe((result) -> onPagesSuccess(result, content, listener), (throwable) -> listener.onContentFailed(content, "Pages failed to load - " + throwable.getMessage()))); + .subscribe( + result -> onPagesSuccess(result, content, listener), + throwable -> listener.onPagedResultFailed(content, "Pages failed to load - " + throwable.getMessage())) + ); } @Override - public void searchBooks(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { + public void searchBooksPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { // NB : Mikan does not support booksPerPage and orderStyle params List sites = Helper.extractAttributeIdsByType(metadata, AttributeType.SOURCE); @@ -160,42 +171,55 @@ public void searchBooks(String query, List metadata, int page, int bo params.put("page", page + ""); List attributes = Helper.extractAttributeIdsByType(metadata, AttributeType.ARTIST); - if (attributes.size() > 0) params.put("artist", Helper.buildListAsString(attributes)); + if (!attributes.isEmpty()) params.put("artist", Helper.buildListAsString(attributes)); attributes = Helper.extractAttributeIdsByType(metadata, AttributeType.CIRCLE); - if (attributes.size() > 0) params.put("group", Helper.buildListAsString(attributes)); + if (!attributes.isEmpty()) params.put("group", Helper.buildListAsString(attributes)); attributes = Helper.extractAttributeIdsByType(metadata, AttributeType.CHARACTER); - if (attributes.size() > 0) params.put("character", Helper.buildListAsString(attributes)); + if (!attributes.isEmpty()) params.put("character", Helper.buildListAsString(attributes)); attributes = Helper.extractAttributeIdsByType(metadata, AttributeType.TAG); - if (attributes.size() > 0) params.put("tag", Helper.buildListAsString(attributes)); + if (!attributes.isEmpty()) params.put("tag", Helper.buildListAsString(attributes)); attributes = Helper.extractAttributeIdsByType(metadata, AttributeType.LANGUAGE); - if (attributes.size() > 0) params.put("language", Helper.buildListAsString(attributes)); + if (!attributes.isEmpty()) params.put("language", Helper.buildListAsString(attributes)); compositeDisposable.add(MikanServer.API.search(getMikanCodeForSite(site) + suffix, params) .observeOn(mainThread()) - .subscribe((result) -> onContentSuccess(result, listener), (throwable) -> listener.onContentFailed(null, "Search failed to load - " + throwable.getMessage()))); + .subscribe( + result -> onContentSuccess(result, listener), + throwable -> listener.onPagedResultFailed(null, "Search failed to load - " + throwable.getMessage())) + ); + } + + @Override + public void searchBookIdsPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); + } + + @Override + public void countBooks(String query, List metadata, boolean favouritesOnly, PagedResultListener listener) { + // Just counting is not possible with Mikan interface => call to searchBooksPaged anyway + searchBooksPaged(query, metadata, 1, 1, 1, favouritesOnly, listener); } @Override - public void countBooks(String query, List metadata, boolean favouritesOnly, ContentListener listener) { - // Just counting is not possible with Mikan interface => call to searchBooks anyway - searchBooks(query, metadata, 1, 1, 1, favouritesOnly, listener); + public void searchBooksUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + // Mikan does not allow "universal" search => call to searchBooksPaged with empty metadata + searchBooksPaged(query, Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly, listener); } @Override - public void searchBooksUniversal(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { - // Mikan does not allow "universal" search => call to searchBooks with empty metadata - searchBooks(query, Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly, listener); + public void searchBookIdsUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override - public void countBooksUniversal(String query, boolean favouritesOnly, ContentListener listener) { - // Just counting is not possible with Mikan interface => call to searchBooks anyway - searchBooks(query, Collections.emptyList(), 1, 1, 1, favouritesOnly, listener); + public void countBooksUniversal(String query, boolean favouritesOnly, PagedResultListener listener) { + // Just counting is not possible with Mikan interface => call to searchBooksPaged anyway + searchBooksPaged(query, Collections.emptyList(), 1, 1, 1, favouritesOnly, listener); } private void getAttributeMasterData(AttributeType type, String filter, int sortOrder, ResultListener> listener) { @@ -204,13 +228,12 @@ private void getAttributeMasterData(AttributeType type, String filter, int sortO List attributes = AttributeCache.getFromCache(type.name()); // If not cached (or cache expired), get it from network - if (null == attributes) { + if (attributes.isEmpty()) { String endpoint = getEndpointPath(type); compositeDisposable.add(MikanServer.API.getMasterData(endpoint) .observeOn(mainThread()) - .subscribe((result) -> { - onMasterDataSuccess(result, type.name(), filter, sortOrder, listener); // TODO handle caching in computing thread - }, (throwable) -> listener.onResultFailed("Attributes failed to load - " + throwable.getMessage()))); + .subscribe(result -> onMasterDataSuccess(result, type.name(), filter, sortOrder, listener), // TODO handle caching in computing thread + throwable -> listener.onResultFailed("Attributes failed to load - " + throwable.getMessage()))); } else { List result = filter(attributes, filter); listener.onResultReady(result, result.size()); @@ -224,8 +247,8 @@ public void getAttributeMasterData(List types, String filter, int } @Override - public void getPagedAttributeMasterData(List types, String filter, int page, int booksPerPage, int orderStyle, ResultListener> listener) { - throw new UnsupportedOperationException("Not implemented with Mikan"); + public void getAttributeMasterDataPaged(List types, String filter, int page, int booksPerPage, int orderStyle, ResultListener> listener) { + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override @@ -240,22 +263,22 @@ public boolean supportsAttributesPaging() { @Override public void getAttributeMasterData(List types, String filter, List attrs, boolean filterFavourites, int sortOrder, ResultListener> listener) { - throw new UnsupportedOperationException("Not implemented with Mikan"); + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override - public void getPagedAttributeMasterData(List types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener> listener) { - throw new UnsupportedOperationException("Not implemented with Mikan"); + public void getAttributeMasterDataPaged(List types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener> listener) { + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override public void getAvailableAttributes(List types, List attrs, boolean filterFavourites, ResultListener> listener) { - throw new UnsupportedOperationException("Not implemented with Mikan"); + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override public void countAttributesPerType(List filter, ResultListener listener) { - throw new UnsupportedOperationException("Not implemented with Mikan"); + throw new UnsupportedOperationException(HentoidApp.getAppContext().getString(R.string.mikan_unsupported)); } @Override @@ -266,28 +289,28 @@ public void dispose() { // === CALLBACKS - private void onContentSuccess(MikanContentResponse response, ContentListener listener) { + private void onContentSuccess(MikanContentResponse response, PagedResultListener listener) { if (null == response) { - listener.onContentFailed(null, "Content failed to load - Empty response"); + listener.onPagedResultFailed(null, "Content failed to load - Empty response"); return; } int maxItems = response.maxpage * response.result.size(); // Roughly : number of pages * number of books per page - listener.onContentReady(response.toContentList(libraryMatcher), maxItems, maxItems); + listener.onPagedResultReady(response.toContentList(libraryMatcher), maxItems, maxItems); } - private void onPagesSuccess(MikanContentResponse response, Content content, ContentListener listener) { + private void onPagesSuccess(MikanContentResponse response, Content content, PagedResultListener listener) { if (null == response) { - listener.onContentFailed(content, "Pages failed to load - Empty response"); + listener.onPagedResultFailed(content, "Pages failed to load - Empty response"); return; } if (null == content) - listener.onContentFailed(null, "Pages failed to load - Unexpected empty content"); + listener.onPagedResultFailed(null, "Pages failed to load - Unexpected empty content"); else { List list = Arrays.asList(content); content.addImageFiles(response.toImageFileList()).setQtyPages(response.pages.size()); - listener.onContentReady(list, 1, 1); + listener.onPagedResultReady(list, 1, 1); } } 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 372347619e..bceba882bb 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java +++ b/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java @@ -16,6 +16,7 @@ public class DatabaseMaintenance { * Clean up and upgrade database * NB : Heavy operations; must be performed in the background to avoid ANR at startup */ + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) public static void performDatabaseHousekeeping(Context context) { ObjectBoxDB db = ObjectBoxDB.getInstance(context); @@ -37,9 +38,20 @@ private static void performDatabaseCleanups(ObjectBoxDB db) { db.updateContentStatus(StatusContent.DOWNLOADING, StatusContent.PAUSED); Timber.i("Updating queue status : done"); + // Add back in the queue isolated DOWNLOADING or PAUSED books that aren't in the queue (since version code 106 / v1.8.0) + Timber.i("Moving back isolated items to queue : start"); + List contents = db.selectContentByStatus(StatusContent.PAUSED); + List queueContents = db.selectQueueContents(); + contents.removeAll(queueContents); + if (!contents.isEmpty()) { + int queueMaxPos = (int) db.selectMaxQueueOrder(); + for (Content c : contents) db.insertQueue(c.getId(), ++queueMaxPos); + } + Timber.i("Moving back isolated items to queue : done"); + // Clear temporary books created from browsing a book page without downloading it (since versionCode 60 / v1.3.7) Timber.i("Clearing temporary books : start"); - List contents = db.selectContentByStatus(StatusContent.SAVED); + contents = db.selectContentByStatus(StatusContent.SAVED); Timber.i("Clearing temporary books : %s books detected", contents.size()); for (Content c : contents) db.deleteContent(c); Timber.i("Clearing temporary books : done"); @@ -48,13 +60,12 @@ private static void performDatabaseCleanups(ObjectBoxDB db) { Timber.i("Upgrading Pururin image hosts : start"); contents = db.selectContentWithOldPururinHost(); Timber.i("Upgrading Pururin image hosts : %s books detected", contents.size()); - for (Content c : contents) - { - c.setCoverImageUrl(c.getCoverImageUrl().replace("api.pururin.io/images/","cdn.pururin.io/assets/images/data/")); - for (ImageFile i : c.getImageFiles()) - { - db.updateImageFileUrl( i.setUrl(i.getUrl().replace("api.pururin.io/images/","cdn.pururin.io/assets/images/data/")) ); - } + for (Content c : contents) { + c.setCoverImageUrl(c.getCoverImageUrl().replace("api.pururin.io/images/", "cdn.pururin.io/assets/images/data/")); + if (c.getImageFiles() != null) + for (ImageFile i : c.getImageFiles()) { + db.updateImageFileUrl(i.setUrl(i.getUrl().replace("api.pururin.io/images/", "cdn.pururin.io/assets/images/data/"))); + } db.insertContent(c); } Timber.i("Upgrading Pururin image hosts : done"); @@ -63,11 +74,11 @@ private static void performDatabaseCleanups(ObjectBoxDB db) { /** * Handles complex DB version updates at startup */ - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) public static void performOldDatabaseUpdate(HentoidDB db) { // Update all "storage_folder" fields in CONTENT table (mandatory) (since versionCode 44 / v1.2.2) List contents = db.selectContentEmptyFolder(); - if (contents != null && contents.size() > 0) { + if (contents != null && !contents.isEmpty()) { for (int i = 0; i < contents.size(); i++) { Content content = contents.get(i); content.setStorageFolder("/" + content.getSite().getDescription() + "/" + content.getOldUniqueSiteId()); // This line must use deprecated code, as it migrates it to newest version @@ -79,11 +90,11 @@ public static void performOldDatabaseUpdate(HentoidDB db) { // Gets books that should be in the queue but aren't List contentToMigrate = db.selectContentsForQueueMigration(); - if (contentToMigrate.size() > 0) { + if (!contentToMigrate.isEmpty()) { // Gets last index of the queue List> queue = db.selectQueue(); int lastIndex = 1; - if (queue.size() > 0) { + if (!queue.isEmpty()) { lastIndex = queue.get(queue.size() - 1).second + 1; } @@ -93,6 +104,7 @@ public static void performOldDatabaseUpdate(HentoidDB db) { } } + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) public static boolean hasToMigrate(Context context) { HentoidDB oldDb = HentoidDB.getInstance(context); return (oldDb.countContentEntries() > 0); diff --git a/app/src/main/java/me/devsaki/hentoid/database/HentoidDB.java b/app/src/main/java/me/devsaki/hentoid/database/HentoidDB.java index 06dc7a4eb7..0e9bc53b4e 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/HentoidDB.java +++ b/app/src/main/java/me/devsaki/hentoid/database/HentoidDB.java @@ -33,7 +33,11 @@ /** * Created by DevSaki on 10/05/2015. * db maintenance class + * + * @deprecated Replaced by {@link ObjectBoxDB}; class is kept for data migration purposes */ +@Deprecated +@SuppressWarnings("squid:S1192") // Putting SQL literals into constants would be too cumbersome public class HentoidDB extends SQLiteOpenHelper { private static final int DATABASE_VERSION = 8; diff --git a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxCollectionAccessor.java b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxCollectionAccessor.java index cd0007bcfd..6124a8031f 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxCollectionAccessor.java +++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxCollectionAccessor.java @@ -17,22 +17,29 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Language; import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.listener.ContentListener; +import me.devsaki.hentoid.listener.PagedResultListener; import me.devsaki.hentoid.listener.ResultListener; +import me.devsaki.hentoid.util.Helper; public class ObjectBoxCollectionAccessor implements CollectionAccessor { private final ObjectBoxDB db; private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private final int MODE_SEARCH_CONTENT_MODULAR = 0; - private final int MODE_COUNT_CONTENT_MODULAR = 1; - private final int MODE_SEARCH_CONTENT_UNIVERSAL = 2; - private final int MODE_COUNT_CONTENT_UNIVERSAL = 3; + private static final int MODE_SEARCH_CONTENT_MODULAR = 0; + private static final int MODE_COUNT_CONTENT_MODULAR = 1; + private static final int MODE_SEARCH_CONTENT_UNIVERSAL = 2; + private static final int MODE_COUNT_CONTENT_UNIVERSAL = 3; - private final int MODE_SEARCH_ATTRIBUTE_TEXT = 0; - private final int MODE_SEARCH_ATTRIBUTE_AVAILABLE = 1; - private final int MODE_SEARCH_ATTRIBUTE_COMBINED = 2; + private static final int MODE_SEARCH_ATTRIBUTE_TEXT = 0; + private static final int MODE_SEARCH_ATTRIBUTE_AVAILABLE = 1; + private static final int MODE_SEARCH_ATTRIBUTE_COMBINED = 2; + + static class ContentIdQueryResult { + long[] pagedContentIds; + long totalContent; + long totalSelectedContent; + } static class ContentQueryResult { List pagedContents; @@ -56,67 +63,106 @@ public ObjectBoxCollectionAccessor(Context ctx) { @Override - public void getRecentBooks(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { + public void getRecentBooksPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + compositeDisposable.add( + Single.fromCallable( + () -> pagedContentSearch(MODE_SEARCH_CONTENT_MODULAR, "", Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(contentQueryResult -> listener.onPagedResultReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + ); + } + + @Override + public void getRecentBookIdsPaged(Site site, Language language, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { compositeDisposable.add( Single.fromCallable( - () -> contentSearch(MODE_SEARCH_CONTENT_MODULAR, "", Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) + () -> pagedContentIdSearch(MODE_SEARCH_CONTENT_MODULAR, "", Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(contentQueryResult -> listener.onContentReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + .subscribe(contentIdQueryResult -> listener.onPagedResultReady( + Helper.getListFromPrimitiveArray(contentIdQueryResult.pagedContentIds), contentIdQueryResult.totalSelectedContent, contentIdQueryResult.totalContent)) ); } @Override - public void getPages(Content content, ContentListener listener) { + public void getPages(Content content, PagedResultListener listener) { throw new UnsupportedOperationException("Not implemented"); } @Override - public void searchBooks(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { + public void searchBooksPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { compositeDisposable.add( Single.fromCallable( - () -> contentSearch(MODE_SEARCH_CONTENT_MODULAR, query, metadata, page, booksPerPage, orderStyle, favouritesOnly) + () -> pagedContentSearch(MODE_SEARCH_CONTENT_MODULAR, query, metadata, page, booksPerPage, orderStyle, favouritesOnly) ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(contentQueryResult -> listener.onContentReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + .subscribe(contentQueryResult -> listener.onPagedResultReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) ); } @Override - public void countBooks(String query, List metadata, boolean favouritesOnly, ContentListener listener) { + public void searchBookIdsPaged(String query, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { compositeDisposable.add( Single.fromCallable( - () -> contentSearch(MODE_SEARCH_CONTENT_MODULAR, query, metadata, 1, 1, 1, favouritesOnly) + () -> pagedContentIdSearch(MODE_SEARCH_CONTENT_MODULAR, query, metadata, page, booksPerPage, orderStyle, favouritesOnly) ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(contentQueryResult -> listener.onContentReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + .subscribe(contentIdQueryResult -> listener.onPagedResultReady( + Helper.getListFromPrimitiveArray(contentIdQueryResult.pagedContentIds), contentIdQueryResult.totalSelectedContent, contentIdQueryResult.totalContent)) ); } @Override - public void searchBooksUniversal(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, ContentListener listener) { + public void countBooks(String query, List metadata, boolean favouritesOnly, PagedResultListener listener) { compositeDisposable.add( Single.fromCallable( - () -> contentSearch(MODE_SEARCH_CONTENT_UNIVERSAL, query, Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) + () -> pagedContentSearch(MODE_SEARCH_CONTENT_MODULAR, query, metadata, 1, 1, 1, favouritesOnly) ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(contentQueryResult -> listener.onContentReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + .subscribe(contentQueryResult -> listener.onPagedResultReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) ); } @Override - public void countBooksUniversal(String query, boolean favouritesOnly, ContentListener listener) { + public void searchBooksUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { compositeDisposable.add( Single.fromCallable( - () -> contentSearch(MODE_COUNT_CONTENT_UNIVERSAL, query, Collections.emptyList(), 1, 1, 1, favouritesOnly) + () -> pagedContentSearch(MODE_SEARCH_CONTENT_UNIVERSAL, query, Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(contentQueryResult -> listener.onContentReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + .subscribe(contentQueryResult -> listener.onPagedResultReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) + ); + } + + @Override + public void searchBookIdsUniversalPaged(String query, int page, int booksPerPage, int orderStyle, boolean favouritesOnly, PagedResultListener listener) { + compositeDisposable.add( + Single.fromCallable( + () -> pagedContentIdSearch(MODE_SEARCH_CONTENT_UNIVERSAL, query, Collections.emptyList(), page, booksPerPage, orderStyle, favouritesOnly) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(contentIdQueryResult -> listener.onPagedResultReady( + Helper.getListFromPrimitiveArray(contentIdQueryResult.pagedContentIds), contentIdQueryResult.totalSelectedContent, contentIdQueryResult.totalContent)) + ); + } + + @Override + public void countBooksUniversal(String query, boolean favouritesOnly, PagedResultListener listener) { + compositeDisposable.add( + Single.fromCallable( + () -> pagedContentSearch(MODE_COUNT_CONTENT_UNIVERSAL, query, Collections.emptyList(), 1, 1, 1, favouritesOnly) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(contentQueryResult -> listener.onPagedResultReady(contentQueryResult.pagedContents, contentQueryResult.totalSelectedContent, contentQueryResult.totalContent)) ); } @@ -133,7 +179,7 @@ public void getAttributeMasterData(List types, String filter, int } @Override - public void getPagedAttributeMasterData(List types, String filter, int page, int booksPerPage, int orderStyle, ResultListener> listener) { + public void getAttributeMasterDataPaged(List types, String filter, int page, int booksPerPage, int orderStyle, ResultListener> listener) { compositeDisposable.add( Single.fromCallable( () -> pagedAttributeSearch(MODE_SEARCH_ATTRIBUTE_TEXT, types, filter, Collections.emptyList(), false, orderStyle, page, booksPerPage) @@ -167,7 +213,7 @@ public void getAttributeMasterData(List types, String filter, Lis } @Override - public void getPagedAttributeMasterData(List types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener> listener) { + public void getAttributeMasterDataPaged(List types, String filter, List attrs, boolean filterFavourites, int page, int booksPerPage, int orderStyle, ResultListener> listener) { compositeDisposable.add( Single.fromCallable( () -> pagedAttributeSearch(MODE_SEARCH_ATTRIBUTE_COMBINED, types, filter, attrs, filterFavourites, orderStyle, page, booksPerPage) @@ -207,11 +253,11 @@ public void dispose() { compositeDisposable.clear(); } - private ContentQueryResult contentSearch(int mode, String filter, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly) { + private ContentQueryResult pagedContentSearch(int mode, String filter, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly) { // StringBuilder sb = new StringBuilder(); // for (Attribute a : metadata) sb.append(a.getName()).append(";"); -// timber.log.Timber.i("contentSearch mode=" + mode +",filter=" + filter +",meta=" + sb.toString() + ",p=" + page +",bpp=" + booksPerPage +",os=" + orderStyle +",fav=" + favouritesOnly); +// timber.log.Timber.i("pagedContentSearch mode=" + mode +",filter=" + filter +",meta=" + sb.toString() + ",p=" + page +",bpp=" + booksPerPage +",os=" + orderStyle +",fav=" + favouritesOnly); ContentQueryResult result = new ContentQueryResult(); @@ -235,7 +281,32 @@ private ContentQueryResult contentSearch(int mode, String filter, List metadata, int page, int booksPerPage, int orderStyle, boolean favouritesOnly) { + + ContentIdQueryResult result = new ContentIdQueryResult(); + + if (MODE_SEARCH_CONTENT_MODULAR == mode) { + result.pagedContentIds = db.selectContentSearchId(filter, page, booksPerPage, metadata, favouritesOnly, orderStyle); + } else if (MODE_SEARCH_CONTENT_UNIVERSAL == mode) { + result.pagedContentIds = db.selectContentUniversalId(filter, page, booksPerPage, favouritesOnly, orderStyle); + } else { + result.pagedContentIds = new long[0]; + } + // Fetch total query count (i.e. total number of books corresponding to the given filter, in all pages) + if (MODE_SEARCH_CONTENT_MODULAR == mode || MODE_COUNT_CONTENT_MODULAR == mode) { + result.totalSelectedContent = db.countContentSearch(filter, metadata, favouritesOnly); + } else if (MODE_SEARCH_CONTENT_UNIVERSAL == mode || MODE_COUNT_CONTENT_UNIVERSAL == mode) { + result.totalSelectedContent = db.countContentUniversal(filter, favouritesOnly); + } else { + result.totalSelectedContent = 0; + } + // Fetch total book count (i.e. total number of books in all the collection, regardless of filter) + result.totalContent = db.countAllContent(); return result; } 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 6530b0c228..565f642431 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java +++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java @@ -49,11 +49,11 @@ public class ObjectBoxDB { // TODO - put indexes - private final static int[] visibleContentStatus = new int[]{StatusContent.DOWNLOADED.getCode(), + private static final int[] visibleContentStatus = new int[]{StatusContent.DOWNLOADED.getCode(), StatusContent.ERROR.getCode(), StatusContent.MIGRATED.getCode()}; - private final static List visibleContentStatusAsList = Helper.getListFromPrimitiveArray(visibleContentStatus); + private static final List visibleContentStatusAsList = Helper.getListFromPrimitiveArray(visibleContentStatus); private static ObjectBoxDB instance; @@ -88,7 +88,8 @@ public long insertContent(Content content) { // Master data management managed manually // Ensure all known attributes are replaced by their ID before being inserted // Watch https://github.com/objectbox/objectbox-java/issues/509 for a lighter solution based on @Unique annotation - Attribute dbAttr, inputAttr; + Attribute dbAttr; + Attribute inputAttr; for (int i = 0; i < attributes.size(); i++) { inputAttr = attributes.get(i); dbAttr = (Attribute) attrByUniqueKey.setParameter(Attribute_.name, inputAttr.getName()) @@ -219,6 +220,10 @@ public List selectQueueContents() { return result; } + long selectMaxQueueOrder() { + return store.boxFor(QueueRecord.class).query().build().property(QueueRecord_.rank).max(); + } + public void insertQueue(long id, int order) { store.boxFor(QueueRecord.class).put(new QueueRecord(id, order)); } @@ -275,7 +280,7 @@ public Attribute selectAttributeById(long id) { private static long[] getIdsFromAttributes(@Nonnull List attrs) { long[] result = new long[attrs.size()]; - if (attrs.size() > 0) { + if (!attrs.isEmpty()) { int index = 0; for (Attribute a : attrs) result[index++] = a.getId(); } @@ -318,7 +323,9 @@ private Query queryContentSearchContent(String title, List m metadataMap.addAll(metadata); boolean hasTitleFilter = (title != null && title.length() > 0); - boolean hasSiteFilter = metadataMap.containsKey(AttributeType.SOURCE) && (metadataMap.get(AttributeType.SOURCE) != null) && (metadataMap.get(AttributeType.SOURCE).size() > 0); + boolean hasSiteFilter = metadataMap.containsKey(AttributeType.SOURCE) + && (metadataMap.get(AttributeType.SOURCE) != null) + && !(metadataMap.get(AttributeType.SOURCE).isEmpty()); boolean hasTagFilter = metadataMap.keySet().size() > (hasSiteFilter ? 1 : 0); QueryBuilder query = store.boxFor(Content.class).query(); @@ -332,7 +339,7 @@ private Query queryContentSearchContent(String title, List m for (AttributeType attrType : metadataMap.keySet()) { if (!attrType.equals(AttributeType.SOURCE)) { // Not a "real" attribute in database List attrs = metadataMap.get(attrType); - if (attrs.size() > 0) { + if (attrs != null && !attrs.isEmpty()) { query.in(Content_.id, getFilteredContent(attrs, false)); } } @@ -379,8 +386,10 @@ private static List shuffleRandomSort(Query query, int start, Collections.shuffle(order, new Random(RandomSeedSingleton.getInstance().getSeed())); int maxPage; - if (booksPerPage < 0) maxPage = order.size(); - else maxPage = Math.min(start + booksPerPage, order.size()); + if (booksPerPage < 0) { + start = 0; + maxPage = order.size(); + } else maxPage = Math.min(start + booksPerPage, order.size()); List result = new ArrayList<>(); for (int i = start; i < maxPage; i++) { @@ -389,19 +398,72 @@ private static List shuffleRandomSort(Query query, int start, return result; } + private static long[] shuffleRandomSortId(Query query, int start, int booksPerPage) { + LazyList lazyList = query.findLazy(); + List order = new ArrayList<>(); + for (int i = 0; i < lazyList.size(); i++) order.add(i); + Collections.shuffle(order, new Random(RandomSeedSingleton.getInstance().getSeed())); + + int maxPage; + if (booksPerPage < 0) { + start = 0; + maxPage = order.size(); + } else maxPage = Math.min(start + booksPerPage, order.size()); + + List result = new ArrayList<>(); + for (int i = start; i < maxPage; i++) { + result.add(lazyList.get(order.get(i)).getId()); + } + return Helper.getPrimitiveLongArrayFromList(result); + } + List selectContentSearch(String title, int page, int booksPerPage, List tags, boolean filterFavourites, int orderStyle) { + List result; int start = (page - 1) * booksPerPage; Query query = queryContentSearchContent(title, tags, filterFavourites, orderStyle); if (orderStyle != Preferences.Constant.ORDER_CONTENT_RANDOM) { - if (booksPerPage < 0) return query.find(); - else return query.find(start, booksPerPage); + if (booksPerPage < 0) result = query.find(); + else result = query.find(start, booksPerPage); } else { - return shuffleRandomSort(query, start, booksPerPage); + result = shuffleRandomSort(query, start, booksPerPage); } + return setQueryIndexes(result, page, booksPerPage); + } + + long[] selectContentSearchId(String title, int page, int booksPerPage, List tags, boolean filterFavourites, int orderStyle) { + long[] result; + int start = (page - 1) * booksPerPage; + Query query = queryContentSearchContent(title, tags, filterFavourites, orderStyle); + + if (orderStyle != Preferences.Constant.ORDER_CONTENT_RANDOM) { + if (booksPerPage < 0) result = query.findIds(); + else result = query.findIds(start, booksPerPage); + } else { + result = shuffleRandomSortId(query, start, booksPerPage); + } + return result; } List selectContentUniversal(String queryStr, int page, int booksPerPage, boolean filterFavourites, int orderStyle) { + List result; + int start = (page - 1) * booksPerPage; + // Due to objectBox limitations (see https://github.com/objectbox/objectbox-java/issues/497 and https://github.com/objectbox/objectbox-java/issues/533) + // querying Content and attributes have to be done separately + Query contentAttrSubQuery = queryContentUniversalAttributes(queryStr, filterFavourites); + Query query = queryContentUniversalContent(queryStr, filterFavourites, contentAttrSubQuery.findIds(), orderStyle); + + if (orderStyle != Preferences.Constant.ORDER_CONTENT_RANDOM) { + if (booksPerPage < 0) result = query.find(); + else result = query.find(start, booksPerPage); + } else { + result = shuffleRandomSort(query, start, booksPerPage); + } + return setQueryIndexes(result, page, booksPerPage); + } + + long[] selectContentUniversalId(String queryStr, int page, int booksPerPage, boolean filterFavourites, int orderStyle) { + long[] result; int start = (page - 1) * booksPerPage; // Due to objectBox limitations (see https://github.com/objectbox/objectbox-java/issues/497 and https://github.com/objectbox/objectbox-java/issues/533) // querying Content and attributes have to be done separately @@ -409,11 +471,18 @@ List selectContentUniversal(String queryStr, int page, int booksPerPage Query query = queryContentUniversalContent(queryStr, filterFavourites, contentAttrSubQuery.findIds(), orderStyle); if (orderStyle != Preferences.Constant.ORDER_CONTENT_RANDOM) { - if (booksPerPage < 0) return query.find(); - else return query.find(start, booksPerPage); + if (booksPerPage < 0) result = query.findIds(); + else result = query.findIds(start, booksPerPage); } else { - return shuffleRandomSort(query, start, booksPerPage); + result = shuffleRandomSortId(query, start, booksPerPage); } + return result; + } + + private List setQueryIndexes(List content, int page, int booksPerPage) { + for (int i = 0; i < content.size(); i++) + content.get(i).setQueryOrder((page - 1) * booksPerPage + i); + return content; } long countContentUniversal(String queryStr, boolean filterFavourites) { @@ -425,7 +494,7 @@ long countContentUniversal(String queryStr, boolean filterFavourites) { } private long[] getFilteredContent(List attrs, boolean filterFavourites) { - if (null == attrs || 0 == attrs.size()) return new long[0]; + if (null == attrs || attrs.isEmpty()) return new long[0]; // Pre-build queries to reuse them efficiently within the loops QueryBuilder contentFromSourceQueryBuilder = store.boxFor(Content.class).query(); @@ -486,7 +555,7 @@ List selectAvailableSources(List filter) { for (AttributeType attrType : metadataMap.keySet()) { if (!attrType.equals(AttributeType.SOURCE)) { // Not a "real" attribute in database List attrs = metadataMap.get(attrType); - if (attrs.size() > 0) { + if (attrs != null && !attrs.isEmpty()) { query.in(Content_.id, getFilteredContent(attrs, false)); } } @@ -510,8 +579,8 @@ List selectAvailableSources(List filter) { private Query queryAvailableAttributes(AttributeType type, String filter, List filteredContent) { QueryBuilder query = store.boxFor(Attribute.class).query(); - if (filteredContent.size() > 0) - query.filter((attr) -> (Stream.of(attr.contents).filter(c -> filteredContent.contains(c.getId())).filter(c -> visibleContentStatusAsList.contains(c.getStatus().getCode())).count() > 0)); + if (!filteredContent.isEmpty()) + query.filter(attr -> (Stream.of(attr.contents).filter(c -> filteredContent.contains(c.getId())).filter(c -> visibleContentStatusAsList.contains(c.getStatus().getCode())).count() > 0)); // query.link(Attribute_.contents).in(Content_.id, filteredContent).in(Content_.status, visibleContentStatus); <-- does not work for an obscure reason; need to reproduce that on a clean project and submit it to ObjectBox query.equal(Attribute_.type, type.getCode()); if (filter != null && !filter.trim().isEmpty()) @@ -525,7 +594,8 @@ long countAvailableAttributes(AttributeType type, List attributeFilte return queryAvailableAttributes(type, filter, filteredContent).count(); } - @SuppressWarnings("squid:S2184") // In our case, limit() argument has to be human-readable -> no issue concerning its type staying in the int range + @SuppressWarnings("squid:S2184") + // In our case, limit() argument has to be human-readable -> no issue concerning its type staying in the int range List selectAvailableAttributes(AttributeType type, List attributeFilter, String filter, boolean filterFavourites, int sortOrder, int page, int itemsPerPage) { long[] filteredContent = getFilteredContent(attributeFilter, filterFavourites); List filteredContentAsList = Helper.getListFromPrimitiveArray(filteredContent); @@ -649,4 +719,14 @@ public void deleteErrorRecords(long contentId) { List records = selectErrorRecordByContentId(contentId); store.boxFor(ErrorRecord.class).remove(records); } + + public void insertImageFile(ImageFile img) { + if (img.getId() > 0) store.boxFor(ImageFile.class).put(img); + } + + @Nullable + public ImageFile selectImageFile(long id) { + if (id > 0) return store.boxFor(ImageFile.class).get(id); + else return null; + } } diff --git a/app/src/main/java/me/devsaki/hentoid/database/constants/ImageFileTable.java b/app/src/main/java/me/devsaki/hentoid/database/constants/ImageFileTable.java index 37583cce22..420c14082c 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/constants/ImageFileTable.java +++ b/app/src/main/java/me/devsaki/hentoid/database/constants/ImageFileTable.java @@ -4,6 +4,7 @@ * Created by DevSaki on 10/05/2015. * db Image File Table */ +@SuppressWarnings("squid:S1192") // That's okay here public abstract class ImageFileTable { public static final String TABLE_NAME = "image_file"; diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/Attribute.java b/app/src/main/java/me/devsaki/hentoid/database/domains/Attribute.java index 43f020da61..1ffa5e7e2d 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/domains/Attribute.java +++ b/app/src/main/java/me/devsaki/hentoid/database/domains/Attribute.java @@ -1,5 +1,7 @@ package me.devsaki.hentoid.database.domains; +import androidx.annotation.NonNull; + import com.google.gson.annotations.Expose; import java.io.DataInputStream; @@ -27,7 +29,7 @@ @Entity public class Attribute { - private final static int ATTRIBUTE_FILE_VERSION = 1; + private static final int ATTRIBUTE_FILE_VERSION = 1; @Id private long id; @@ -163,6 +165,7 @@ public void addLocationsFrom(Attribute sourceAttribute) { } } + @NonNull @Override public String toString() { return getName(); @@ -186,9 +189,7 @@ public void saveToStream(DataOutputStream output) throws IOException { public static final Comparator NAME_COMPARATOR = (a, b) -> a.getName().compareTo(b.getName()); - public static final Comparator COUNT_COMPARATOR = (a, b) -> { - return Long.compare(a.getCount(), b.getCount()) * -1; /* Inverted - higher count first */ - }; + public static final Comparator COUNT_COMPARATOR = (a, b) -> Long.compare(a.getCount(), b.getCount()) * -1; /* Inverted - higher count first */ @Override public boolean equals(Object o) { 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 f734b8f298..7d99c6252f 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 @@ -1,6 +1,6 @@ package me.devsaki.hentoid.database.domains; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Objects; import io.objectbox.annotation.Backlink; import io.objectbox.annotation.Convert; @@ -16,21 +17,21 @@ import io.objectbox.annotation.Id; import io.objectbox.annotation.Transient; import io.objectbox.relation.ToMany; -import me.devsaki.hentoid.activities.websites.ASMHentaiActivity; -import me.devsaki.hentoid.activities.websites.BaseWebActivity; -import me.devsaki.hentoid.activities.websites.EHentaiActivity; -import me.devsaki.hentoid.activities.websites.FakkuActivity; -import me.devsaki.hentoid.activities.websites.HentaiCafeActivity; -import me.devsaki.hentoid.activities.websites.HitomiActivity; -import me.devsaki.hentoid.activities.websites.NhentaiActivity; -import me.devsaki.hentoid.activities.websites.PandaActivity; -import me.devsaki.hentoid.activities.websites.PururinActivity; -import me.devsaki.hentoid.activities.websites.TsuminoActivity; +import me.devsaki.hentoid.activities.sources.ASMHentaiActivity; +import me.devsaki.hentoid.activities.sources.BaseWebActivity; +import me.devsaki.hentoid.activities.sources.EHentaiActivity; +import me.devsaki.hentoid.activities.sources.FakkuActivity; +import me.devsaki.hentoid.activities.sources.HentaiCafeActivity; +import me.devsaki.hentoid.activities.sources.HitomiActivity; +import me.devsaki.hentoid.activities.sources.MusesActivity; +import me.devsaki.hentoid.activities.sources.NexusActivity; +import me.devsaki.hentoid.activities.sources.NhentaiActivity; +import me.devsaki.hentoid.activities.sources.PururinActivity; +import me.devsaki.hentoid.activities.sources.TsuminoActivity; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.util.AttributeMap; -import me.devsaki.hentoid.util.Preferences; /** * Created by DevSaki on 09/05/2015. @@ -75,6 +76,8 @@ public class Content implements Serializable { private long reads = 0; @Expose private long lastReadDate; + @Expose + private int lastReadPageIndex = 0; // Temporary during SAVED state only; no need to expose them for JSON persistence @Expose(serialize = false, deserialize = false) private String downloadParams; @@ -82,20 +85,28 @@ public class Content implements Serializable { @Expose(serialize = false, deserialize = false) @Backlink(to = "content") private ToMany errorLog; - @Expose(serialize = false, deserialize = false) - private int lastReadPageIndex = 0; + // Needs to be in the DB to keep the information when deletion takes a long time and user navigates + // No need to save that into JSON @Expose(serialize = false, deserialize = false) private boolean isBeingDeleted = false; + // Needs to be in the DB to optimize I/O + // No need to save that into the JSON file itself, obviously + @Expose(serialize = false, deserialize = false) + private String jsonUri; - // Runtime attributes; no need to expose them nor to persist them + // Runtime attributes; no need to expose them for JSON persistence nor to persist them to DB @Transient private double percent; @Transient private int queryOrder; @Transient + private boolean isFirst; + @Transient + private boolean isLast; + @Transient private boolean selected = false; - // Kept for retro-compatibility with contentV2.json Hentoid files + // Attributes kept for retro-compatibility with contentV2.json Hentoid files @Transient @Expose @SerializedName("attributes") @@ -150,7 +161,7 @@ private String computeUniqueSiteId() { switch (site) { case FAKKU: - return url.substring(url.lastIndexOf("/") + 1); + return url.substring(url.lastIndexOf('/') + 1); case EHENTAI: case PURURIN: paths = url.split("/"); @@ -164,12 +175,15 @@ private String computeUniqueSiteId() { case NHENTAI: case PANDA: case TSUMINO: + case NEXUS: return url.replace("/", ""); case HENTAICAFE: return url.replace("/?p=", ""); case FAKKU2: paths = url.split("/"); return paths[paths.length - 1]; + case MUSES: + return url.replace("/comics/album/", "").replace("/", "."); default: return ""; } @@ -181,7 +195,7 @@ public String getOldUniqueSiteId() { String[] paths; switch (site) { case FAKKU: - return url.substring(url.lastIndexOf("/") + 1); + return url.substring(url.lastIndexOf('/') + 1); case PURURIN: paths = url.split("/"); return paths[2].replace(".html", "") + "-" + paths[1]; @@ -224,22 +238,24 @@ public static Class getWebActivityClass(Site site) { return PururinActivity.class; case EHENTAI: return EHentaiActivity.class; - case PANDA: - return PandaActivity.class; case FAKKU2: return FakkuActivity.class; + case NEXUS: + return NexusActivity.class; + case MUSES: + return MusesActivity.class; default: - return BaseWebActivity.class; // Fallback for FAKKU + return BaseWebActivity.class; } } public String getCategory() { if (site == Site.FAKKU) { - return url.substring(1, url.lastIndexOf("/")); + return url.substring(1, url.lastIndexOf('/')); } else { if (attributes != null) { List attributesList = getAttributeMap().get(AttributeType.CATEGORY); - if (attributesList != null && attributesList.size() > 0) { + if (attributesList != null && !attributesList.isEmpty()) { return attributesList.get(0).getName(); } } @@ -279,6 +295,10 @@ public String getGalleryUrl() { case FAKKU2: galleryConst = "/hentai/"; break; + case NEXUS: + galleryConst = "/view"; + break; + case MUSES: case FAKKU: case HENTAICAFE: case PANDA: @@ -309,23 +329,27 @@ public String getReaderUrl() { return site.getUrl() + "/read/" + url.substring(1).replace("/", "/01/"); case FAKKU2: return getGalleryUrl() + "/read/page/1"; + case NEXUS: + return site.getUrl() + "/read" + url + "/001"; + case MUSES: + return site.getUrl().replace("album", "picture") + "/1"; default: return null; } } public Content populateAuthor() { - String author = ""; + String authorStr = ""; AttributeMap attrMap = getAttributeMap(); - if (attrMap.containsKey(AttributeType.ARTIST) && attrMap.get(AttributeType.ARTIST).size() > 0) - author = attrMap.get(AttributeType.ARTIST).get(0).getName(); - if (null == author || author.equals("")) // Try and get Circle - { - if (attrMap.containsKey(AttributeType.CIRCLE) && attrMap.get(AttributeType.CIRCLE).size() > 0) - author = attrMap.get(AttributeType.CIRCLE).get(0).getName(); - } - if (null == author) author = ""; - setAuthor(author); + if (attrMap.containsKey(AttributeType.ARTIST) && !attrMap.get(AttributeType.ARTIST).isEmpty()) + authorStr = attrMap.get(AttributeType.ARTIST).get(0).getName(); + if ((null == authorStr || authorStr.equals("")) + && attrMap.containsKey(AttributeType.CIRCLE) + && !attrMap.get(AttributeType.CIRCLE).isEmpty()) // Try and get Circle + authorStr = attrMap.get(AttributeType.CIRCLE).get(0).getName(); + + if (null == authorStr) authorStr = ""; + setAuthor(authorStr); return this; } @@ -483,6 +507,22 @@ public Content setQueryOrder(int order) { return this; } + public boolean isLast() { + return isLast; + } + + public void setLast(boolean last) { + this.isLast = last; + } + + public boolean isFirst() { + return isFirst; + } + + public void setFirst(boolean first) { + this.isFirst = first; + } + public boolean isSelected() { return selected; } @@ -491,7 +531,6 @@ public void setSelected(boolean selected) { this.selected = selected; } - public long getReads() { return reads; } @@ -540,15 +579,24 @@ public void setIsBeingDeleted(boolean isBeingDeleted) { this.isBeingDeleted = isBeingDeleted; } + public String getJsonUri() { + return (null == jsonUri) ? "" : jsonUri; + } + + public void setJsonUri(String jsonUri) { + this.jsonUri = jsonUri; + } + @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof Content)) { + return false; + } Content content = (Content) o; - return (url != null ? url.equals(content.url) : content.url == null) && site == content.site; + return this == o || (Objects.equals(content.url, url) && Objects.equals(content.site, site)); } @Override @@ -558,52 +606,9 @@ public int hashCode() { return result; } - public static Comparator getComparator(int compareMethod) { - switch (compareMethod) { - case Preferences.Constant.ORDER_CONTENT_TITLE_ALPHA: - return TITLE_ALPHA_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_LAST_DL_DATE_FIRST: - return DLDATE_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_TITLE_ALPHA_INVERTED: - return TITLE_ALPHA_INV_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_LAST_DL_DATE_LAST: - return DLDATE_INV_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_RANDOM: - return QUERY_ORDER_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_LAST_UL_DATE_FIRST: - return ULDATE_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_LEAST_READ: - return READS_ORDER_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_MOST_READ: - return READS_ORDER_INV_COMPARATOR; - case Preferences.Constant.ORDER_CONTENT_LAST_READ: - return READ_DATE_INV_COMPARATOR; - default: - return QUERY_ORDER_COMPARATOR; - } + public static Comparator getComparator() { + return QUERY_ORDER_COMPARATOR; } - private static final Comparator TITLE_ALPHA_COMPARATOR = (a, b) -> a.getTitle().compareTo(b.getTitle()); - - private static final Comparator DLDATE_COMPARATOR = (a, b) -> Long.compare(a.getDownloadDate(), b.getDownloadDate()) * -1; // Inverted - last download date first - - private static final Comparator ULDATE_COMPARATOR = (a, b) -> Long.compare(a.getUploadDate(), b.getUploadDate()) * -1; // Inverted - last upload date first - - private static final Comparator TITLE_ALPHA_INV_COMPARATOR = (a, b) -> a.getTitle().compareTo(b.getTitle()) * -1; - - private static final Comparator DLDATE_INV_COMPARATOR = (a, b) -> Long.compare(a.getDownloadDate(), b.getDownloadDate()); - - public static final Comparator READS_ORDER_COMPARATOR = (a, b) -> { - int comp = Long.compare(a.getReads(), b.getReads()); - return (0 == comp) ? Long.compare(a.getLastReadDate(), b.getLastReadDate()) : comp; - }; - - public static final Comparator READS_ORDER_INV_COMPARATOR = (a, b) -> { - int comp = Long.compare(a.getReads(), b.getReads()) * -1; - return (0 == comp) ? Long.compare(a.getLastReadDate(), b.getLastReadDate()) * -1 : comp; - }; - - public static final Comparator READ_DATE_INV_COMPARATOR = (a, b) -> Long.compare(a.getLastReadDate(), b.getLastReadDate()) * -1; - private static final Comparator QUERY_ORDER_COMPARATOR = (a, b) -> Integer.compare(a.getQueryOrder(), b.getQueryOrder()); } diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/ContentV1.java b/app/src/main/java/me/devsaki/hentoid/database/domains/ContentV1.java index b5214da66c..6527a1d70f 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/domains/ContentV1.java +++ b/app/src/main/java/me/devsaki/hentoid/database/domains/ContentV1.java @@ -4,7 +4,6 @@ import java.util.List; -import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.util.AttributeMap; @@ -12,6 +11,8 @@ /** * Created by DevSaki on 09/05/2015. * Content builder (legacy: kept to support older library) + * + * @deprecated Replaced by {@link Content}; class is kept for retrocompatibilty */ @Deprecated public class ContentV1 { diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/ErrorRecord.java b/app/src/main/java/me/devsaki/hentoid/database/domains/ErrorRecord.java index 71e17f276d..5fe43f219b 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/domains/ErrorRecord.java +++ b/app/src/main/java/me/devsaki/hentoid/database/domains/ErrorRecord.java @@ -1,5 +1,7 @@ package me.devsaki.hentoid.database.domains; +import androidx.annotation.NonNull; + import io.objectbox.annotation.Convert; import io.objectbox.annotation.Entity; import io.objectbox.annotation.Id; @@ -15,7 +17,7 @@ public class ErrorRecord { @Convert(converter = ErrorType.ErrorTypeConverter.class, dbType = Integer.class) public ErrorType type; public String url; - public String contentPart; + String contentPart; public String description; public ErrorRecord() { @@ -29,6 +31,7 @@ public ErrorRecord(long contentId, ErrorType type, String url, String contentPar this.description = description; } + @NonNull @Override public String toString() { return String.format("%s - [%s] : %s @ %s", contentPart, type.getName(), description, url); 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 b21a360fd3..c6667d96ed 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 @@ -7,6 +7,7 @@ import io.objectbox.annotation.Convert; import io.objectbox.annotation.Entity; import io.objectbox.annotation.Id; +import io.objectbox.annotation.Transient; import io.objectbox.relation.ToOne; import me.devsaki.hentoid.enums.StatusContent; @@ -26,11 +27,22 @@ public class ImageFile { @Expose private String name; @Expose + private boolean favourite = false; + @Expose @Convert(converter = StatusContent.StatusContentConverter.class, dbType = Integer.class) private StatusContent status; + public ToOne content; + // Temporary during SAVED state only; no need to expose them for JSON persistence + @Expose(serialize = false, deserialize = false) private String downloadParams; - public ToOne content; + + // Runtime attributes; no need to expose them nor to persist them + @Transient + private int displayOrder; + @Transient + private String absolutePath; + public ImageFile() { } @@ -40,14 +52,9 @@ public ImageFile(int order, String url, StatusContent status) { this.name = String.format(Locale.US, "%03d", order); this.url = url; this.status = status; + this.favourite = false; } -/* - public Integer getId() { - return url.hashCode(); - } -*/ - public long getId() { return this.id; } @@ -100,4 +107,28 @@ public ImageFile setDownloadParams(String params) { downloadParams = params; return this; } + + public boolean isFavourite() { + return favourite; + } + + public void setFavourite(boolean favourite) { + this.favourite = favourite; + } + + public String getAbsolutePath() { + return absolutePath; + } + + public void setAbsolutePath(String absolutePath) { + this.absolutePath = absolutePath; + } + + public int getDisplayOrder() { + return displayOrder; + } + + public void setDisplayOrder(int displayOrder) { + this.displayOrder = displayOrder; + } } diff --git a/app/src/main/java/me/devsaki/hentoid/dirpicker/adapter/DirAdapter.java b/app/src/main/java/me/devsaki/hentoid/dirpicker/adapter/DirAdapter.java index bf96998d68..14976ca7f3 100644 --- a/app/src/main/java/me/devsaki/hentoid/dirpicker/adapter/DirAdapter.java +++ b/app/src/main/java/me/devsaki/hentoid/dirpicker/adapter/DirAdapter.java @@ -1,7 +1,7 @@ package me.devsaki.hentoid.dirpicker.adapter; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -30,7 +30,7 @@ public DirAdapter(DirList dirList) { @Override public DirAdapterViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View root = LayoutInflater.from( - parent.getContext()).inflate(R.layout.item_picker, parent, false); + parent.getContext()).inflate(R.layout.item_text, parent, false); return new DirAdapterViewHolder(root); } @@ -51,7 +51,7 @@ class DirAdapterViewHolder extends RecyclerView.ViewHolder { DirAdapterViewHolder(View root) { super(root); - textView = root.findViewById(R.id.picker_item_name); + textView = root.findViewById(R.id.drawer_item_txt); textView.setOnClickListener(this::onClick); } diff --git a/app/src/main/java/me/devsaki/hentoid/dirpicker/events/CurrentRootDirChangedEvent.java b/app/src/main/java/me/devsaki/hentoid/dirpicker/events/CurrentRootDirChangedEvent.java index 17f74ed606..bf5418e3cc 100644 --- a/app/src/main/java/me/devsaki/hentoid/dirpicker/events/CurrentRootDirChangedEvent.java +++ b/app/src/main/java/me/devsaki/hentoid/dirpicker/events/CurrentRootDirChangedEvent.java @@ -1,6 +1,7 @@ package me.devsaki.hentoid.dirpicker.events; import java.io.File; +import java.util.Objects; /** * Created by avluis on 06/11/2016. @@ -19,17 +20,13 @@ public File getCurrentDirectory() { @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof CurrentRootDirChangedEvent)) { return false; } CurrentRootDirChangedEvent event = (CurrentRootDirChangedEvent) o; - return currentDir != null ? currentDir.equals(event.currentDir) : event.currentDir == null; + return this == o || Objects.equals(event.currentDir, currentDir); } @Override diff --git a/app/src/main/java/me/devsaki/hentoid/dirpicker/events/UpdateDirTreeEvent.java b/app/src/main/java/me/devsaki/hentoid/dirpicker/events/UpdateDirTreeEvent.java index a5301913dd..6cc5c93acc 100644 --- a/app/src/main/java/me/devsaki/hentoid/dirpicker/events/UpdateDirTreeEvent.java +++ b/app/src/main/java/me/devsaki/hentoid/dirpicker/events/UpdateDirTreeEvent.java @@ -1,6 +1,7 @@ package me.devsaki.hentoid.dirpicker.events; import java.io.File; +import java.util.Objects; /** * Created by avluis on 06/11/2016. @@ -15,17 +16,13 @@ public UpdateDirTreeEvent(File rootDir) { @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof UpdateDirTreeEvent)) { return false; } UpdateDirTreeEvent event = (UpdateDirTreeEvent) o; - return rootDir != null ? rootDir.equals(event.rootDir) : event.rootDir == null; + return this == o || Objects.equals(event.rootDir, rootDir); } @Override diff --git a/app/src/main/java/me/devsaki/hentoid/dirpicker/model/FileBuilder.java b/app/src/main/java/me/devsaki/hentoid/dirpicker/model/FileBuilder.java index e076292d6e..b5747efbc7 100644 --- a/app/src/main/java/me/devsaki/hentoid/dirpicker/model/FileBuilder.java +++ b/app/src/main/java/me/devsaki/hentoid/dirpicker/model/FileBuilder.java @@ -1,8 +1,9 @@ package me.devsaki.hentoid.dirpicker.model; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import java.io.File; +import java.util.Objects; /** * Created by avluis on 06/12/2016. @@ -15,6 +16,24 @@ public FileBuilder(String path) { super(path); } + @Override + public boolean equals(Object o) { + if (!(o instanceof FileBuilder)) { + return false; + } + + FileBuilder fileBuilder = (FileBuilder) o; + + return this == o || Objects.equals(fileBuilder.getName(), name); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } + @NonNull @Override public String getName() { diff --git a/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/DirListBuilder.java b/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/DirListBuilder.java index 65f450dc62..93cada25fb 100644 --- a/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/DirListBuilder.java +++ b/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/DirListBuilder.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.dirpicker.ops; import android.content.Context; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import java.io.File; diff --git a/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/ListDir.java b/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/ListDir.java index 0087f2dcc2..a80fa1cd5a 100644 --- a/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/ListDir.java +++ b/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/ListDir.java @@ -21,7 +21,7 @@ class ListDir { private final DirTree dirTree; - protected final CompositeDisposable compositeDisposable = new CompositeDisposable(); + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); ListDir(DirTree dirTree) { this.dirTree = dirTree; diff --git a/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/MakeDir.java b/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/MakeDir.java index 6e6cea5f1b..351a336d85 100644 --- a/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/MakeDir.java +++ b/app/src/main/java/me/devsaki/hentoid/dirpicker/ops/MakeDir.java @@ -6,7 +6,6 @@ import me.devsaki.hentoid.dirpicker.exceptions.DirExistsException; import me.devsaki.hentoid.dirpicker.exceptions.PermissionDeniedException; import me.devsaki.hentoid.util.FileHelper; -import timber.log.Timber; /** * Created by avluis on 06/12/2016. @@ -14,7 +13,7 @@ */ public class MakeDir { - public static void TryMakeDir(File rootDir, String dirName) throws IOException { + public static void tryMakeDir(File rootDir, String dirName) throws IOException { if (!rootDir.canWrite()) { throw new PermissionDeniedException(); } @@ -23,12 +22,8 @@ public static void TryMakeDir(File rootDir, String dirName) throws IOException { if (newDir.exists()) { throw new DirExistsException(); } else { - boolean isDirCreated = FileHelper.createDirectory(newDir); - if (isDirCreated) { - return; - } else { + if (!FileHelper.createDirectory(newDir)) throw new IOException(); - } } } } diff --git a/app/src/main/java/me/devsaki/hentoid/dirpicker/ui/CreateDirDialog.java b/app/src/main/java/me/devsaki/hentoid/dirpicker/ui/CreateDirDialog.java index bb1ae6d80f..12dd65cfa5 100644 --- a/app/src/main/java/me/devsaki/hentoid/dirpicker/ui/CreateDirDialog.java +++ b/app/src/main/java/me/devsaki/hentoid/dirpicker/ui/CreateDirDialog.java @@ -1,11 +1,12 @@ package me.devsaki.hentoid.dirpicker.ui; import android.content.Context; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; import android.text.Editable; import android.widget.EditText; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + import org.greenrobot.eventbus.EventBus; import java.io.File; diff --git a/app/src/main/java/me/devsaki/hentoid/dirpicker/ui/DirChooserFragment.java b/app/src/main/java/me/devsaki/hentoid/dirpicker/ui/DirChooserFragment.java index 239fdd5f32..68d5705b6f 100644 --- a/app/src/main/java/me/devsaki/hentoid/dirpicker/ui/DirChooserFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/dirpicker/ui/DirChooserFragment.java @@ -4,17 +4,18 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.app.DialogFragment; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -51,9 +52,9 @@ public class DirChooserFragment extends DialogFragment { private RecyclerView recyclerView; private TextView textView; - private FloatingActionButton fabCreateDir, - fabRequestSD; - private Button selectDirBtn; + private FloatingActionButton fabCreateDir; + private FloatingActionButton fabRequestSD; + private View selectDirBtn; private File currentRootDir; private DirListBuilder dirListBuilder; @@ -91,7 +92,7 @@ public void onStop() { } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { outState.putSerializable(CURRENT_ROOT_DIR, currentRootDir); super.onSaveInstanceState(outState); } @@ -121,9 +122,9 @@ private void initUI(View rootView) { fabCreateDir.setOnClickListener(this::onClick); selectDirBtn.setOnClickListener(this::onClick); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && FileHelper.isSDPresent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && FileHelper.isSdPresent) { fabRequestSD.setOnClickListener(this::onClick); - fabRequestSD.setVisibility(View.VISIBLE); + fabRequestSD.show(); } } @@ -147,7 +148,7 @@ public void onUpdateDirTreeEvent(UpdateDirTreeEvent event) { @Subscribe public void onMakeDirEvent(OnMakeDirEvent event) { try { - MakeDir.TryMakeDir(event.root, event.dirName); + MakeDir.tryMakeDir(event.root, event.dirName); } catch (DirExistsException dee) { ToastUtil.toast(R.string.folder_already_exists); } catch (PermissionDeniedException dee) { @@ -185,12 +186,12 @@ private void setCurrentDir() { } @Override - public void onCancel(DialogInterface dialog) { + public void onCancel(@NonNull DialogInterface dialog) { EventBus.getDefault().post(new OnDirCancelEvent()); super.onCancel(dialog); } - public void onClick(View v) { + private void onClick(View v) { if (v.equals(textView)) { onTextViewClicked(false); } else if (v.equals(fabCreateDir)) { @@ -202,7 +203,7 @@ public void onClick(View v) { } } - public boolean onLongClick(View v) { + private boolean onLongClick(View v) { if (v.equals(textView)) { onTextViewClicked(true); return true; diff --git a/app/src/main/java/me/devsaki/hentoid/enums/AttributeType.java b/app/src/main/java/me/devsaki/hentoid/enums/AttributeType.java index 35c1ceb1fe..2f949a5991 100644 --- a/app/src/main/java/me/devsaki/hentoid/enums/AttributeType.java +++ b/app/src/main/java/me/devsaki/hentoid/enums/AttributeType.java @@ -2,7 +2,6 @@ import javax.annotation.Nullable; -import io.objectbox.annotation.Entity; import io.objectbox.converter.PropertyConverter; import me.devsaki.hentoid.R; diff --git a/app/src/main/java/me/devsaki/hentoid/enums/DrawerItem.java b/app/src/main/java/me/devsaki/hentoid/enums/DrawerItem.java index 869bac75b3..8f979e60ea 100644 --- a/app/src/main/java/me/devsaki/hentoid/enums/DrawerItem.java +++ b/app/src/main/java/me/devsaki/hentoid/enums/DrawerItem.java @@ -1,19 +1,21 @@ package me.devsaki.hentoid.enums; +import androidx.appcompat.app.AppCompatActivity; + import me.devsaki.hentoid.R; import me.devsaki.hentoid.activities.AboutActivity; -import me.devsaki.hentoid.activities.DownloadsActivity; import me.devsaki.hentoid.activities.PrefsActivity; import me.devsaki.hentoid.activities.QueueActivity; -import me.devsaki.hentoid.activities.websites.ASMHentaiActivity; -import me.devsaki.hentoid.activities.websites.EHentaiActivity; -import me.devsaki.hentoid.activities.websites.FakkuActivity; -import me.devsaki.hentoid.activities.websites.HentaiCafeActivity; -import me.devsaki.hentoid.activities.websites.HitomiActivity; -import me.devsaki.hentoid.activities.websites.NhentaiActivity; -import me.devsaki.hentoid.activities.websites.PandaActivity; -import me.devsaki.hentoid.activities.websites.PururinActivity; -import me.devsaki.hentoid.activities.websites.TsuminoActivity; +import me.devsaki.hentoid.activities.sources.ASMHentaiActivity; +import me.devsaki.hentoid.activities.sources.EHentaiActivity; +import me.devsaki.hentoid.activities.sources.FakkuActivity; +import me.devsaki.hentoid.activities.sources.HentaiCafeActivity; +import me.devsaki.hentoid.activities.sources.HitomiActivity; +import me.devsaki.hentoid.activities.sources.MusesActivity; +import me.devsaki.hentoid.activities.sources.NexusActivity; +import me.devsaki.hentoid.activities.sources.NhentaiActivity; +import me.devsaki.hentoid.activities.sources.PururinActivity; +import me.devsaki.hentoid.activities.sources.TsuminoActivity; public enum DrawerItem { @@ -23,20 +25,21 @@ public enum DrawerItem { ASM("ASMHENTAI", R.drawable.ic_menu_asmhentai, ASMHentaiActivity.class), TSUMINO("TSUMINO", R.drawable.ic_menu_tsumino, TsuminoActivity.class), PURURIN("PURURIN", R.drawable.ic_menu_pururin, PururinActivity.class), - PANDA("PANDA", R.drawable.ic_menu_panda, PandaActivity.class), EHENTAI("E-HENTAI", R.drawable.ic_menu_ehentai, EHentaiActivity.class), FAKKU("FAKKU", R.drawable.ic_menu_fakku, FakkuActivity.class), + NEXUS("HENTAI NEXUS", R.drawable.ic_menu_nexus, NexusActivity.class), + MUSES("8MUSES", R.drawable.ic_menu_8muses, MusesActivity.class), // MIKAN("MIKAN SEARCH", R.drawable.ic_menu_mikan, MikanSearchActivity.class), - HOME("HOME", R.drawable.ic_menu_downloads, DownloadsActivity.class), + // HOME("HOME", R.drawable.ic_menu_home, DownloadsActivity.class), QUEUE("QUEUE", R.drawable.ic_menu_queue, QueueActivity.class), PREFS("PREFERENCES", R.drawable.ic_menu_prefs, PrefsActivity.class), ABOUT("ABOUT", R.drawable.ic_menu_about, AboutActivity.class); public final String label; public final int icon; - public final Class activityClass; + public final Class activityClass; - DrawerItem(String label, int icon, Class activityClass) { + DrawerItem(String label, int icon, Class activityClass) { this.label = label; this.icon = icon; this.activityClass = activityClass; diff --git a/app/src/main/java/me/devsaki/hentoid/enums/ErrorType.java b/app/src/main/java/me/devsaki/hentoid/enums/ErrorType.java index 1602ec4c8e..612ee80007 100644 --- a/app/src/main/java/me/devsaki/hentoid/enums/ErrorType.java +++ b/app/src/main/java/me/devsaki/hentoid/enums/ErrorType.java @@ -1,9 +1,6 @@ package me.devsaki.hentoid.enums; -import javax.annotation.Nullable; - import io.objectbox.converter.PropertyConverter; -import timber.log.Timber; public enum ErrorType { @@ -30,7 +27,7 @@ public static ErrorType searchByCode(int code) { return UNDEFINED; } - public int getCode() { + private int getCode() { return code; } diff --git a/app/src/main/java/me/devsaki/hentoid/enums/Site.java b/app/src/main/java/me/devsaki/hentoid/enums/Site.java index 97f5f434a7..860ca58a22 100644 --- a/app/src/main/java/me/devsaki/hentoid/enums/Site.java +++ b/app/src/main/java/me/devsaki/hentoid/enums/Site.java @@ -1,5 +1,7 @@ package me.devsaki.hentoid.enums; +import java.io.File; + import io.objectbox.converter.PropertyConverter; import me.devsaki.hentoid.R; import timber.log.Timber; @@ -22,6 +24,8 @@ public enum Site { ASMHENTAI_COMICS(7, "asmhentai comics", "https://comics.asmhentai.com", "comics.asmhentai", R.drawable.ic_menu_asmcomics, true, true, false), EHENTAI(8, "e-hentai", "https://e-hentai.org", "e-hentai", R.drawable.ic_menu_ehentai, true, true, false), FAKKU2(9, "Fakku", "https://www.fakku.net", "fakku2", R.drawable.ic_menu_fakku, true, false, true), + NEXUS(10, "Hentai Nexus", "https://hentainexus.com", "nexus", R.drawable.ic_menu_nexus, true, false, false), + MUSES(11, "8Muses", "https://www.8muses.com", "8muses", R.drawable.ic_menu_8muses, true, false, false), NONE(98, "none", "", "none", R.drawable.ic_menu_about, true, true, false), // Fallback site PANDA(99, "panda", "https://www.mangapanda.com", "mangapanda", R.drawable.ic_menu_panda, true, true, false); // Safe-for-work/wife/gf option @@ -84,7 +88,7 @@ public String getDescription() { return description; } - public String getUniqueKeyword() { + private String getUniqueKeyword() { return uniqueKeyword; } @@ -110,9 +114,9 @@ public boolean hasImageProcessing() { public String getFolder() { if (this == FAKKU) { - return "/Downloads/"; + return File.separator + "Downloads" + File.separator; } else { - return '/' + description + '/'; + return File.separator + description + File.separator; } } diff --git a/app/src/main/java/me/devsaki/hentoid/events/DownloadPreparationEvent.java b/app/src/main/java/me/devsaki/hentoid/events/DownloadPreparationEvent.java new file mode 100644 index 0000000000..5e07e0ba76 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/events/DownloadPreparationEvent.java @@ -0,0 +1,20 @@ +package me.devsaki.hentoid.events; + +/** + * Created by Robb on 2019/06/19 + * Tracks download preparation events for interested subscribers. + */ +public class DownloadPreparationEvent { + public final int done; // Number of steps done + public final int total; // Total number of steps to do + + public DownloadPreparationEvent(int done, int total) { + this.done = done; + this.total = total; + } + + public boolean isCompleted() + { + return (done == total); + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/events/ImportEvent.java b/app/src/main/java/me/devsaki/hentoid/events/ImportEvent.java index f9aa6e7034..f9592dfec7 100644 --- a/app/src/main/java/me/devsaki/hentoid/events/ImportEvent.java +++ b/app/src/main/java/me/devsaki/hentoid/events/ImportEvent.java @@ -13,7 +13,7 @@ public class ImportEvent { public static final int EV_COMPLETE = 1; // Import complete public final int eventType; // Event type (see constants EV_XXX above) - public final Content content; // Corresponding book (for EV_PROGRESS) + private final Content content; // Corresponding book (for EV_PROGRESS) public final int booksOK; // Number of pages that have been downloaded successfully for current book public final int booksKO; // Number of pages that have been downloaded with errors for current book public final int booksTotal; // Number of pages to download for current book diff --git a/app/src/main/java/me/devsaki/hentoid/events/UpdateEvent.java b/app/src/main/java/me/devsaki/hentoid/events/UpdateEvent.java new file mode 100644 index 0000000000..2fffd5106c --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/events/UpdateEvent.java @@ -0,0 +1,10 @@ +package me.devsaki.hentoid.events; + +public class UpdateEvent { + public final boolean hasNewVersion; + + public UpdateEvent(boolean hasNewVersion) + { + this.hasNewVersion = hasNewVersion; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/LibRefreshDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/LibRefreshDialogFragment.java index 60bd5d9f91..0c6db51501 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/LibRefreshDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/LibRefreshDialogFragment.java @@ -2,21 +2,21 @@ import android.content.Intent; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.FragmentManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; import android.widget.CheckBox; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; + import me.devsaki.hentoid.R; import me.devsaki.hentoid.activities.ImportActivity; import me.devsaki.hentoid.activities.bundles.ImportActivityBundle; -import static android.support.v4.view.ViewCompat.requireViewById; +import static androidx.core.view.ViewCompat.requireViewById; /** * Created by Robb on 11/2018 @@ -24,14 +24,9 @@ */ public class LibRefreshDialogFragment extends DialogFragment { - private CheckBox renameChk; - private CheckBox cleanAbsentChk; - private CheckBox cleanNoImagesChk; - private CheckBox cleanUnreadableChk; - public static void invoke(FragmentManager fragmentManager) { LibRefreshDialogFragment fragment = new LibRefreshDialogFragment(); - fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.PrefsThemeDialog); + fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.Dialog); fragment.show(fragmentManager, null); } @@ -45,12 +40,12 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - renameChk = requireViewById(view, R.id.refresh_options_rename); - cleanAbsentChk = requireViewById(view, R.id.refresh_options_remove_1); - cleanNoImagesChk = requireViewById(view, R.id.refresh_options_remove_2); - cleanUnreadableChk = requireViewById(view, R.id.refresh_options_remove_3); + CheckBox renameChk = requireViewById(view, R.id.refresh_options_rename); + CheckBox cleanAbsentChk = requireViewById(view, R.id.refresh_options_remove_1); + CheckBox cleanNoImagesChk = requireViewById(view, R.id.refresh_options_remove_2); + CheckBox cleanUnreadableChk = requireViewById(view, R.id.refresh_options_remove_3); - Button okBtn = requireViewById(view, R.id.refresh_ok); + View okBtn = requireViewById(view, R.id.refresh_ok); okBtn.setOnClickListener(v -> launchRefreshImport(renameChk.isChecked(), cleanAbsentChk.isChecked(), cleanNoImagesChk.isChecked(), cleanUnreadableChk.isChecked())); } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/QueueFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/QueueFragment.java index e64d7960f1..55a0fc0115 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/QueueFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/QueueFragment.java @@ -1,9 +1,6 @@ package me.devsaki.hentoid.fragments; -import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.FragmentActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -11,6 +8,8 @@ import android.widget.ListView; import android.widget.TextView; +import androidx.annotation.NonNull; + import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -25,10 +24,12 @@ import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.events.DownloadEvent; +import me.devsaki.hentoid.events.DownloadPreparationEvent; import me.devsaki.hentoid.fragments.downloads.ErrorStatsDialogFragment; import me.devsaki.hentoid.services.ContentQueueManager; import me.devsaki.hentoid.ui.BlinkAnimation; import me.devsaki.hentoid.util.Helper; +import me.devsaki.hentoid.views.CircularProgressView; import timber.log.Timber; /** @@ -37,7 +38,6 @@ */ public class QueueFragment extends BaseFragment { - private Context context; // App context private QueueContentAdapter mAdapter; // Adapter for queue management // UI ELEMENTS @@ -47,11 +47,13 @@ public class QueueFragment extends BaseFragment { private ImageButton btnStats; // Error statistics button private TextView queueStatus; // 1st line of text displayed on the right of the queue pause / play button private TextView queueInfo; // 2nd line of text displayed on the right of the queue pause / play button + private CircularProgressView dlPreparationProgressBar; // Circular progress bar for downloads preparation + // State + private boolean isPreparingDownload = false; + private boolean isPaused = false; + private boolean isEmpty = false; - public static QueueFragment newInstance() { - return new QueueFragment(); - } @Override public void onResume() { @@ -59,17 +61,6 @@ public void onResume() { update(); } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - FragmentActivity activity = getActivity(); - if (null == activity) { - Timber.e("Activity unreachable !"); - return; - } - context = activity.getApplicationContext(); - } - @Override public void onDestroy() { mAdapter.dispose(); @@ -90,19 +81,16 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, btnStats = rootView.findViewById(R.id.btnStats); queueStatus = rootView.findViewById(R.id.queueStatus); queueInfo = rootView.findViewById(R.id.queueInfo); - - // Remplace placeholder text used in UI designer by empty strings - queueStatus.setText(R.string.queue_empty2); - queueInfo.setText(R.string.queue_empty2); + dlPreparationProgressBar = rootView.findViewById(R.id.queueDownloadPreparationProgressBar); // Both queue control buttons actually just need to send a signal that will be processed accordingly by whom it may concern btnStart.setOnClickListener(v -> EventBus.getDefault().post(new DownloadEvent(DownloadEvent.EV_UNPAUSE))); btnPause.setOnClickListener(v -> EventBus.getDefault().post(new DownloadEvent(DownloadEvent.EV_PAUSE))); btnStats.setOnClickListener(v -> showStats()); - ObjectBoxDB db = ObjectBoxDB.getInstance(context); + ObjectBoxDB db = ObjectBoxDB.getInstance(requireActivity()); List contents = db.selectQueueContents(); - mAdapter = new QueueContentAdapter(context, contents); + mAdapter = new QueueContentAdapter(requireActivity(), contents); mListView.setAdapter(mAdapter); return rootView; @@ -125,9 +113,9 @@ public void onDownloadEvent(DownloadEvent event) { break; case DownloadEvent.EV_UNPAUSE: ContentQueueManager.getInstance().unpauseQueue(); - ObjectBoxDB db = ObjectBoxDB.getInstance(context); + ObjectBoxDB db = ObjectBoxDB.getInstance(requireActivity()); db.updateContentStatus(StatusContent.PAUSED, StatusContent.DOWNLOADING); - ContentQueueManager.getInstance().resumeQueue(context); + ContentQueueManager.getInstance().resumeQueue(requireActivity()); update(event.eventType); break; case DownloadEvent.EV_SKIP: @@ -137,17 +125,38 @@ public void onDownloadEvent(DownloadEvent event) { updateBookTitle(content.getTitle()); queueInfo.setText(""); } + dlPreparationProgressBar.setVisibility(View.GONE); break; case DownloadEvent.EV_COMPLETE: mAdapter.removeFromQueue(event.content); if (0 == mAdapter.getCount()) btnStats.setVisibility(View.GONE); update(event.eventType); break; - default: // EV_PAUSE, EV_CANCEL events + EV_COMPLETE that doesn't have a break + default: // EV_PAUSE, EV_CANCEL events + dlPreparationProgressBar.setVisibility(View.GONE); update(event.eventType); } } + /** + * Download preparation event handler + * + * @param event Broadcasted event + */ + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPrepDownloadEvent(DownloadPreparationEvent event) { + if (!dlPreparationProgressBar.isShown() && !event.isCompleted() && !isPaused && !isEmpty) { + dlPreparationProgressBar.setTotal(event.total); + dlPreparationProgressBar.setVisibility(View.VISIBLE); + queueInfo.setText(R.string.queue_preparing); + isPreparingDownload = true; + } else if (dlPreparationProgressBar.isShown() && event.isCompleted()) { + dlPreparationProgressBar.setVisibility(View.GONE); + } + + dlPreparationProgressBar.setProgress(event.total - event.done); + } + /** * Update main progress bar and bottom progress panel for current (1st in queue) book * @@ -159,23 +168,19 @@ private void updateProgress(int pagesOK, int pagesKO, int totalPages) { if (!ContentQueueManager.getInstance().isQueuePaused() && mAdapter != null && mAdapter.getCount() > 0) { Content content = mAdapter.getItem(0); - if (content != null) { - // Pages download has started - if (pagesKO + pagesOK > 0) { + // Pages download has started + if (content != null && pagesKO + pagesOK > 0) { + // Update book progress bar + content.setPercent((pagesOK + pagesKO) * 100.0 / totalPages); + mAdapter.updateProgress(0, content); - // Update book progress bar - content.setPercent((pagesOK + pagesKO) * 100.0 / totalPages); - mAdapter.updateProgress(0, content); + // Update information bar + StringBuilder message = new StringBuilder(); + String processedPagesFmt = Helper.formatIntAsStr(pagesOK, String.valueOf(totalPages).length()); + message.append(processedPagesFmt).append("/").append(totalPages).append(" processed (").append(pagesKO).append(" errors)"); - // Update information bar - StringBuilder message = new StringBuilder(); - String processedPagesFmt = Helper.compensateStringLength(pagesOK, String.valueOf(totalPages).length()); - message.append(processedPagesFmt).append("/").append(totalPages).append(" processed (").append(pagesKO).append(" errors)"); - - queueInfo.setText(message.toString()); - } else { // Pages download is under preparation - queueInfo.setText(R.string.queue_preparing); - } + queueInfo.setText(message.toString()); + isPreparingDownload = false; } } } @@ -186,10 +191,10 @@ private void updateProgress(int pagesOK, int pagesKO, int totalPages) { * @param bookTitle Book title to display */ private void updateBookTitle(String bookTitle) { - queueStatus.setText(MessageFormat.format(context.getString(R.string.queue_dl), bookTitle)); + queueStatus.setText(MessageFormat.format(requireActivity().getString(R.string.queue_dl), bookTitle)); } - public void update() { + private void update() { update(-1); } @@ -198,10 +203,10 @@ public void update() { * * @param eventType Event type that triggered the update, if any (See types described in DownloadEvent); -1 if none */ - public void update(int eventType) { + private void update(int eventType) { int bookDiff = (eventType == DownloadEvent.EV_CANCEL) ? 1 : 0; // Cancel event means a book will be removed very soon from the queue - boolean isEmpty = (0 == mAdapter.getCount() - bookDiff); - boolean isPaused = (!isEmpty && (eventType == DownloadEvent.EV_PAUSE || ContentQueueManager.getInstance().isQueuePaused() || !ContentQueueManager.getInstance().isQueueActive())); + isEmpty = (0 == mAdapter.getCount() - bookDiff); + isPaused = (!isEmpty && (eventType == DownloadEvent.EV_PAUSE || ContentQueueManager.getInstance().isQueuePaused() || !ContentQueueManager.getInstance().isQueueActive())); boolean isActive = (!isEmpty && !isPaused); Timber.d("Queue state : E/P/A > %s/%s/%s -- %s elements", isEmpty, isPaused, isActive, mAdapter.getCount()); @@ -210,7 +215,7 @@ public void update(int eventType) { mEmptyText.setVisibility(isEmpty ? View.VISIBLE : View.GONE); // Update control bar status - queueInfo.setText(R.string.queue_empty2); + queueInfo.setText(isPreparingDownload && !isEmpty ? R.string.queue_preparing : R.string.queue_empty2); Content firstContent = isEmpty ? null : mAdapter.getItem(0); @@ -236,8 +241,7 @@ public void update(int eventType) { } else { // Empty btnStart.setVisibility(View.GONE); btnStats.setVisibility(View.GONE); - queueStatus.setText(R.string.queue_empty2); - queueInfo.setText(R.string.queue_empty2); + queueStatus.setText(""); } } } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/SearchBottomSheetFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/SearchBottomSheetFragment.java index 9adadd8aec..108d385023 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/SearchBottomSheetFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/SearchBottomSheetFragment.java @@ -3,16 +3,23 @@ import android.app.Activity; import android.app.SearchManager; import android.app.SearchableInfo; -import android.arch.lifecycle.ViewModelProviders; + +import androidx.lifecycle.ViewModelProviders; + import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.BottomSheetDialogFragment; -import android.support.design.widget.Snackbar; -import android.support.v4.app.FragmentManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.snackbar.BaseTransientBottomBar; +import com.google.android.material.snackbar.Snackbar; + +import androidx.fragment.app.FragmentManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.appcompat.widget.SearchView; + import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -45,12 +52,12 @@ /** * TODO: look into recyclerview.extensions.ListAdapter for a RecyclerView.Adapter that can issue - * appropriate notify commands based on list diff + * appropriate notify commands based on list diff */ public class SearchBottomSheetFragment extends BottomSheetDialogFragment { /** - * Strings submitted to this will be debounced to {@link #searchMasterData(String)} after the given + * Strings submitted to this will be debounced to {@link #searchMasterData} after the given * delay. * * @see Debouncer @@ -85,7 +92,7 @@ public class SearchBottomSheetFragment extends BottomSheetDialogFragment { // ======== CONSTANTS - private final static int ATTRS_PER_PAGE = 40; + private static final int ATTRS_PER_PAGE = 40; public static void show(FragmentManager fragmentManager, int mode, AttributeType[] types) { @@ -101,7 +108,7 @@ public static void show(FragmentManager fragmentManager, int mode, AttributeType } @Override - public void onAttach(Context context) { + public void onAttach(@NonNull Context context) { super.onAttach(context); Bundle bundle = getArguments(); @@ -112,7 +119,7 @@ public void onAttach(Context context) { currentPage = 1; if (-1 == mode || selectedAttributeTypes.isEmpty()) { - throw new RuntimeException("Initialization failed"); + throw new IllegalArgumentException("Initialization failed"); } viewModel = ViewModelProviders.of(requireActivity()).get(SearchViewModel.class); @@ -168,7 +175,7 @@ public boolean onQueryTextChange(String s) { if (MODE_MIKAN == mode && mainAttr.equals(AttributeType.TAG) && IllegalTags.isIllegal(s)) { Snackbar.make(view, R.string.masterdata_illegal_tag, Snackbar.LENGTH_LONG).show(); searchMasterDataDebouncer.clear(); - } else /*if (!s.isEmpty())*/ { + } else { searchMasterDataDebouncer.submit(s); } @@ -260,7 +267,7 @@ private void onAttributesFailed(String message) { // Set retry button if Mikan mode on if (MODE_MIKAN == mode) { bar.setAction("RETRY", v -> viewModel.onCategoryFilterChanged(tagSearchView.getQuery().toString(), currentPage, ATTRS_PER_PAGE)); - bar.setDuration(Snackbar.LENGTH_LONG); + bar.setDuration(BaseTransientBottomBar.LENGTH_LONG); } bar.show(); @@ -289,11 +296,11 @@ private void onAttributeChosen(View button) { */ private static SearchableInfo getSearchableInfo(Activity activity) { final SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE); - if (searchManager == null) throw new RuntimeException(); + if (searchManager == null) throw new IllegalArgumentException(); return searchManager.getSearchableInfo(activity.getComponentName()); } - protected boolean isLastPage() { + private boolean isLastPage() { return (currentPage * ATTRS_PER_PAGE >= mTotalSelectedCount); } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/about/ChangelogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/about/ChangelogFragment.java new file mode 100644 index 0000000000..5b14ca1979 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/fragments/about/ChangelogFragment.java @@ -0,0 +1,136 @@ +package me.devsaki.hentoid.fragments.about; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.IFlexible; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import me.devsaki.hentoid.BuildConfig; +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.retrofit.GithubServer; +import me.devsaki.hentoid.services.UpdateCheckService; +import me.devsaki.hentoid.services.UpdateDownloadService; +import me.devsaki.hentoid.viewholders.GitHubRelease; +import timber.log.Timber; + +import static androidx.core.view.ViewCompat.requireViewById; + +public class ChangelogFragment extends Fragment { + + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + private FlexibleAdapter changelogAdapter; + + // Download bar + private TextView downloadLatestText; + private View downloadLatestButton; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_changelog, container, false); + + downloadLatestText = requireViewById(rootView, R.id.changelogDownloadLatestText); + downloadLatestButton = requireViewById(rootView, R.id.changelogDownloadLatestButton); + downloadLatestText.setOnClickListener(this::onDownloadClick); + downloadLatestButton.setOnClickListener(this::onDownloadClick); + + initRecyclerView(rootView); + getReleases(); + + return rootView; + } + + @Override + public void onDestroy() { + compositeDisposable.clear(); + super.onDestroy(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + requireActivity().onBackPressed(); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + private void initRecyclerView(View rootView) { + // TODO - invisible init while loading + changelogAdapter = new FlexibleAdapter<>(null, null, true); + + RecyclerView recyclerView = requireViewById(rootView, R.id.changelogList); + recyclerView.setAdapter(changelogAdapter); + recyclerView.setHasFixedSize(true); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + } + + private void getReleases() { + compositeDisposable.add(GithubServer.API.getReleases() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onCheckSuccess, this::onCheckError) + ); + } + + private void onCheckSuccess(List releasesInfo) { + List releases = new ArrayList<>(); + + String latestTagName = ""; + for (GitHubRelease.Struct r : releasesInfo) { + GitHubRelease release = new GitHubRelease(r); + if (release.isTagPrior(BuildConfig.VERSION_NAME)) releases.add(release); + if (latestTagName.isEmpty()) latestTagName = release.getTagName(); + } + + changelogAdapter.addItems(0, releases); + if (releasesInfo.size() > releases.size()) enableDownloadBar(latestTagName); + // TODO show RecyclerView + } + + private void onCheckError(Throwable t) { + Timber.w(t, "Error fetching GitHub releases data"); + // TODO - don't show recyclerView; show an error message on the entire screen + } + + private void enableDownloadBar(String latestTagName) { + downloadLatestText.setText(downloadLatestText.getContext().getString(R.string.get_latest).replace("@v", latestTagName)); + downloadLatestText.setVisibility(View.VISIBLE); + downloadLatestButton.setVisibility(View.VISIBLE); + } + + private void onDownloadClick(View v) { + // Equivalent to "check for updates" preferences menu + if (!UpdateDownloadService.isRunning()) { + Intent intent = UpdateCheckService.makeIntent(requireContext(), true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + requireContext().startForegroundService(intent); + } else { + requireContext().startService(intent); + } + } + } + +} diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/about/LicensesFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/about/LicensesFragment.java new file mode 100644 index 0000000000..a45e4c05c5 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/fragments/about/LicensesFragment.java @@ -0,0 +1,45 @@ +package me.devsaki.hentoid.fragments.about; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; + +import me.devsaki.hentoid.R; + +import static androidx.core.view.ViewCompat.requireViewById; + +public class LicensesFragment extends Fragment { + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_licenses, container, false); + + WebView webView = requireViewById(rootView, R.id.licenses); + webView.loadUrl("file:///android_asset/licenses.html"); + webView.setInitialScale(95); + + return rootView; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + requireActivity().onBackPressed(); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/AboutMikanDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/AboutMikanDialogFragment.java similarity index 72% rename from app/src/main/java/me/devsaki/hentoid/fragments/AboutMikanDialogFragment.java rename to app/src/main/java/me/devsaki/hentoid/fragments/downloads/AboutMikanDialogFragment.java index 4ef855482a..213e6647e4 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/AboutMikanDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/AboutMikanDialogFragment.java @@ -1,12 +1,14 @@ -package me.devsaki.hentoid.fragments; +package me.devsaki.hentoid.fragments.downloads; import android.app.Dialog; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.FragmentManager; import android.webkit.WebView; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; + public final class AboutMikanDialogFragment extends DialogFragment { public static void show(FragmentManager fragmentManager) { @@ -21,7 +23,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { webView.loadUrl("file:///android_asset/about_mikan.html"); webView.setInitialScale(95); - return new android.support.v7.app.AlertDialog.Builder(requireContext()) + return new AlertDialog.Builder(requireContext()) .setTitle("About Mikan Search") .setPositiveButton(android.R.string.ok, null) .create(); diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/downloads/EndlessFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/EndlessFragment.java index f29dd62110..197f2fcd95 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/downloads/EndlessFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/EndlessFragment.java @@ -16,7 +16,7 @@ public class EndlessFragment extends DownloadsFragment { // True if the user is currently loading a page; false if not - boolean isPageLoading = false; + private boolean isPageLoading = false; @Override @@ -56,7 +56,7 @@ protected boolean forceSearchFromPageOne() { private void onLoadMore() { if (!isLastPage()) { // NB : In EndlessFragment, a "page" is a group of loaded books. Last page is reached when scrolling reaches the very end of the book list - currentPage++; + searchManager.increaseCurrentPage(); isPageLoading = true; searchLibrary(false); Timber.d("Load more data now~"); diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/downloads/ErrorStatsDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/ErrorStatsDialogFragment.java index e92964559e..7f9f4fc9e3 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/downloads/ErrorStatsDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/ErrorStatsDialogFragment.java @@ -1,11 +1,11 @@ package me.devsaki.hentoid.fragments.downloads; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.FragmentManager; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.snackbar.Snackbar; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -50,7 +50,7 @@ public static void invoke(FragmentManager fragmentManager, long id) { args.putLong(ID, id); fragment.setArguments(args); - fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.DownloadsDialog); + fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.Dialog); fragment.show(fragmentManager, null); } @@ -121,12 +121,12 @@ public void onDownloadEvent(DownloadEvent event) { } else if (event.eventType == DownloadEvent.EV_CANCEL) { details.setText("Download cancelled"); previousNbErrors = 0; - } else if (event.eventType == DownloadEvent.EV_PROGRESS) { - if (event.pagesKO > previousNbErrors && event.content != null) { - currentId = event.content.getId(); - previousNbErrors = event.pagesKO; - updateStats(currentId); - } + } else if ((event.eventType == DownloadEvent.EV_PROGRESS) + && (event.pagesKO > previousNbErrors) + && (event.content != null)) { + currentId = event.content.getId(); + previousNbErrors = event.pagesKO; + updateStats(currentId); } } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/downloads/NavigationDrawerFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/NavigationDrawerFragment.java new file mode 100644 index 0000000000..b7c7da34d2 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/NavigationDrawerFragment.java @@ -0,0 +1,112 @@ +package me.devsaki.hentoid.fragments.downloads; + +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.ActivityOptionsCompat; +import androidx.fragment.app.Fragment; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.annimon.stream.Stream; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.SelectableAdapter; +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.activities.DownloadsActivity; +import me.devsaki.hentoid.enums.DrawerItem; +import me.devsaki.hentoid.events.UpdateEvent; +import me.devsaki.hentoid.viewholders.DrawerItemFlex; + +import static androidx.recyclerview.widget.DividerItemDecoration.VERTICAL; + +public final class NavigationDrawerFragment extends Fragment { + + private DownloadsActivity parentActivity; + + private FlexibleAdapter drawerAdapter; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + parentActivity = (DownloadsActivity) context; + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + List drawerItems = Stream.of(DrawerItem.values()) + .map(DrawerItemFlex::new) + .toList(); + + drawerAdapter = new FlexibleAdapter<>(null); + drawerAdapter.setMode(SelectableAdapter.Mode.SINGLE); + drawerAdapter.addListener((FlexibleAdapter.OnItemClickListener) this::onItemClick); + drawerAdapter.addItems(0, drawerItems); + + DividerItemDecoration divider = new DividerItemDecoration(parentActivity, VERTICAL); + + Drawable d = ContextCompat.getDrawable(parentActivity, R.drawable.line_divider); + if (d != null) divider.setDrawable(d); + + View view = inflater.inflate(R.layout.fragment_navigation_drawer, container, false); + + RecyclerView recyclerView = view.findViewById(R.id.drawer_list); + recyclerView.setAdapter(drawerAdapter); + recyclerView.addItemDecoration(divider); + + return view; + } + + private boolean onItemClick(View view, int position) { + Class activityClass = DrawerItem.values()[position].activityClass; + Intent intent = new Intent(parentActivity, activityClass); + Bundle bundle = ActivityOptionsCompat + .makeCustomAnimation(parentActivity, R.anim.fade_in, R.anim.fade_out) + .toBundle(); + ContextCompat.startActivity(parentActivity, intent, bundle); + + parentActivity.overridePendingTransition(R.anim.fade_in, R.anim.fade_out); + parentActivity.onNavigationDrawerItemClicked(); + + return true; + } + + private void showFlagAboutItem() { + int aboutItemPos = DrawerItem.ABOUT.ordinal(); + DrawerItemFlex item = drawerAdapter.getItem(aboutItemPos); + if (item != null) { + item.setFlag(true); + drawerAdapter.notifyItemChanged(aboutItemPos); + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onUpdateEvent(UpdateEvent event) { + if (event.hasNewVersion) showFlagAboutItem(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (EventBus.getDefault().isRegistered(this)) EventBus.getDefault().unregister(this); + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/downloads/PagerFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/PagerFragment.java index f892549221..759dceac16 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/downloads/PagerFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/PagerFragment.java @@ -1,6 +1,6 @@ package me.devsaki.hentoid.fragments.downloads; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView; import android.view.View; import android.widget.ImageButton; @@ -8,6 +8,7 @@ import me.devsaki.hentoid.R; import me.devsaki.hentoid.abstracts.DownloadsFragment; +import me.devsaki.hentoid.collection.CollectionAccessor; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.ui.CarouselDecorator; import me.devsaki.hentoid.util.ToastUtil; @@ -24,8 +25,8 @@ public class PagerFragment extends DownloadsFragment { @Override - protected void initUI(View rootView) { - super.initUI(rootView); + protected void initUI(View rootView, CollectionAccessor accessor) { + super.initUI(rootView, accessor); RecyclerView pageCarousel = rootView.findViewById(R.id.pager); pageCarousel.setHasFixedSize(true); @@ -50,9 +51,9 @@ protected boolean forceSearchFromPageOne() { private void attachPrevious(View rootView) { ImageButton btnPrevious = rootView.findViewById(R.id.btnPrevious); btnPrevious.setOnClickListener(v -> { - if (currentPage > 1 && !isLoading) { - currentPage--; - pager.setCurrentPage(currentPage); // Cleaner when displayed on bottom bar _before_ the update starts + if (searchManager.getCurrentPage() > 1 && !isLoading) { + searchManager.decreaseCurrentPage(); + pager.setCurrentPage(searchManager.getCurrentPage()); // Cleaner when displayed on bottom bar _before_ the update starts searchLibrary(); } else if (booksPerPage > 0 && !isLoading) { ToastUtil.toast(mContext, R.string.not_previous_page); @@ -69,8 +70,8 @@ private void attachNext(View rootView) { Timber.d("No limit per page."); } else { if (!isLastPage() && !isLoading) { - currentPage++; - pager.setCurrentPage(currentPage); // Cleaner when displayed on bottom bar _before_ the update starts + searchManager.increaseCurrentPage(); + pager.setCurrentPage(searchManager.getCurrentPage()); // Cleaner when displayed on bottom bar _before_ the update starts searchLibrary(); } else if (isLastPage()) { ToastUtil.toast(mContext, R.string.not_next_page); @@ -84,8 +85,8 @@ private void attachPageSelector() { } private void onPageChange(int page) { - if (page != currentPage) { - currentPage = page; + if (page != searchManager.getCurrentPage()) { + searchManager.setCurrentPage(page); searchLibrary(); } } @@ -101,7 +102,7 @@ protected void displayResults(List results, long totalSelectedContent) toggleUI(SHOW_RESULT); pager.setPageCount((int) Math.ceil(totalSelectedContent * 1.0 / booksPerPage)); - pager.setCurrentPage(currentPage); + pager.setCurrentPage(searchManager.getCurrentPage()); mListView.scrollToPosition(0); } } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/downloads/SearchBookIdDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/SearchBookIdDialogFragment.java index 4e25443788..a768631e5a 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/downloads/SearchBookIdDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/SearchBookIdDialogFragment.java @@ -2,11 +2,11 @@ import android.content.Intent; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.FragmentManager; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -39,7 +39,7 @@ public static void invoke(FragmentManager fragmentManager, String id, ArrayList< SearchBookIdDialogFragment fragment = new SearchBookIdDialogFragment(); fragment.setArguments(args); - fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.DownloadsDialog); + fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.Dialog); fragment.show(fragmentManager, null); } @@ -62,13 +62,16 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat // Not possible for Pururin, e-hentai List sites = new ArrayList<>(); - if (!foundSitesList.contains(Site.HITOMI.getCode())) sites.add(Site.HITOMI); - if (!foundSitesList.contains(Site.NHENTAI.getCode())) sites.add(Site.NHENTAI); - if (!foundSitesList.contains(Site.ASMHENTAI.getCode())) sites.add(Site.ASMHENTAI); - if (!foundSitesList.contains(Site.ASMHENTAI_COMICS.getCode())) - sites.add(Site.ASMHENTAI_COMICS); - if (!foundSitesList.contains(Site.HENTAICAFE.getCode())) sites.add(Site.HENTAICAFE); - if (!foundSitesList.contains(Site.TSUMINO.getCode())) sites.add(Site.TSUMINO); + if (foundSitesList != null) { + if (!foundSitesList.contains(Site.HITOMI.getCode())) sites.add(Site.HITOMI); + if (!foundSitesList.contains(Site.NHENTAI.getCode())) sites.add(Site.NHENTAI); + if (!foundSitesList.contains(Site.ASMHENTAI.getCode())) sites.add(Site.ASMHENTAI); + if (!foundSitesList.contains(Site.ASMHENTAI_COMICS.getCode())) + sites.add(Site.ASMHENTAI_COMICS); + if (!foundSitesList.contains(Site.HENTAICAFE.getCode())) sites.add(Site.HENTAICAFE); + if (!foundSitesList.contains(Site.TSUMINO.getCode())) sites.add(Site.TSUMINO); + if (!foundSitesList.contains(Site.NEXUS.getCode())) sites.add(Site.NEXUS); + } SiteAdapter siteAdapter = new SiteAdapter(); siteAdapter.setOnClickListener(this::onItemSelected); @@ -93,6 +96,8 @@ private static String getUrlFromId(Site site, String id) { return site.getUrl() + "/?p=" + id; case TSUMINO: return site.getUrl() + "/Book/Info/" + id + "/"; + case NEXUS: + return site.getUrl() + "/view/" + id; default: return site.getUrl(); } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/downloads/UpdateSuccessDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/UpdateSuccessDialogFragment.java new file mode 100644 index 0000000000..5fc8cfe626 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/fragments/downloads/UpdateSuccessDialogFragment.java @@ -0,0 +1,95 @@ +package me.devsaki.hentoid.fragments.downloads; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.IFlexible; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.retrofit.GithubServer; +import me.devsaki.hentoid.viewholders.GitHubRelease; +import me.devsaki.hentoid.viewholders.GitHubReleaseDescription; +import timber.log.Timber; + +import static androidx.core.view.ViewCompat.requireViewById; + +/** + * Created by Robb on 11/2018 + * Launcher dialog for the library refresh feature + */ +public class UpdateSuccessDialogFragment extends DialogFragment { + + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + + private TextView releaseName; + private FlexibleAdapter releaseDescriptionAdapter; + + public static void invoke(FragmentManager fragmentManager) { + UpdateSuccessDialogFragment fragment = new UpdateSuccessDialogFragment(); + fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.Dialog); + fragment.show(fragmentManager, null); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedState) { + View rootView = inflater.inflate(R.layout.dialog_update_success, container, false); + + releaseName = requireViewById(rootView, R.id.changelogReleaseTitle); + + releaseDescriptionAdapter = new FlexibleAdapter<>(null); + RecyclerView releaseDescription = requireViewById(rootView, R.id.changelogReleaseDescription); + releaseDescription.setAdapter(releaseDescriptionAdapter); + releaseDescription.setLayoutManager(new LinearLayoutManager(rootView.getContext())); + + getReleases(); + + return rootView; + } + + @Override + public void onDestroy() { + compositeDisposable.clear(); + super.onDestroy(); + } + + private void getReleases() { + compositeDisposable.add(GithubServer.API.getLatestRelease() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onCheckSuccess, this::onCheckError) + ); + } + + private void onCheckSuccess(GitHubRelease.Struct latestReleaseInfo) { + releaseName.setText(latestReleaseInfo.name); + // Parse content and add lines to the description + for (String s : latestReleaseInfo.body.split("\\r\\n")) { + s = s.trim(); + if (s.startsWith("-")) addListContent(s); + else addDescContent(s); + } + } + + private void onCheckError(Throwable t) { + Timber.w(t, "Error fetching GitHub latest release data"); + } + + private void addDescContent(String text) { + releaseDescriptionAdapter.addItem(new GitHubReleaseDescription(text, GitHubReleaseDescription.Type.DESCRIPTION)); + } + + private void addListContent(String text) { + releaseDescriptionAdapter.addItem(new GitHubReleaseDescription(text, GitHubReleaseDescription.Type.LIST_ITEM)); + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/BaseSlide.java b/app/src/main/java/me/devsaki/hentoid/fragments/intro/BaseSlide.java similarity index 88% rename from app/src/main/java/me/devsaki/hentoid/fragments/BaseSlide.java rename to app/src/main/java/me/devsaki/hentoid/fragments/intro/BaseSlide.java index 570c2924b0..48af75e0e6 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/BaseSlide.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/intro/BaseSlide.java @@ -1,9 +1,9 @@ -package me.devsaki.hentoid.fragments; +package me.devsaki.hentoid.fragments.intro; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/intro/DoneIntroFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/intro/DoneIntroFragment.java index f74699d8cd..6e89d5031a 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/intro/DoneIntroFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/intro/DoneIntroFragment.java @@ -1,9 +1,9 @@ package me.devsaki.hentoid.fragments.intro; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -15,6 +15,6 @@ public class DoneIntroFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.intro_slide_06, container, false); + return inflater.inflate(R.layout.intro_slide_end, container, false); } } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/intro/ImportIntroFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/intro/ImportIntroFragment.java index c75769f89e..03cc3c5708 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/intro/ImportIntroFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/intro/ImportIntroFragment.java @@ -2,9 +2,9 @@ import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -19,7 +19,7 @@ public class ImportIntroFragment extends Fragment { private IntroActivity parentActivity; @Override - public void onAttach(Context context) { + public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof IntroActivity) { parentActivity = (IntroActivity) context; diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/intro/PermissionIntroFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/intro/PermissionIntroFragment.java index 71282717f1..231cd5b3a4 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/intro/PermissionIntroFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/intro/PermissionIntroFragment.java @@ -7,11 +7,11 @@ import android.net.Uri; import android.os.Bundle; import android.provider.Settings; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.snackbar.Snackbar; +import androidx.fragment.app.Fragment; +import androidx.core.content.ContextCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -22,16 +22,16 @@ import me.devsaki.hentoid.R; import me.devsaki.hentoid.activities.IntroActivity; -import static android.support.design.widget.Snackbar.LENGTH_LONG; +import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; public class PermissionIntroFragment extends Fragment implements ISlidePolicy { - public static final int PERMISSION_REQUEST_CODE = 0; + private static final int PERMISSION_REQUEST_CODE = 0; private IntroActivity parentActivity; @Override - public void onAttach(Context context) { + public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof IntroActivity) { parentActivity = (IntroActivity) context; diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/intro/ThemeIntroFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/intro/ThemeIntroFragment.java new file mode 100644 index 0000000000..d41475bd28 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/fragments/intro/ThemeIntroFragment.java @@ -0,0 +1,42 @@ +package me.devsaki.hentoid.fragments.intro; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.activities.IntroActivity; +import me.devsaki.hentoid.util.Preferences; + +public class ThemeIntroFragment extends Fragment { + + private IntroActivity parentActivity; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof IntroActivity) { + parentActivity = (IntroActivity) context; + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.intro_slide_06, container, false); + + View lightBtn = view.findViewById(R.id.intro_6_light); + lightBtn.setOnClickListener(v -> parentActivity.setThemePrefs(Preferences.Constant.DARK_MODE_OFF)); + + View darkBtn = view.findViewById(R.id.intro_6_dark); + darkBtn.setOnClickListener(v -> parentActivity.setThemePrefs(Preferences.Constant.DARK_MODE_ON)); + + return view; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/intro/WelcomeIntroFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/intro/WelcomeIntroFragment.java index 809abaf1f6..fef491dcb7 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/intro/WelcomeIntroFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/intro/WelcomeIntroFragment.java @@ -1,9 +1,9 @@ package me.devsaki.hentoid.fragments.intro; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatePinDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatePinDialogFragment.java index 835c4fe18c..9f0b1c3054 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatePinDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatePinDialogFragment.java @@ -3,8 +3,8 @@ import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.view.View; import me.devsaki.hentoid.R; diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatedPinPreferenceFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatedPinPreferenceFragment.java index 01b2fc9683..cc5301f2ae 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatedPinPreferenceFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatedPinPreferenceFragment.java @@ -1,10 +1,10 @@ package me.devsaki.hentoid.fragments.pin; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.snackbar.Snackbar; +import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -12,7 +12,7 @@ import me.devsaki.hentoid.R; -import static android.support.v4.view.ViewCompat.requireViewById; +import static androidx.core.view.ViewCompat.requireViewById; public final class ActivatedPinPreferenceFragment extends Fragment implements DeactivatePinDialogFragment.Parent, ResetPinDialogFragment.Parent { diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatePinDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatePinDialogFragment.java index 4e527090a2..89ed44ff7b 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatePinDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatePinDialogFragment.java @@ -3,8 +3,8 @@ import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.view.View; import me.devsaki.hentoid.R; diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatedPinPreferenceFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatedPinPreferenceFragment.java index 9d874fdc7e..b7267e3301 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatedPinPreferenceFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatedPinPreferenceFragment.java @@ -1,11 +1,11 @@ package me.devsaki.hentoid.fragments.pin; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.app.Fragment; -import android.support.v4.view.ViewCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.snackbar.Snackbar; +import androidx.fragment.app.Fragment; +import androidx.core.view.ViewCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/PinDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/pin/PinDialogFragment.java index 64c327d766..08369701cb 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/PinDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/PinDialogFragment.java @@ -1,14 +1,12 @@ package me.devsaki.hentoid.fragments.pin; import android.content.Context; -import android.content.DialogInterface; import android.os.Bundle; -import android.os.VibrationEffect; import android.os.Vibrator; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.support.v4.app.DialogFragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.fragment.app.DialogFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -18,11 +16,11 @@ import me.devsaki.hentoid.R; -import static android.support.v4.view.ViewCompat.requireViewById; +import static androidx.core.view.ViewCompat.requireViewById; public abstract class PinDialogFragment extends DialogFragment { - abstract protected void onPinAccept(String pin); + protected abstract void onPinAccept(String pin); private final StringBuilder pinValue = new StringBuilder(4); diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/ResetPinDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/pin/ResetPinDialogFragment.java index ffab989311..1217fe4113 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/ResetPinDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/ResetPinDialogFragment.java @@ -2,8 +2,8 @@ import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.view.View; import java.security.InvalidParameterException; diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/UnlockPinDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/pin/UnlockPinDialogFragment.java index 23b2e9198b..7b6f1ddb35 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/UnlockPinDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/UnlockPinDialogFragment.java @@ -3,8 +3,8 @@ import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.view.View; import me.devsaki.hentoid.R; diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/BrowseModeDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/BrowseModeDialogFragment.java index e32f544cab..203d1b5021 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/BrowseModeDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/BrowseModeDialogFragment.java @@ -1,10 +1,10 @@ package me.devsaki.hentoid.fragments.viewer; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -12,13 +12,12 @@ import me.devsaki.hentoid.R; import me.devsaki.hentoid.util.Preferences; -import static android.support.v4.view.ViewCompat.requireViewById; +import static androidx.core.view.ViewCompat.requireViewById; public class BrowseModeDialogFragment extends DialogFragment { public static void invoke(Fragment parent) { BrowseModeDialogFragment fragment = new BrowseModeDialogFragment(); - fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.ViewerBrowseModeDialog); fragment.setCancelable(false); fragment.show(parent.getChildFragmentManager(), null); } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/GoToPageDialogFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/GoToPageDialogFragment.java index 07dfbda42b..494cf958a3 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/GoToPageDialogFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/GoToPageDialogFragment.java @@ -4,22 +4,23 @@ import android.content.DialogInterface; import android.content.res.Configuration; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; -import android.support.v7.app.AlertDialog; import android.text.InputType; import android.view.WindowManager; import android.widget.EditText; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; + public final class GoToPageDialogFragment extends DialogFragment { private Parent parent; - public static void show(Fragment parentFragment) { + public static void invoke(Fragment parent) { GoToPageDialogFragment fragment = new GoToPageDialogFragment(); - fragment.show(parentFragment.getChildFragmentManager(), null); + fragment.show(parent.getChildFragmentManager(), null); } @Override diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ImageGalleryFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ImageGalleryFragment.java new file mode 100644 index 0000000000..ea3c170c06 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ImageGalleryFragment.java @@ -0,0 +1,164 @@ +package me.devsaki.hentoid.fragments.viewer; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.adapters.ImageGalleryAdapter; +import me.devsaki.hentoid.database.domains.ImageFile; +import me.devsaki.hentoid.viewholders.ImageFileFlex; +import me.devsaki.hentoid.viewmodels.ImageViewerViewModel; + +import static androidx.core.view.ViewCompat.requireViewById; + +public class ImageGalleryFragment extends Fragment { + + private static final String KEY_FILTER_FAVOURITES = "filter_favourites"; + + private ImageGalleryAdapter galleryImagesAdapter; + private ImageViewerViewModel viewModel; + private MenuItem favouritesFilterMenu; + + private Boolean filterFavourites = false; + + + static ImageGalleryFragment newInstance(boolean filterFavourites) { + ImageGalleryFragment fragment = new ImageGalleryFragment(); + Bundle args = new Bundle(); + args.putBoolean(KEY_FILTER_FAVOURITES, filterFavourites); + fragment.setArguments(args); + return fragment; + } + + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_viewer_gallery, container, false); + + Bundle arguments = getArguments(); + if (arguments != null) + filterFavourites = arguments.getBoolean(KEY_FILTER_FAVOURITES, false); + + setHasOptionsMenu(true); + viewModel = ViewModelProviders.of(requireActivity()).get(ImageViewerViewModel.class); + + initUI(view); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + viewModel.getImages() + .observe(this, this::onImagesChanged); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + requireActivity().onBackPressed(); + return true; + } else if (item.getItemId() == R.id.gallery_menu_action_favourites) { + toggleFavouritesDisplay(); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onCreateOptionsMenu(final Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.gallery_menu, menu); + favouritesFilterMenu = menu.findItem(R.id.gallery_menu_action_favourites); + updateFavouriteDisplay(); + } + + private void initUI(View rootView) { + Toolbar toolbar = requireViewById(rootView, R.id.viewer_gallery_toolbar); + ((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar); + toolbar.setTitle("Gallery"); + toolbar.setNavigationIcon(R.drawable.ic_arrow_back); + toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed()); + + galleryImagesAdapter = new ImageGalleryAdapter(null, this::onFavouriteClick); + galleryImagesAdapter.addListener((FlexibleAdapter.OnItemClickListener) this::onItemClick); + RecyclerView releaseDescription = requireViewById(rootView, R.id.viewer_gallery_recycler); + releaseDescription.setAdapter(galleryImagesAdapter); + } + + private void onImagesChanged(List images) { + for (ImageFile img : images) galleryImagesAdapter.addItem(new ImageFileFlex(img)); + } + + private boolean onItemClick(View view, int position) { + ImageFileFlex imgFileFlex = (ImageFileFlex) galleryImagesAdapter.getItem(position); + if (imgFileFlex != null) + viewModel.setStartingIndex(imgFileFlex.getItem().getDisplayOrder()); + requireActivity().onBackPressed(); + return true; + } + + private void onFavouriteClick(ImageFile img) { + viewModel.togglePageFavourite(img, this::onFavouriteSuccess); + } + + private void onFavouriteSuccess(ImageFile img) { + if (filterFavourites) { + // Reset favs filter if no favourite page remains + if (!galleryImagesAdapter.isFavouritePresent()) { + filterFavourites = false; + galleryImagesAdapter.setFilter(filterFavourites); + galleryImagesAdapter.filterItems(); + if (galleryImagesAdapter.getItemCount() > 0) galleryImagesAdapter.smoothScrollToPosition(0); + } else { + galleryImagesAdapter.notifyDataSetChanged(); // Because no easy way to spot which item has changed when the view is filtered + } + } else galleryImagesAdapter.notifyItemChanged(img.getDisplayOrder()); + + favouritesFilterMenu.setVisible(galleryImagesAdapter.isFavouritePresent()); + } + + private void toggleFavouritesDisplay() { + filterFavourites = !filterFavourites; + updateFavouriteDisplay(); + } + + private void updateFavouriteDisplay() { + favouritesFilterMenu.setVisible(galleryImagesAdapter.isFavouritePresent()); + favouritesFilterMenu.setIcon(filterFavourites ? R.drawable.ic_fav_full : R.drawable.ic_fav_empty); + galleryImagesAdapter.setFilter(filterFavourites); + galleryImagesAdapter.filterItems(); + if (galleryImagesAdapter.getItemCount() > 0) galleryImagesAdapter.smoothScrollToPosition(0); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_FILTER_FAVOURITES, filterFavourites); + } + + @Override + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + if (savedInstanceState != null) + filterFavourites = savedInstanceState.getBoolean(KEY_FILTER_FAVOURITES, false); + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ImagePagerFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ImagePagerFragment.java index f3f307147a..c9f41f5906 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ImagePagerFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ImagePagerFragment.java @@ -1,59 +1,98 @@ package me.devsaki.hentoid.fragments.viewer; -import android.arch.lifecycle.ViewModelProviders; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; import android.content.SharedPreferences; -import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.Window; import android.view.WindowManager; +import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + import java.util.List; import me.devsaki.hentoid.R; -import me.devsaki.hentoid.adapters.ImageRecyclerAdapter; -import me.devsaki.hentoid.util.Consts; -import me.devsaki.hentoid.util.Helper; +import me.devsaki.hentoid.adapters.ImagePagerAdapter; +import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.database.domains.ImageFile; import me.devsaki.hentoid.util.Preferences; +import me.devsaki.hentoid.util.ToastUtil; import me.devsaki.hentoid.viewmodels.ImageViewerViewModel; +import me.devsaki.hentoid.views.ZoomableFrame; +import me.devsaki.hentoid.views.ZoomableRecyclerView; import me.devsaki.hentoid.widget.OnZoneTapListener; import me.devsaki.hentoid.widget.PageSnapWidget; import me.devsaki.hentoid.widget.PrefetchLinearLayoutManager; import me.devsaki.hentoid.widget.ScrollPositionListener; -import me.devsaki.hentoid.widget.VolumeKeyListener; +import me.devsaki.hentoid.widget.VolumeGestureListener; -import static android.support.v4.view.ViewCompat.requireViewById; +import static android.content.Context.CLIPBOARD_SERVICE; +import static androidx.core.view.ViewCompat.requireViewById; import static java.lang.String.format; +// TODO : better document and/or encapsulate the difference between +// - paper roll mode (currently used for vertical display) +// - independent page mode (currently used for horizontal display) public class ImagePagerFragment extends Fragment implements GoToPageDialogFragment.Parent, BrowseModeDialogFragment.Parent { - private final static String KEY_HUD_VISIBLE = "hud_visible"; + private static final String KEY_HUD_VISIBLE = "hud_visible"; + private static final String KEY_GALLERY_SHOWN = "gallery_shown"; - private View controlsOverlay; + private ImagePagerAdapter adapter; private PrefetchLinearLayoutManager llm; - private ImageRecyclerAdapter adapter; - private SeekBar seekBar; - private TextView pageNumber; - private TextView pageCurrentNumber; - private TextView pageMaxNumber; - private RecyclerView recyclerView; private PageSnapWidget pageSnapWidget; - private ImageViewerViewModel viewModel; + private ZoomableFrame zoomFrame; + private ImageViewerViewModel viewModel; private SharedPreferences.OnSharedPreferenceChangeListener listener = this::onSharedPreferenceChanged; + private final RequestOptions glideRequestOptions = new RequestOptions().centerInside(); + private int imageIndex = -1; private int maxPosition; + private boolean hasGalleryBeenShown = false; + + + // Controls + private TextView pageNumberOverlay; + private ZoomableRecyclerView recyclerView; + + // == CONTROLS OVERLAY == + private View controlsOverlay; + + // Top bar controls + private TextView bookInfoText; + private View moreMenu; + private ImageView pageShuffleButton; + private TextView pageShuffleText; + private ImageView pageFavouriteButton; + private TextView pageFavouriteText; + + // Bottom bar controls + private ImageView previewImage1; + private ImageView previewImage2; + private ImageView previewImage3; + private SeekBar seekBar; + private TextView pageCurrentNumber; + private TextView pageMaxNumber; + private View prevBookButton; + private View nextBookButton; + private View favouritesGalleryBtn; @Override @@ -61,6 +100,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c View view = inflater.inflate(R.layout.activity_viewer, container, false); Preferences.registerPrefsChangedListener(listener); + viewModel = ViewModelProviders.of(requireActivity()).get(ImageViewerViewModel.class); initPager(view); initControlsOverlay(view); @@ -76,19 +116,28 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - viewModel = ViewModelProviders.of(requireActivity()).get(ImageViewerViewModel.class); - viewModel - .getImages() + viewModel.onRestoreState(savedInstanceState); + + viewModel.getImages() .observe(this, this::onImagesChanged); - if (Preferences.isViewerResumeLastLeft()) - recyclerView.scrollToPosition(viewModel.getInitialPosition()); + viewModel.getStartingIndex() + .observe(this, this::onStartingIndexChanged); + + viewModel.getContent() + .observe(this, this::onContentChanged); + + viewModel.setOnShuffledChangeListener(this::onShuffleChanged); + + if (Preferences.isOpenBookInGalleryMode() && !hasGalleryBeenShown) displayGallery(false); } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(KEY_HUD_VISIBLE, controlsOverlay.getVisibility()); + outState.putBoolean(KEY_GALLERY_SHOWN, hasGalleryBeenShown); + viewModel.onSaveState(outState); } @Override @@ -97,6 +146,7 @@ public void onViewStateRestored(@Nullable Bundle savedInstanceState) { int hudVisibility = View.INVISIBLE; // Default state at startup if (savedInstanceState != null) { hudVisibility = savedInstanceState.getInt(KEY_HUD_VISIBLE, View.INVISIBLE); + hasGalleryBeenShown = savedInstanceState.getBoolean(KEY_GALLERY_SHOWN, false); } controlsOverlay.setVisibility(hudVisibility); } @@ -107,64 +157,124 @@ public void onResume() { setSystemBarsVisible(controlsOverlay.getVisibility() == View.VISIBLE); // System bars are visible only if HUD is visible if (Preferences.Constant.PREF_VIEWER_BROWSE_NONE == Preferences.getViewerBrowseMode()) BrowseModeDialogFragment.invoke(this); + updatePageDisplay(); + updateFavouriteDisplay(); + } + + @Override + public void onStop() { + super.onStop(); + viewModel.savePosition(imageIndex); } + private void initPager(View rootView) { - adapter = new ImageRecyclerAdapter(); + adapter = new ImagePagerAdapter(); + + zoomFrame = requireViewById(rootView, R.id.image_viewer_zoom_frame); - VolumeKeyListener volumeKeyListener = new VolumeKeyListener() - .setOnVolumeDownKeyListener(this::previousPage) - .setOnVolumeUpKeyListener(this::nextPage); + VolumeGestureListener volumeGestureListener = new VolumeGestureListener() + .setOnVolumeDownListener(this::previousPage) + .setOnVolumeUpListener(this::nextPage) + .setOnBackListener(this::onBackClick); - recyclerView = requireViewById(rootView, R.id.image_viewer_recycler); + recyclerView = requireViewById(rootView, R.id.image_viewer_zoom_recycler); recyclerView.setAdapter(adapter); recyclerView.setHasFixedSize(true); recyclerView.addOnScrollListener(new ScrollPositionListener(this::onCurrentPositionChange)); - recyclerView.setOnKeyListener(volumeKeyListener.getListener()); - - llm = new PrefetchLinearLayoutManager(getContext()); - llm.setItemPrefetchEnabled(true); - llm.setPreloadItemCount(2); - recyclerView.setLayoutManager(llm); - - pageSnapWidget = new PageSnapWidget(recyclerView) - .setPageSnapEnabled(true); + recyclerView.setOnKeyListener(volumeGestureListener); + recyclerView.requestFocus(); + recyclerView.setOnScaleListener(scale -> { + if (pageSnapWidget != null && Preferences.Constant.PREF_VIEWER_ORIENTATION_HORIZONTAL == Preferences.getViewerOrientation()) { + if (1.0 == scale && !pageSnapWidget.isPageSnapEnabled()) + pageSnapWidget.setPageSnapEnabled(true); + else if (1.0 != scale && pageSnapWidget.isPageSnapEnabled()) + pageSnapWidget.setPageSnapEnabled(false); + } + }); + recyclerView.setLongTapListener(ev -> false); OnZoneTapListener onZoneTapListener = new OnZoneTapListener(recyclerView) .setOnLeftZoneTapListener(this::onLeftTap) .setOnRightZoneTapListener(this::onRightTap) .setOnMiddleZoneTapListener(this::onMiddleTap); + recyclerView.setTapListener(onZoneTapListener); // For paper roll mode (vertical) + adapter.setItemTouchListener(onZoneTapListener); // For independent images mode (horizontal) + + adapter.setRecyclerView(recyclerView); - adapter.setItemTouchListener(onZoneTapListener); + llm = new PrefetchLinearLayoutManager(getContext()); + llm.setItemPrefetchEnabled(true); + llm.setPreloadItemCount(2); + recyclerView.setLayoutManager(llm); + + pageSnapWidget = new PageSnapWidget(recyclerView); } private void initControlsOverlay(View rootView) { controlsOverlay = requireViewById(rootView, R.id.image_viewer_controls_overlay); - // Tap back button + // Back button View backButton = requireViewById(rootView, R.id.viewer_back_btn); backButton.setOnClickListener(v -> onBackClick()); - // Tap settings button + + // Settings button View settingsButton = requireViewById(rootView, R.id.viewer_settings_btn); settingsButton.setOnClickListener(v -> onSettingsClick()); - // Tap discord button - View discordButton = requireViewById(rootView, R.id.viewer_discord_text); - discordButton.setOnClickListener(v -> Helper.openUrl(requireContext(), Consts.URL_DISCORD)); + + // More button & menu + View moreButton = requireViewById(rootView, R.id.viewer_more_btn); + moreButton.setOnClickListener(v -> onMoreClick()); + moreMenu = requireViewById(rootView, R.id.viewer_more_menu); + moreMenu.setVisibility(View.INVISIBLE); + + // More menu / Page shuffle option + pageShuffleButton = requireViewById(rootView, R.id.viewer_shuffle_btn); + pageShuffleText = requireViewById(rootView, R.id.viewer_shuffle_text); + pageShuffleButton.setOnClickListener(v -> onShuffleClick()); + pageShuffleText.setOnClickListener(v -> onShuffleClick()); + + // More menu / Favourite page option + pageFavouriteButton = requireViewById(rootView, R.id.viewer_favourite_btn); + pageFavouriteText = requireViewById(rootView, R.id.viewer_favourite_text); + pageFavouriteButton.setOnClickListener(v -> onFavouriteClick()); + pageFavouriteText.setOnClickListener(v -> onFavouriteClick()); + + + // Book info text + bookInfoText = requireViewById(rootView, R.id.viewer_book_info_text); + // Page number button pageCurrentNumber = requireViewById(rootView, R.id.viewer_currentpage_text); - pageCurrentNumber.setOnClickListener(v -> GoToPageDialogFragment.show(this)); + pageCurrentNumber.setOnClickListener(v -> GoToPageDialogFragment.invoke(this)); pageMaxNumber = requireViewById(rootView, R.id.viewer_maxpage_text); - pageNumber = requireViewById(rootView, R.id.viewer_pagenumber_text); - // Slider + pageNumberOverlay = requireViewById(rootView, R.id.viewer_pagenumber_text); + + // Next/previous book + prevBookButton = requireViewById(rootView, R.id.viewer_prev_book_btn); + prevBookButton.setOnClickListener(v -> previousBook()); + nextBookButton = requireViewById(rootView, R.id.viewer_next_book_btn); + nextBookButton.setOnClickListener(v -> nextBook()); + + // Slider and preview + previewImage1 = requireViewById(rootView, R.id.viewer_image_preview1); + previewImage2 = requireViewById(rootView, R.id.viewer_image_preview2); + previewImage3 = requireViewById(rootView, R.id.viewer_image_preview3); seekBar = requireViewById(rootView, R.id.viewer_seekbar); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override - public void onStopTrackingTouch(SeekBar seekBar) { - // No need to do anything + public void onStartTrackingTouch(SeekBar seekBar) { + previewImage1.setVisibility(View.VISIBLE); + previewImage2.setVisibility(View.VISIBLE); + previewImage3.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.INVISIBLE); } @Override - public void onStartTrackingTouch(SeekBar seekBar) { - // No need to do anything + public void onStopTrackingTouch(SeekBar seekBar) { + previewImage1.setVisibility(View.INVISIBLE); + previewImage2.setVisibility(View.INVISIBLE); + previewImage3.setVisibility(View.INVISIBLE); + recyclerView.setVisibility(View.VISIBLE); } @Override @@ -172,6 +282,22 @@ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) seekToPosition(progress); } }); + + // Gallery + View galleryBtn = requireViewById(rootView, R.id.viewer_gallery_btn); + galleryBtn.setOnClickListener(v -> displayGallery(false)); + favouritesGalleryBtn = requireViewById(rootView, R.id.viewer_favourites_btn); + favouritesGalleryBtn.setOnClickListener(v -> displayGallery(true)); + } + + private boolean onBookTitleLongClick(Content content) { + ClipboardManager clipboard = (ClipboardManager) requireActivity().getSystemService(CLIPBOARD_SERVICE); + if (clipboard != null) { + ClipData clip = ClipData.newPlainText("book URL", content.getGalleryUrl()); + clipboard.setPrimaryClip(clip); + ToastUtil.toast("Book URL copied to clipboard"); + return true; + } else return false; } @Override @@ -180,38 +306,221 @@ public void onDestroy() { super.onDestroy(); } + /** + * Back button handler + */ private void onBackClick() { + viewModel.savePosition(imageIndex); requireActivity().onBackPressed(); } + /** + * Show the viewer settings dialog + */ private void onSettingsClick() { + hideMoreMenu(); ViewerPrefsDialogFragment.invoke(this); } - private void onImagesChanged(List images) { + /** + * Show/hide the "more" submenu when the "more" button is clicked + * TODO : Use a toolbar instead of all this custom stuff + */ + private void onMoreClick() { + if (View.VISIBLE == moreMenu.getVisibility()) moreMenu.setVisibility(View.INVISIBLE); + else moreMenu.setVisibility(View.VISIBLE); + } + + /** + * Hide the "more" submenu of the top bar + * TODO : Use a toolbar instead of all this custom stuff + */ + private void hideMoreMenu() { + moreMenu.setVisibility(View.INVISIBLE); + } + + /** + * Handle click on "Shuffle" action button + */ + private void onShuffleClick() { + hideMoreMenu(); + goToPage(1); + viewModel.onShuffleClick(); + } + + /** + * Handle click on "Favourite" action button + */ + private void onFavouriteClick() { + ImageFile currentImage = adapter.getImageAt(imageIndex); + if (currentImage != null) + viewModel.togglePageFavourite(currentImage, this::onFavouriteSuccess); + hideMoreMenu(); + } + + /** + * Success callback when the new favourite'd state has been successfully persisted + * + * @param img The favourite'd / unfavourite'd ImageFile in its new state + */ + private void onFavouriteSuccess(ImageFile img) { + // Check if the updated image is still the one displayed on screen + ImageFile currentImage = adapter.getImageAt(imageIndex); + if (currentImage != null && img.getId() == currentImage.getId()) { + currentImage.setFavourite(img.isFavourite()); + updateFavouriteDisplay(img.isFavourite()); + } + updateFavouritesGalleryButtonDisplay(); + } + + /** + * Observer for changes in the book's list of images + * + * @param images Book's list of images + */ + private void onImagesChanged(List images) { + hideMoreMenu(); + + adapter.setImages(images); + onUpdateImageDisplay(); // Remove cached images + maxPosition = images.size() - 1; - adapter.setImageUris(images); seekBar.setMax(maxPosition); - updatePageDisplay(); } - // Scroll listener + /** + * Observer for changes on the book's starting image index + * + * @param startingIndex Book's starting image index + */ + private void onStartingIndexChanged(Integer startingIndex) { + recyclerView.scrollToPosition(startingIndex); + } + + /** + * Observer for changes on the current book + * + * @param content Loaded book + */ + private void onContentChanged(Content content) { + updateBookInfo(content); + updateBookNavigation(content); + } + + /** + * Observer for changes on the shuffled state + * + * @param isShuffled New shuffled state + */ + private void onShuffleChanged(boolean isShuffled) { + if (isShuffled) { + pageShuffleButton.setImageResource(R.drawable.ic_menu_sort_123); + pageShuffleText.setText(R.string.viewer_order_123); + } else { + pageShuffleButton.setImageResource(R.drawable.ic_menu_sort_random); + pageShuffleText.setText(R.string.viewer_order_shuffle); + } + } + + + /** + * Scroll listener + * + * @param position New position + */ private void onCurrentPositionChange(int position) { - viewModel.setCurrentPosition(position); - seekBar.setProgress(viewModel.getCurrentPosition()); - updatePageDisplay(); + if (this.imageIndex != position) { + this.imageIndex = position; + + // Resets zoom if we're using horizontal (independent pages) mode + if (Preferences.Constant.PREF_VIEWER_ORIENTATION_HORIZONTAL == Preferences.getViewerOrientation()) + adapter.resetPosition(position); + + seekBar.setProgress(position); + updatePageDisplay(); + updateFavouriteDisplay(); + hideMoreMenu(); + } } + /** + * Update the display of page position controls (text and bar) + */ private void updatePageDisplay() { - String pageNum = viewModel.getCurrentPosition() + 1 + ""; + String pageNum = imageIndex + 1 + ""; String maxPage = maxPosition + 1 + ""; pageCurrentNumber.setText(pageNum); pageMaxNumber.setText(maxPage); - pageNumber.setText(format("%s / %s", pageNum, maxPage)); + pageNumberOverlay.setText(format("%s / %s", pageNum, maxPage)); + } + + /** + * Update the visibility of "next/previous book" buttons + * + * @param content Current book + */ + private void updateBookNavigation(Content content) { + if (content.isFirst()) prevBookButton.setVisibility(View.INVISIBLE); + else prevBookButton.setVisibility(View.VISIBLE); + if (content.isLast()) nextBookButton.setVisibility(View.INVISIBLE); + else nextBookButton.setVisibility(View.VISIBLE); + } + + /** + * Update the display of all favourite controls (favourite page action _and_ favourites gallery launcher) + */ + private void updateFavouriteDisplay() { + updateFavouritesGalleryButtonDisplay(); + + ImageFile currentImage = adapter.getImageAt(imageIndex); + if (currentImage != null) + updateFavouriteDisplay(currentImage.isFavourite()); + } + + /** + * Update the display of the favourites gallery launcher + */ + private void updateFavouritesGalleryButtonDisplay() { + if (adapter.isFavouritePresent()) + favouritesGalleryBtn.setVisibility(View.VISIBLE); + else favouritesGalleryBtn.setVisibility(View.INVISIBLE); + } + + /** + * Update the display of the "favourite page" action button + * + * @param isFavourited True if the button has to represent a favourite page; false instead + */ + private void updateFavouriteDisplay(boolean isFavourited) { + if (isFavourited) { + pageFavouriteButton.setImageResource(R.drawable.ic_fav_full); + pageFavouriteText.setText(R.string.viewer_favourite_on); + } else { + pageFavouriteButton.setImageResource(R.drawable.ic_fav_empty); + pageFavouriteText.setText(R.string.viewer_favourite_off); + } } - public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + /** + * Update the book title and author on the top bar + * + * @param content Current content whose information to display + */ + private void updateBookInfo(Content content) { + String title = content.getTitle(); + if (!content.getAuthor().isEmpty()) title += "\nby " + content.getAuthor(); + bookInfoText.setText(title); + bookInfoText.setOnLongClickListener(v -> onBookTitleLongClick(content)); + } + + /** + * Listener for preference changes (from the settings dialog) + * + * @param prefs Shared preferences object + * @param key Key that has been changed + */ + private void onSharedPreferenceChanged(SharedPreferences prefs, String key) { switch (key) { case Preferences.Key.PREF_VIEWER_BROWSE_MODE: onBrowseModeChange(); @@ -242,7 +551,7 @@ private void onUpdatePrefsScreenOn() { } private void onUpdateFlingFactor() { - pageSnapWidget.setFlingFactor(Preferences.getViewerFlingFactor()); + pageSnapWidget.setFlingSensitivity(Preferences.getViewerFlingFactor() / 100f); } private void onUpdateImageDisplay() { @@ -250,7 +559,7 @@ private void onUpdateImageDisplay() { } private void onUpdatePageNumDisplay() { - pageNumber.setVisibility(Preferences.isViewerDisplayPageNum() ? View.VISIBLE : View.GONE); + pageNumberOverlay.setVisibility(Preferences.isViewerDisplayPageNum() ? View.VISIBLE : View.GONE); } @Override @@ -258,19 +567,28 @@ public void onBrowseModeChange() { int currentLayoutDirection; // LinearLayoutManager.setReverseLayout behaves _relatively_ to current Layout Direction // => need to know that direction before deciding how to set setReverseLayout - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - if (View.LAYOUT_DIRECTION_LTR == controlsOverlay.getLayoutDirection()) - currentLayoutDirection = Preferences.Constant.PREF_VIEWER_DIRECTION_LTR; - else currentLayoutDirection = Preferences.Constant.PREF_VIEWER_DIRECTION_RTL; - } else { - currentLayoutDirection = Preferences.Constant.PREF_VIEWER_DIRECTION_LTR; // Only possibility before JELLY_BEAN_MR1 - } + if (View.LAYOUT_DIRECTION_LTR == controlsOverlay.getLayoutDirection()) + currentLayoutDirection = Preferences.Constant.PREF_VIEWER_DIRECTION_LTR; + else currentLayoutDirection = Preferences.Constant.PREF_VIEWER_DIRECTION_RTL; llm.setReverseLayout(Preferences.getViewerDirection() != currentLayoutDirection); + // Resets the views to switch between paper roll mode (vertical) and independent page mode (horizontal) + recyclerView.resetScale(); + adapter.notifyDataSetChanged(); + + if (Preferences.Constant.PREF_VIEWER_ORIENTATION_VERTICAL == Preferences.getViewerOrientation()) + zoomFrame.enable(); + else zoomFrame.disable(); + llm.setOrientation(getOrientation()); pageSnapWidget.setPageSnapEnabled(Preferences.Constant.PREF_VIEWER_ORIENTATION_VERTICAL != Preferences.getViewerOrientation()); } + /** + * Transforms current Preferences orientation into LinearLayoutManager orientation code + * + * @return Preferred orientation, as LinearLayoutManager orientation code + */ private int getOrientation() { if (Preferences.Constant.PREF_VIEWER_ORIENTATION_HORIZONTAL == Preferences.getViewerOrientation()) { return LinearLayoutManager.HORIZONTAL; @@ -279,87 +597,201 @@ private int getOrientation() { } } - public void nextPage() { - if (viewModel.getCurrentPosition() == maxPosition) return; - recyclerView.smoothScrollToPosition(viewModel.getCurrentPosition() + 1); + /** + * Load next page + */ + private void nextPage() { + hideMoreMenu(); + if (imageIndex == maxPosition) return; + if (Preferences.isViewerTapTransitions()) + recyclerView.smoothScrollToPosition(imageIndex + 1); + else + recyclerView.scrollToPosition(imageIndex + 1); } - public void previousPage() { - if (viewModel.getCurrentPosition() == 0) return; - recyclerView.smoothScrollToPosition(viewModel.getCurrentPosition() - 1); + /** + * Load previous page + */ + private void previousPage() { + hideMoreMenu(); + if (imageIndex == 0) return; + if (Preferences.isViewerTapTransitions()) + recyclerView.smoothScrollToPosition(imageIndex - 1); + else + recyclerView.scrollToPosition(imageIndex - 1); } + /** + * Load next book + */ + private void nextBook() { + viewModel.savePosition(imageIndex); + viewModel.loadNextContent(); + } + + /** + * Load previous book + */ + private void previousBook() { + viewModel.savePosition(imageIndex); + viewModel.loadPreviousContent(); + } + + /** + * Seek to the given position; update preview images if they are visible + * + * @param position Position to go to (0-indexed) + */ private void seekToPosition(int position) { - if (position == viewModel.getCurrentPosition() + 1 || position == viewModel.getCurrentPosition() - 1) { + hideMoreMenu(); + if (View.VISIBLE == previewImage2.getVisibility()) { + Context ctx = previewImage2.getContext().getApplicationContext(); + ImageFile img = adapter.getImageAt(position - 1); + if (img != null) { + Glide.with(ctx) + .load(img.getAbsolutePath()) + .apply(glideRequestOptions) + .into(previewImage1); + previewImage1.setVisibility(View.VISIBLE); + } else previewImage1.setVisibility(View.INVISIBLE); + + img = adapter.getImageAt(position); + if (img != null) + Glide.with(ctx) + .load(img.getAbsolutePath()) + .apply(glideRequestOptions) + .into(previewImage2); + + img = adapter.getImageAt(position + 1); + if (img != null) { + Glide.with(ctx) + .load(img.getAbsolutePath()) + .apply(glideRequestOptions) + .into(previewImage3); + previewImage3.setVisibility(View.VISIBLE); + } else previewImage3.setVisibility(View.INVISIBLE); + } + + if (position == imageIndex + 1 || position == imageIndex - 1) { recyclerView.smoothScrollToPosition(position); } else { recyclerView.scrollToPosition(position); } } + /** + * Go to the given page number + * + * @param pageNum Page number to go to (1-indexed) + */ @Override public void goToPage(int pageNum) { + hideMoreMenu(); int position = pageNum - 1; - if (position == viewModel.getCurrentPosition() || position < 0 || position > maxPosition) + if (position == imageIndex || position < 0 || position > maxPosition) return; seekToPosition(position); } + /** + * Handler for tapping on the left zone of the screen + */ private void onLeftTap() { + // Side-tapping disabled when view is zoomed + if (recyclerView.getCurrentScale() != 1.0) return; + if (Preferences.Constant.PREF_VIEWER_DIRECTION_LTR == Preferences.getViewerDirection()) previousPage(); else nextPage(); } + /** + * Handler for tapping on the right zone of the screen + */ private void onRightTap() { + // Side-tapping disabled when view is zoomed + if (recyclerView.getCurrentScale() != 1.0) return; + if (Preferences.Constant.PREF_VIEWER_DIRECTION_LTR == Preferences.getViewerDirection()) nextPage(); else previousPage(); } + /** + * Handler for tapping on the middle zone of the screen + */ private void onMiddleTap() { - // TODO AlphaAnimation to make it appear progressively if (View.VISIBLE == controlsOverlay.getVisibility()) { - controlsOverlay.setVisibility(View.INVISIBLE); + controlsOverlay.animate() + .alpha(0.0f) + .setDuration(100) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + controlsOverlay.setVisibility(View.INVISIBLE); + } + }); setSystemBarsVisible(false); } else { - controlsOverlay.setVisibility(View.VISIBLE); - setSystemBarsVisible(true); + controlsOverlay.animate() + .alpha(1.0f) + .setDuration(100) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + controlsOverlay.setVisibility(View.VISIBLE); + setSystemBarsVisible(true); + } + }); } + hideMoreMenu(); + } + + /** + * Display the viewer gallery + * + * @param filterFavourites True if only favourite pages have to be shown; false for all pages + */ + private void displayGallery(boolean filterFavourites) { + hasGalleryBeenShown = true; + viewModel.setStartingIndex(imageIndex); // Memorize the current page + requireFragmentManager() + .beginTransaction() + .replace(android.R.id.content, ImageGalleryFragment.newInstance(filterFavourites)) + .addToBackStack(null) // This triggers a memory leak in LeakCanary but is _not_ a leak : see https://stackoverflow.com/questions/27913009/memory-leak-in-fragmentmanager + .commit(); } + /** + * Show / hide bottom and top Android system bars + * + * @param visible True if bars have to be shown; false instead + */ private void setSystemBarsVisible(boolean visible) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - Window window = requireActivity().getWindow(); - if (!visible) { - window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - } + int uiOptions; + if (visible) { + uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; } else { - int uiOptions; - if (visible) { - uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; - } else { - uiOptions = View.SYSTEM_UI_FLAG_IMMERSIVE - // Set the content to appear under the system bars so that the - // content doesn't resize when the system bars hide and show. - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - // Hide the nav bar and status bar - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN; - } - - // Defensive programming here because crash reports show that getView() sometimes is null - // (just don't ask me why...) - View v = getView(); - if (v != null) v.setSystemUiVisibility(uiOptions); + uiOptions = View.SYSTEM_UI_FLAG_IMMERSIVE + // Set the content to appear under the system bars so that the + // content doesn't resize when the system bars hide and show. + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + // Hide the nav bar and status bar + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN; } + + // Defensive programming here because crash reports show that getView() sometimes is null + // (just don't ask me why...) + View v = getView(); + if (v != null) v.setSystemUiVisibility(uiOptions); } } 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 5756d53517..a6c9d19981 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 @@ -1,11 +1,10 @@ package me.devsaki.hentoid.fragments.viewer; -import android.os.Build; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -18,13 +17,12 @@ import me.devsaki.hentoid.R; import me.devsaki.hentoid.util.Preferences; -import static android.support.v4.view.ViewCompat.requireViewById; +import static androidx.core.view.ViewCompat.requireViewById; public class ViewerPrefsDialogFragment extends DialogFragment { public static void invoke(Fragment parent) { ViewerPrefsDialogFragment fragment = new ViewerPrefsDialogFragment(); - fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.ViewerBrowseModeDialog); fragment.show(parent.getChildFragmentManager(), null); } @@ -38,18 +36,26 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - Switch theSwitch = requireViewById(view, R.id.viewer_prefs_resume_reading_action); + Switch theSwitch = requireViewById(view, R.id.viewer_prefs_keep_screen_action); + theSwitch.setChecked(Preferences.isViewerKeepScreenOn()); + theSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> Preferences.setViewerKeepScreenOn(isChecked)); + + theSwitch = requireViewById(view, R.id.viewer_prefs_resume_reading_action); theSwitch.setChecked(Preferences.isViewerResumeLastLeft()); theSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> Preferences.setViewerResumeLastLeft(isChecked)); - theSwitch = requireViewById(view, R.id.viewer_prefs_keep_screen_action); - theSwitch.setChecked(Preferences.isViewerKeepScreenOn()); - theSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> Preferences.setViewerKeepScreenOn(isChecked)); + theSwitch = requireViewById(view, R.id.viewer_prefs_open_gallery_action); + theSwitch.setChecked(Preferences.isOpenBookInGalleryMode()); + theSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> Preferences.setOpenBookInGalleryMode(isChecked)); theSwitch = requireViewById(view, R.id.viewer_prefs_display_pagenum_action); theSwitch.setChecked(Preferences.isViewerDisplayPageNum()); theSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> Preferences.setViewerDisplayPageNum(isChecked)); + theSwitch = requireViewById(view, R.id.viewer_prefs_tap_transitions_action); + theSwitch.setChecked(Preferences.isViewerTapTransitions()); + theSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> Preferences.setViewerTapTransitions(isChecked)); + RadioGroup theRadio = requireViewById(view, R.id.viewer_prefs_display_mode_group); switch (Preferences.getViewerResizeMode()) { case (Preferences.Constant.PREF_VIEWER_DISPLAY_FIT): @@ -80,10 +86,6 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat theRadio.setOnCheckedChangeListener(this::onChangeBrowseMode); SeekBar flingSensitivity = requireViewById(view, R.id.viewer_prefs_fling_sensitivity); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - flingSensitivity.setMin(25); - } - flingSensitivity.setMax(100); flingSensitivity.setProgress(Preferences.getViewerFlingFactor()); flingSensitivity.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override diff --git a/app/src/main/java/me/devsaki/hentoid/listener/ContentClickListener.java b/app/src/main/java/me/devsaki/hentoid/listener/ContentClickListener.java index e9e4ff7b15..933dad350d 100644 --- a/app/src/main/java/me/devsaki/hentoid/listener/ContentClickListener.java +++ b/app/src/main/java/me/devsaki/hentoid/listener/ContentClickListener.java @@ -1,12 +1,10 @@ package me.devsaki.hentoid.listener; -import android.content.Context; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.util.FileHelper; import timber.log.Timber; /** @@ -17,15 +15,13 @@ public class ContentClickListener implements OnClickListener, OnLongClickListene // TODO - rework this class : each time it is used, either onClick or onLongClick are overriden - private final Context context; private final Content content; private final ItemSelectListener listener; private final int position; private int selectedItemCount; private boolean selected; - protected ContentClickListener(Context context, Content content, int pos, ItemSelectListener listener) { - this.context = context; + protected ContentClickListener(Content content, int pos, ItemSelectListener listener) { this.content = content; this.position = pos; this.selectedItemCount = 0; @@ -45,14 +41,6 @@ private void updateSelector() { } } - @Override - public void onClick(View v) { - if (!selected) { - FileHelper.openContent(context, content); - } - } - - @Override public boolean onLongClick(View v) { updateSelector(); @@ -62,6 +50,10 @@ public boolean onLongClick(View v) { return true; } + public void onClick(View v) { + // Empty; ready to be overriden + } + public interface ItemSelectListener { void onItemSelected(int selectedCount); diff --git a/app/src/main/java/me/devsaki/hentoid/listener/ContentListener.java b/app/src/main/java/me/devsaki/hentoid/listener/ContentListener.java deleted file mode 100644 index ae97e08c54..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/listener/ContentListener.java +++ /dev/null @@ -1,12 +0,0 @@ -package me.devsaki.hentoid.listener; - -import java.util.List; - -import me.devsaki.hentoid.database.domains.Content; - -public interface ContentListener { - void onContentReady(List results, long totalSelectedContent, long totalContent); - - void onContentFailed(Content content, String message); -} - diff --git a/app/src/main/java/me/devsaki/hentoid/listener/PagedResultListener.java b/app/src/main/java/me/devsaki/hentoid/listener/PagedResultListener.java new file mode 100644 index 0000000000..b71caf9bfb --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/listener/PagedResultListener.java @@ -0,0 +1,10 @@ +package me.devsaki.hentoid.listener; + +import java.util.List; + +public interface PagedResultListener { + void onPagedResultReady(List results, long totalSelected, long total); + + void onPagedResultFailed(T result, String message); +} + diff --git a/app/src/main/java/me/devsaki/hentoid/model/DoujinBuilder.java b/app/src/main/java/me/devsaki/hentoid/model/DoujinBuilder.java index 565ceb1ec6..aae5382db9 100644 --- a/app/src/main/java/me/devsaki/hentoid/model/DoujinBuilder.java +++ b/app/src/main/java/me/devsaki/hentoid/model/DoujinBuilder.java @@ -6,6 +6,8 @@ /** * General builder for works. + * + * @deprecated Replaced by {@link me.devsaki.hentoid.services.ImportService} methods; class is kept for retrocompatibilty */ @Deprecated public class DoujinBuilder { @@ -32,10 +34,10 @@ public class DoujinBuilder { private List lstTags; public String getId() { - int idxStart = url.lastIndexOf("/"); + int idxStart = url.lastIndexOf('/'); String id = url.substring(idxStart); String category = url.replace(id, ""); - category = category.substring(category.lastIndexOf("/")); + category = category.substring(category.lastIndexOf('/')); return category + id; } diff --git a/app/src/main/java/me/devsaki/hentoid/model/State.java b/app/src/main/java/me/devsaki/hentoid/model/State.java deleted file mode 100644 index 3b2230d7b6..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/model/State.java +++ /dev/null @@ -1,70 +0,0 @@ -package me.devsaki.hentoid.model; - -import java.util.Collections; -import java.util.List; - -/** - * Prescriptive object that represents the state of a ViewModel for use with - * SearchBottomSheetFragment. This is only an example. - *

- * This should be immutable. In case that this should change due to a user interaction, the - * ViewModel should be notified of the user interaction, create a new State that represents it's new - * state, then notify the view about the change in the viewmodel's state. - */ -public class State { - - /** all is well */ - public static final int STATUS_SUCCESS = 0; - - /** - * the view may interpret this as to show a loading indicator - */ - public static final int STATUS_LOADING = 1; - - /** - * may be used as a default error status. the view may interpret this as a generic error and - * display a generic error message - */ - public static final int STATUS_ERROR_1 = 2; - - /** - * may be used for other error state that the view may be interested in. in case that a - * different display message should be used for different types of errors - */ - public static final int STATUS_ERROR_2 = 3; - - /** - * some error states may be interpreted by the view as a recoverable error that may offer a - * 'retry' affordance - */ - public static final int STATUS_ERROR_3 = 4; - - private final int status; - - private final List choices; - - public State(int status, List choices) { - this.status = status; - this.choices = Collections.unmodifiableList(choices); - } - - /** - * It is up to the ViewModel to declare the status while it is up to the View to interpret how - * to display that status such as what error message to display, or what layout to use - *

- * Should be one of the ff: {@link #STATUS_SUCCESS}, {@link #STATUS_LOADING}, {@link - * #STATUS_ERROR_1}, {@link #STATUS_ERROR_2}, {@link #STATUS_ERROR_3} - */ - public int getStatus() { - return status; - } - - /** - * The view may choose to call this depending on the value of {@link #status} - * - * @return an immutable list of choices which each represent a choice for the user to select - */ - public List getChoices() { - return choices; - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/model/URLBuilder.java b/app/src/main/java/me/devsaki/hentoid/model/URLBuilder.java index c3455d1a0e..eeb05c6570 100644 --- a/app/src/main/java/me/devsaki/hentoid/model/URLBuilder.java +++ b/app/src/main/java/me/devsaki/hentoid/model/URLBuilder.java @@ -4,6 +4,8 @@ /** * General builder for URLs + * + * @deprecated Replaced by {@link me.devsaki.hentoid.services.ImportService} methods; class is kept for retrocompatibilty */ @Deprecated public class URLBuilder { @@ -14,10 +16,10 @@ public class URLBuilder { private String description; public String getId() { - int idxStart = url.lastIndexOf("/"); + int idxStart = url.lastIndexOf('/'); String id = url.substring(idxStart); String category = url.replace(id, ""); - category = category.substring(category.lastIndexOf("/")); + category = category.substring(category.lastIndexOf('/')); return category + id; } diff --git a/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadErrorNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadErrorNotification.java index 99101b71b9..79b75d15f7 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadErrorNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadErrorNotification.java @@ -3,8 +3,8 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import me.devsaki.hentoid.R; import me.devsaki.hentoid.activities.DownloadsActivity; diff --git a/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadNotificationChannel.java b/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadNotificationChannel.java index 1155fba1bf..360cae2386 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadNotificationChannel.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadNotificationChannel.java @@ -9,14 +9,22 @@ public class DownloadNotificationChannel { - static final String ID = "download"; + private static final String ID_OLD = "download"; + static final String ID = "downloads"; public static void init(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - String name = "Downloads"; + String name = "Book downloads"; int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(ID, name, importance); + channel.setSound(null, null); + channel.setVibrationPattern(null); + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + + // Mandatory; it is not possible to change the sound of an existing channel after its initial creation + notificationManager.deleteNotificationChannel(ID_OLD); + Objects.requireNonNull(notificationManager).createNotificationChannel(channel); } } diff --git a/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadProgressNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadProgressNotification.java index f7342e41d2..f09794c81f 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadProgressNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadProgressNotification.java @@ -3,9 +3,10 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; -import android.support.v4.content.ContextCompat; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.content.ContextCompat; import java.util.Locale; @@ -30,16 +31,17 @@ public DownloadProgressNotification(String title, int progress, int max) { @NonNull @Override public android.app.Notification onCreateNotification(Context context) { - return new NotificationCompat.Builder(context) + return new NotificationCompat.Builder(context, DownloadNotificationChannel.ID) .setSmallIcon(R.drawable.ic_stat_hentoid) .setContentTitle(context.getString(R.string.downloading)) .setContentText(title) .setContentInfo(getProgressString()) .setProgress(max, progress, false) - .setColor(ContextCompat.getColor(context, R.color.accent)) + .setColor(ContextCompat.getColor(context, R.color.secondary)) .setContentIntent(getDefaultIntent(context)) .setLocalOnly(true) .setOngoing(true) + .setOnlyAlertOnce(true) .build(); } diff --git a/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadSuccessNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadSuccessNotification.java index da5217d96d..ed74a9b4c3 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadSuccessNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadSuccessNotification.java @@ -3,8 +3,8 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import me.devsaki.hentoid.R; import me.devsaki.hentoid.activities.DownloadsActivity; diff --git a/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadWarningNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadWarningNotification.java index cbc7607b7e..a903728383 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadWarningNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/download/DownloadWarningNotification.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.notification.download; import android.content.Context; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import me.devsaki.hentoid.R; import me.devsaki.hentoid.util.notification.Notification; diff --git a/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportCompleteNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportCompleteNotification.java index 950577c0bd..68f01c9733 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportCompleteNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportCompleteNotification.java @@ -1,15 +1,16 @@ package me.devsaki.hentoid.notification.import_; import android.content.Context; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import me.devsaki.hentoid.R; import me.devsaki.hentoid.util.notification.Notification; public class ImportCompleteNotification implements Notification { - private final int booksOK, booksKO; + private final int booksOK; + private final int booksKO; public ImportCompleteNotification(int booksOK, int booksKO) { diff --git a/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportNotificationChannel.java b/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportNotificationChannel.java index d7b3db1cad..7d28d1457d 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportNotificationChannel.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportNotificationChannel.java @@ -9,14 +9,22 @@ public class ImportNotificationChannel { - static final String ID = "import"; + private static final String ID_OLD = "import"; + static final String ID = "import2"; public static void init(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { String name = "Library imports"; int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(ID, name, importance); + channel.setSound(null, null); + channel.setVibrationPattern(null); + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + + // Mandatory; it is not possible to change the sound of an existing channel after its initial creation + notificationManager.deleteNotificationChannel(ID_OLD); + Objects.requireNonNull(notificationManager).createNotificationChannel(channel); } } diff --git a/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportStartNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportStartNotification.java index 83533e9db0..acd269c83d 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportStartNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/import_/ImportStartNotification.java @@ -1,11 +1,10 @@ package me.devsaki.hentoid.notification.import_; import android.content.Context; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import me.devsaki.hentoid.R; -import me.devsaki.hentoid.notification.update.UpdateNotificationChannel; import me.devsaki.hentoid.util.notification.Notification; public class ImportStartNotification implements Notification { diff --git a/app/src/main/java/me/devsaki/hentoid/notification/maintenance/MaintenanceNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/maintenance/MaintenanceNotification.java new file mode 100644 index 0000000000..560ce697f5 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/notification/maintenance/MaintenanceNotification.java @@ -0,0 +1,32 @@ +package me.devsaki.hentoid.notification.maintenance; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.content.ContextCompat; + +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.util.notification.Notification; + +public class MaintenanceNotification implements Notification { + + private final String title; + + public MaintenanceNotification(String title) { + this.title = title; + } + + @NonNull + @Override + public android.app.Notification onCreateNotification(Context context) { + return new NotificationCompat.Builder(context, MaintenanceNotificationChannel.ID) + .setSmallIcon(R.drawable.ic_stat_hentoid) + .setContentTitle(context.getString(R.string.maintenance)) + .setContentText(title) + .setColor(ContextCompat.getColor(context, R.color.secondary)) + .setLocalOnly(true) + .setOngoing(true) + .build(); + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/notification/maintenance/MaintenanceNotificationChannel.java b/app/src/main/java/me/devsaki/hentoid/notification/maintenance/MaintenanceNotificationChannel.java new file mode 100644 index 0000000000..81b37eefc3 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/notification/maintenance/MaintenanceNotificationChannel.java @@ -0,0 +1,31 @@ +package me.devsaki.hentoid.notification.maintenance; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; + +import java.util.Objects; + +public class MaintenanceNotificationChannel { + + private static final String ID_OLD = "maintenance"; + static final String ID = "maintenance2"; + + public static void init(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String name = "Technical maintenance"; + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel(ID, name, importance); + channel.setSound(null, null); + channel.setVibrationPattern(null); + + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + + // Mandatory; it is not possible to change the sound of an existing channel after its initial creation + notificationManager.deleteNotificationChannel(ID_OLD); + + Objects.requireNonNull(notificationManager).createNotificationChannel(channel); + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateAvailableNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateAvailableNotification.java index c9605123a5..fe623169b5 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateAvailableNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateAvailableNotification.java @@ -3,8 +3,8 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import me.devsaki.hentoid.R; import me.devsaki.hentoid.services.UpdateDownloadService; diff --git a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateCheckNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateCheckNotification.java index b441cc1835..ccc3c32f63 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateCheckNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateCheckNotification.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.notification.update; import android.content.Context; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import me.devsaki.hentoid.R; import me.devsaki.hentoid.util.notification.Notification; @@ -13,7 +13,11 @@ public class UpdateCheckNotification implements Notification { @Override public android.app.Notification onCreateNotification(Context context) { return new NotificationCompat.Builder(context, UpdateNotificationChannel.ID) + .setDefaults(0) .setSmallIcon(R.drawable.ic_stat_hentoid) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setVibrate(null) + .setSound(null) .setContentTitle("Checking for updates") .setContentText("Please wait") .setProgress(0, 0, true) diff --git a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateFailedNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateFailedNotification.java index 77e8dcf8aa..be409495d6 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateFailedNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateFailedNotification.java @@ -4,8 +4,8 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import me.devsaki.hentoid.R; import me.devsaki.hentoid.services.UpdateDownloadService; diff --git a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateInstallNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateInstallNotification.java index cf07a91a8e..fe4a836bed 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateInstallNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateInstallNotification.java @@ -4,8 +4,8 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import me.devsaki.hentoid.R; import me.devsaki.hentoid.util.notification.Notification; diff --git a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateNotificationChannel.java b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateNotificationChannel.java index ff6ecbef8d..a41dcf2df0 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateNotificationChannel.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateNotificationChannel.java @@ -9,14 +9,20 @@ public class UpdateNotificationChannel { - static final String ID = "update"; + private static final String ID_OLD = "update"; + static final String ID = "update2"; public static void init(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { String name = "App updates"; - int importance = NotificationManager.IMPORTANCE_DEFAULT; + int importance = NotificationManager.IMPORTANCE_MIN; NotificationChannel channel = new NotificationChannel(ID, name, importance); + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + + // Mandatory; it is not possible to change the sound of an existing channel after its initial creation + notificationManager.deleteNotificationChannel(ID_OLD); + Objects.requireNonNull(notificationManager).createNotificationChannel(channel); } } diff --git a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateProgressNotification.java b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateProgressNotification.java index 5a24309e97..9bf64c6308 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateProgressNotification.java +++ b/app/src/main/java/me/devsaki/hentoid/notification/update/UpdateProgressNotification.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.notification.update; import android.content.Context; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import me.devsaki.hentoid.R; import me.devsaki.hentoid.util.notification.Notification; @@ -24,6 +24,7 @@ public android.app.Notification onCreateNotification(Context context) { .setSmallIcon(R.drawable.ic_stat_hentoid) .setContentTitle("Downloading update") .setProgress(100, progress, progress == INDETERMINATE) + .setOnlyAlertOnce(true) .build(); } } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/ASMHentaiParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/ASMHentaiParser.java index be2f071e3a..f0ae0293a8 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/ASMHentaiParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/ASMHentaiParser.java @@ -18,7 +18,7 @@ protected List parseImages(Content content) throws IOException { Document doc = getOnlineDocument(content.getReaderUrl()); if (doc != null) { - String imgUrl = "http:" + + String imgUrl = "https:" + doc.select("div.full_gallery") .select("a") .select("img") diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/BaseParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/BaseParser.java index edfb8cb4ae..e9affd4eb0 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/BaseParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/BaseParser.java @@ -2,33 +2,46 @@ import android.webkit.URLUtil; -import java.io.IOException; -import java.util.Collections; import java.util.List; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.database.domains.ImageFile; import timber.log.Timber; -public abstract class BaseParser implements ContentParser { +public abstract class BaseParser implements ImageListParser { + + private int currentStep; + private int maxSteps; protected abstract List parseImages(Content content) throws Exception; public List parseImageList(Content content) throws Exception { String readerUrl = content.getReaderUrl(); - List images = Collections.emptyList(); - if (!URLUtil.isValidUrl(readerUrl)) { - throw new Exception("Invalid gallery URL : " + readerUrl); - } + if (!URLUtil.isValidUrl(readerUrl)) + throw new IllegalArgumentException("Invalid gallery URL : " + readerUrl); + Timber.d("Gallery URL: %s", readerUrl); List imgUrls = parseImages(content); - images = ParseHelper.urlsToImageFiles(imgUrls); + List images = ParseHelper.urlsToImageFiles(imgUrls); Timber.d("%s", images); return images; } + void progressStart(int maxSteps) { + currentStep = 0; + this.maxSteps = maxSteps; + ParseHelper.signalProgress(currentStep, maxSteps); + } + + void progressPlus() { + ParseHelper.signalProgress(++currentStep, maxSteps); + } + + void progressComplete() { + ParseHelper.signalProgress(maxSteps, maxSteps); + } } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/ContentParserFactory.java b/app/src/main/java/me/devsaki/hentoid/parsers/ContentParserFactory.java index 53682fbcf7..2ee4746aa1 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/ContentParserFactory.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/ContentParserFactory.java @@ -2,6 +2,16 @@ import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.parsers.content.ASMHentaiContent; +import me.devsaki.hentoid.parsers.content.DummyContent; +import me.devsaki.hentoid.parsers.content.FakkuContent; +import me.devsaki.hentoid.parsers.content.HentaiCafeContent; +import me.devsaki.hentoid.parsers.content.HitomiContent; +import me.devsaki.hentoid.parsers.content.MusesContent; +import me.devsaki.hentoid.parsers.content.NexusContent; +import me.devsaki.hentoid.parsers.content.NhentaiContent; +import me.devsaki.hentoid.parsers.content.PururinContent; +import me.devsaki.hentoid.parsers.content.TsuminoContent; public class ContentParserFactory { @@ -14,11 +24,39 @@ public static ContentParserFactory getInstance() { return mInstance; } - public ContentParser getParser(Content content) { - return (null == content) ? new DummyParser() : getParser(content.getSite()); + + public Class getContentParserClass(Site site) { + switch (site) { + case NHENTAI: + return NhentaiContent.class; + case ASMHENTAI: + case ASMHENTAI_COMICS: + return ASMHentaiContent.class; + case HENTAICAFE: + return HentaiCafeContent.class; + case HITOMI: + return HitomiContent.class; + case TSUMINO: + return TsuminoContent.class; + case PURURIN: + return PururinContent.class; + case FAKKU2: + return FakkuContent.class; + case NEXUS: + return NexusContent.class; + case MUSES: + return MusesContent.class; + case EHENTAI: // E-H uses the API of the site -> no HTML parser + default: + return DummyContent.class; + } + } + + public ImageListParser getImageListParser(Content content) { + return (null == content) ? new DummyParser() : getImageListParser(content.getSite()); } - private ContentParser getParser(Site site) { + private ImageListParser getImageListParser(Site site) { switch (site) { case ASMHENTAI: case ASMHENTAI_COMICS: @@ -33,10 +71,12 @@ private ContentParser getParser(Site site) { return new PururinParser(); case EHENTAI: return new EHentaiParser(); - case PANDA: - return new PandaParser(); case FAKKU2: return new FakkuParser(); + case NEXUS: + return new NexusParser(); + case MUSES: // No image parser; images are fetched by ContentParser + case NHENTAI: default: return new DummyParser(); } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/DummyParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/DummyParser.java index ea7636bfc4..1d1bbf2334 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/DummyParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/DummyParser.java @@ -6,7 +6,7 @@ import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.database.domains.ImageFile; -public class DummyParser implements ContentParser { +public class DummyParser implements ImageListParser { @Override public List parseImageList(Content content) { return Collections.emptyList(); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/EHentaiParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/EHentaiParser.java index d675f5555d..cd81ec623d 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/EHentaiParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/EHentaiParser.java @@ -11,6 +11,7 @@ import java.util.List; import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.util.HttpHelper; import static me.devsaki.hentoid.util.HttpHelper.getOnlineDocument; @@ -31,15 +32,17 @@ protected List parseImages(Content content) throws IOException { // 1- Detect the number of pages of the gallery Element e; List> headers = new ArrayList<>(); - headers.add(new Pair<>("cookie", "nw=1")); // nw=1 (always) avoids the Offensive Content popup (equivalent to clicking the "Never warn me again" link) + headers.add(new Pair<>(HttpHelper.HEADER_COOKIE_KEY, "nw=1")); // nw=1 (always) avoids the Offensive Content popup (equivalent to clicking the "Never warn me again" link) Document doc = getOnlineDocument(content.getGalleryUrl(), headers, true); if (doc != null) { Elements elements = doc.select("table.ptt a"); - if (null == elements || 0 == elements.size()) return result; + if (null == elements || elements.isEmpty()) return result; int tabId = (1 == elements.size()) ? 0 : elements.size() - 2; int nbGalleryPages = Integer.parseInt(elements.get(tabId).text()); + progressStart(nbGalleryPages + content.getQtyPages()); + // 2- Browse the gallery and fetch the URL for every page (since all of them have a different temporary key...) List pageUrls = new ArrayList<>(); @@ -49,6 +52,7 @@ protected List parseImages(Content content) throws IOException { for (int i = 1; i < nbGalleryPages; i++) { doc = getOnlineDocument(content.getGalleryUrl() + "/?p=" + i, headers, true); if (doc != null) fetchPageUrls(doc, pageUrls); + progressPlus(); } } @@ -57,13 +61,15 @@ protected List parseImages(Content content) throws IOException { doc = getOnlineDocument(s, headers, true); if (doc != null) { elements = doc.select("img#img"); - if (elements != null && elements.size() > 0) { + if (elements != null && !elements.isEmpty()) { e = elements.first(); result.add(e.attr("src")); } } + progressPlus(); } } + progressComplete(); return result; } @@ -76,4 +82,5 @@ private void fetchPageUrls(Document doc, List pageUrls) { pageUrls.add(e.attr("href")); } } + } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/FakkuParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/FakkuParser.java index fcc17003c6..b06dcb5f40 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/FakkuParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/FakkuParser.java @@ -27,7 +27,10 @@ * Created by robb_w on 2019/02 * Handles parsing of content from Fakku */ -public class FakkuParser implements ContentParser { +public class FakkuParser implements ImageListParser { + + private int currentStep; + private int maxSteps; public List parseImageList(Content content) { @@ -42,15 +45,15 @@ public List parseImageList(Content content) { }.getType(); Map downloadParams = new Gson().fromJson(downloadParamsStr, type); - if (!downloadParams.containsKey("cookie")) { + if (!downloadParams.containsKey(HttpHelper.HEADER_COOKIE_KEY)) { Timber.e("Download parameters do not contain any cookie"); return result; } List> headers = new ArrayList<>(); - headers.add(new Pair<>("cookie", downloadParams.get("cookie"))); + headers.add(new Pair<>(HttpHelper.HEADER_COOKIE_KEY, downloadParams.get(HttpHelper.HEADER_COOKIE_KEY))); String readUrl = content.getGalleryUrl().replace("www", "books") + "/read"; - FakkuGalleryMetadata info = null; + FakkuGalleryMetadata info; try { info = HttpHelper.getOnlineJson(readUrl, headers, false, FakkuGalleryMetadata.class); } catch (IOException e) { @@ -63,17 +66,22 @@ public List parseImageList(Content content) { return result; } + progressStart(info.pages.keySet().size() + 1); + // Add referer information to downloadParams for future image download - downloadParams.put("referer", content.getReaderUrl()); + downloadParams.put(HttpHelper.HEADER_REFERER_KEY, content.getReaderUrl()); // Process book info to get page detailed info String pid = null; - String[] cookieContent = downloadParams.get("cookie").split(";"); - for (String s : cookieContent) { - String[] cookieParts = s.split("="); - if (cookieParts[0].toLowerCase().trim().equals("fakku_zid")) { - pid = cookieParts[1]; - break; + String cookie = downloadParams.get(HttpHelper.HEADER_COOKIE_KEY); + if (cookie != null) { + String[] cookieContent = cookie.split(";"); + for (String s : cookieContent) { + String[] cookieParts = s.split("="); + if (cookieParts[0].toLowerCase().trim().equals("fakku_zid")) { + pid = cookieParts[1]; + break; + } } } if (null == pid) { @@ -81,7 +89,10 @@ public List parseImageList(Content content) { return result; } - List pageInfo = FakkuDecode.getBookPageData(info.key_hash, Helper.decode64(info.key_data), pid, BuildConfig.FK_TOKEN); + List pageInfo = null; + if (info.key_data != null) + pageInfo = FakkuDecode.getBookPageData(info.key_hash, Helper.decode64(info.key_data), pid, BuildConfig.FK_TOKEN); + progressPlus(); result = new ArrayList<>(); for (String p : info.pages.keySet()) { @@ -89,14 +100,35 @@ public List parseImageList(Content content) { FakkuGalleryMetadata.FakkuPage page = info.pages.get(p); ImageFile img = ParseHelper.urlToImageFile(page.image, order); - downloadParams.put("pageInfo", JsonHelper.serializeToJson(pageInfo.get(order - 1))); // String contains JSON data within a JSON... + String pageInfoValue; + if (pageInfo != null) pageInfoValue = JsonHelper.serializeToJson(pageInfo.get(order - 1)); // String contains JSON data within a JSON... + else pageInfoValue = "unprotected"; + + downloadParams.put("pageInfo", pageInfoValue); downloadParamsStr = JsonHelper.serializeToJson(downloadParams); img.setDownloadParams(downloadParamsStr); result.add(img); + progressPlus(); } Timber.d("%s", result); + progressComplete(); + return result; } + + private void progressStart(int maxSteps) { + currentStep = 0; + this.maxSteps = maxSteps; + ParseHelper.signalProgress(currentStep, maxSteps); + } + + private void progressPlus() { + ParseHelper.signalProgress(++currentStep, maxSteps); + } + + private void progressComplete() { + ParseHelper.signalProgress(maxSteps, maxSteps); + } } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/HentaiCafeParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/HentaiCafeParser.java index b9616c2ad4..ba22e76dce 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/HentaiCafeParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/HentaiCafeParser.java @@ -27,77 +27,74 @@ public class HentaiCafeParser extends BaseParser { @Override - protected List parseImages(Content content) throws IOException { + protected List parseImages(Content content) throws Exception { List result = new ArrayList<>(); - - Document doc = getOnlineDocument(content.getGalleryUrl()); - if (doc != null) { - Elements links = doc.select("a.x-btn"); - - Elements contents; - Element js; - int pages = 0; - - if (links != null) { - if (links.size() > 1) { - Timber.d("Multiple chapters found!"); + String pageUrl = content.getGalleryUrl(); + int pages = 0; + + Document doc = getOnlineDocument(pageUrl); + if (null == doc) throw new Exception("Document unreachable : " + pageUrl); + + Timber.d("Parsing: %s", pageUrl); + Elements links = doc.select("a.x-btn"); + if (links.isEmpty()) throw new Exception("No links found @ " + pageUrl); + + if (links.size() > 1) Timber.d("Multiple chapters found!"); + progressStart(links.size()); + + for (Element link : links) { + String url = link.attr("href"); + // Reconstitute the reader URL piece by piece if needed + // NB : some pages require it (e.g. 2606) + if (url.equals("#") && doc != null) { + // Get the canonical link + Elements canonicalLink = doc.select("head [rel=canonical]"); + if (canonicalLink != null) { + // Remove artist name + String artist = content.getAuthor().replace(" ", "-").toLowerCase() + "-"; + String canonicalUrl = canonicalLink.get(0).attr("href").replace(artist, ""); + // Get the last part + String[] parts = canonicalUrl.split("/"); + String canonicalName = parts[parts.length - 1].replace("-", "_"); + url = content.getReaderUrl().replace("$1", canonicalName); // $1 has to be replaced by the textual unique site ID without the author name } + } - for (int i = 0; i < links.size(); i++) { - - String url = links.get(i).attr("href"); - if (url.equals("#") && doc != null) { // Some pages are like this (e.g. 2606) -> reconstitute the reader URL manually - // Get the canonical link - Elements canonicalLink = doc.select("head [rel=canonical]"); - if (canonicalLink != null) { - // Remove artist name - String artist = content.getAuthor().replace(" ","-").toLowerCase()+"-"; - String canonicalUrl = canonicalLink.get(0).attr("href").replace(artist,""); - // Get the last part - String[] parts = canonicalUrl.split("/"); - String canonicalName = parts[parts.length - 1].replace("-","_"); - url = content.getReaderUrl().replace("$1", canonicalName); // $1 has to be replaced by the textual unique site ID without the author name - } - } - - if (URLUtil.isValidUrl(url)) { - Timber.d("Chapter Links: %s", url); - try { -// doc = Jsoup.connect(links.get(i).attr("href")).timeout(TIMEOUT).get(); - doc = getOnlineDocument(url); - if (doc != null) { - contents = doc.select("article#content"); - js = contents.select("script").last(); - - if (contents.size() > 0) { - pages += Integer.parseInt( - doc.select("div.text").first().text().replace(" ⤵", "")); - Timber.d("Pages: %s", pages); - - JSONArray array = getJSONArrayFromString(js.toString()); - if (array != null) { - for (int j = 0; j < array.length(); j++) { - try { - result.add(array.getJSONObject(j).getString("url")); - } catch (JSONException e) { - Timber.e(e, "Error while reading from array"); - } - } - } else { - Timber.e("Error while parsing pages"); - } + if (URLUtil.isValidUrl(url)) { + Timber.d("Chapter Links: %s", url); + try { + doc = getOnlineDocument(url); + if (doc != null) { + Elements scripts = doc.select("article#content").select("script"); + for (Element script : scripts) { + String scriptStr = script.toString(); + if (scriptStr.contains("\"created\"")) { // That's the one + JSONArray array = getJSONArrayFromString(scriptStr); + if (array != null) { + for (int i = 0; i < array.length(); i++) + result.add(array.getJSONObject(i).getString("url")); + pages += array.length(); + } else { + Timber.e("Error while parsing pages"); } + break; } - } catch (IOException e) { - Timber.e(e, "JSOUP Error"); } } + } catch (JSONException e) { + Timber.e(e, "Error while reading from array"); + } catch (IOException e) { + Timber.e(e, "JSOUP Error"); } - Timber.d("Total Pages: %s", pages); - content.setQtyPages(pages); } + progressPlus(); } + Timber.d("Total Pages: %s", pages); + content.setQtyPages(pages); + + progressComplete(); + return result; } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/HitomiParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/HitomiParser.java index 06c17ed0e1..9398280d22 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/HitomiParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/HitomiParser.java @@ -11,6 +11,7 @@ import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.database.domains.ImageFile; +import me.devsaki.hentoid.util.HttpHelper; import me.devsaki.hentoid.util.JsonHelper; import timber.log.Timber; @@ -20,47 +21,41 @@ * Created by neko on 08/07/2015. * Handles parsing of content from hitomi.la */ -public class HitomiParser implements ContentParser { +public class HitomiParser implements ImageListParser { // Reproduction of the Hitomi.la Javascript to find the hostname of the image server (see subdomain_from_url@reader.js) - private final static int NUMBER_OF_FRONTENDS = 2; - private final static String HOSTNAME_SUFFIX = "a"; - private final static char HOSTNAME_PREFIX_BASE = 97; + private static final int NUMBER_OF_FRONTENDS = 2; + private static final String HOSTNAME_SUFFIX = "a"; + private static final char HOSTNAME_PREFIX_BASE = 97; public List parseImageList(Content content) throws Exception { List result = new ArrayList<>(); + String pageUrl = content.getReaderUrl(); - Document doc = getOnlineDocument(content.getReaderUrl()); - if (doc != null) { - Timber.d("Parsing: %s", content.getReaderUrl()); - Elements imgElements = doc.select(".img-url"); + Document doc = getOnlineDocument(pageUrl); + if (null == doc) throw new Exception("Document unreachable : " + pageUrl); - if (null == imgElements || 0 == imgElements.size()) { - Timber.w("No images found @ %s", content.getReaderUrl()); - return result; - } + Timber.d("Parsing: %s", pageUrl); + Elements imgElements = doc.select(".img-url"); + if (imgElements.isEmpty()) throw new Exception("No images found @ " + pageUrl); - // New Hitomi image URLs starting from june 2018 - // If book ID is even, starts with 'aa'; else starts with 'ba' - int referenceId = Integer.parseInt(content.getUniqueSiteId()) % 10; - if (1 == referenceId) - referenceId = 0; // Yes, this is what Hitomi actually does (see common.js) - String imageSubdomain = Character.toString((char) (HOSTNAME_PREFIX_BASE + (referenceId % NUMBER_OF_FRONTENDS))) + HOSTNAME_SUFFIX; + // New Hitomi image URLs starting from june 2018 + // If book ID is even, starts with 'aa'; else starts with 'ba' + int referenceId = Integer.parseInt(content.getUniqueSiteId()) % 10; + if (1 == referenceId) + referenceId = 0; // Yes, this is what Hitomi actually does (see common.js) + String imageSubdomain = ((char) (HOSTNAME_PREFIX_BASE + (referenceId % NUMBER_OF_FRONTENDS))) + HOSTNAME_SUFFIX; - Map downloadParams = new HashMap<>(); - // Add referer information to downloadParams for future image download - downloadParams.put("referer", content.getReaderUrl()); - String downloadParamsStr = JsonHelper.serializeToJson(downloadParams); + Map downloadParams = new HashMap<>(); + // Add referer information to downloadParams for future image download + downloadParams.put(HttpHelper.HEADER_REFERER_KEY, pageUrl); + String downloadParamsStr = JsonHelper.serializeToJson(downloadParams); - int order = 1; - for (Element element : imgElements) { - ImageFile img = ParseHelper.urlToImageFile("https:" + replaceSubdomainWith(element.text(), imageSubdomain), order++); - img.setDownloadParams(downloadParamsStr); - result.add(img); - } - } else { - Timber.w("Document null @ %s", content.getReaderUrl()); - throw new Exception("Document null @ " + content.getReaderUrl()); + int order = 1; + for (Element element : imgElements) { + ImageFile img = ParseHelper.urlToImageFile("https:" + replaceSubdomainWith(element.text(), imageSubdomain), order++); + img.setDownloadParams(downloadParamsStr); + result.add(img); } return result; diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/ContentParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/ImageListParser.java similarity index 87% rename from app/src/main/java/me/devsaki/hentoid/parsers/ContentParser.java rename to app/src/main/java/me/devsaki/hentoid/parsers/ImageListParser.java index 70c3ced6af..45d74e7dc5 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/ContentParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/ImageListParser.java @@ -5,6 +5,6 @@ import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.database.domains.ImageFile; -public interface ContentParser { +public interface ImageListParser { List parseImageList(Content content) throws Exception; } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/NexusParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/NexusParser.java new file mode 100644 index 0000000000..ab03ff8abf --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/parsers/NexusParser.java @@ -0,0 +1,43 @@ +package me.devsaki.hentoid.parsers; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.util.Helper; + +import static me.devsaki.hentoid.util.HttpHelper.getOnlineDocument; + +public class NexusParser extends BaseParser { + + @Override + protected List parseImages(Content content) throws IOException { + List result = new ArrayList<>(); + + progressStart(content.getQtyPages()); + /* + * Open all pages and grab the URL of the displayed image + */ + for (int i = 0; i < content.getQtyPages(); i++) { + String readerUrl = content.getReaderUrl().replace("001", Helper.formatIntAsStr(i + 1, 3)); + Document doc = getOnlineDocument(readerUrl); + if (doc != null) { + Elements elements = doc.select("a img"); + if (elements != null && !elements.isEmpty()) { + Element e = elements.first(); + result.add(e.attr("src")); + } + } + progressPlus(); + } + + progressComplete(); + + return result; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/PandaParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/PandaParser.java deleted file mode 100644 index e92cb5577f..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/parsers/PandaParser.java +++ /dev/null @@ -1,46 +0,0 @@ -package me.devsaki.hentoid.parsers; - -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import me.devsaki.hentoid.database.domains.Content; - -import static me.devsaki.hentoid.util.HttpHelper.getOnlineDocument; - -public class PandaParser extends BaseParser { - - @Override - protected List parseImages(Content content) throws IOException { - List result = new ArrayList<>(); - - String pageUrl; - for (int i = 0; i < content.getQtyPages(); i++) { - pageUrl = content.getReaderUrl() + "/" + i; - Document doc = getOnlineDocument(pageUrl); - if (doc != null) { - Elements scripts = doc.head().select("script"); - for (Element e : scripts) { - if (e.toString().contains("document['pu']")) // That's the one - { - Pattern pattern = Pattern.compile("document\\['pu'\\] = '(.+)'"); - Matcher matcher = pattern.matcher(e.toString()); - - if (matcher.find() && matcher.groupCount() > 0) { - result.add(matcher.group(1)); - } - break; - } - } - } - } - - return result; - } -} 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 6bc554cd85..7dfdb1bcaa 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/ParseHelper.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/ParseHelper.java @@ -1,5 +1,6 @@ package me.devsaki.hentoid.parsers; +import org.greenrobot.eventbus.EventBus; import org.jsoup.nodes.Element; import java.util.ArrayList; @@ -13,6 +14,7 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; +import me.devsaki.hentoid.events.DownloadPreparationEvent; import me.devsaki.hentoid.util.AttributeMap; public class ParseHelper { @@ -23,7 +25,7 @@ public class ParseHelper { * @return String with removed brackets */ public static String removeBrackets(String s) { - int bracketPos = s.lastIndexOf("("); + int bracketPos = s.lastIndexOf('('); if (bracketPos > 1 && ' ' == s.charAt(bracketPos - 1)) bracketPos--; if (bracketPos > -1) { return s.substring(0, bracketPos); @@ -34,13 +36,15 @@ public static String removeBrackets(String s) { public static void parseAttributes(AttributeMap map, AttributeType type, List elements, boolean filterCount, Site site) { if (elements != null) - for (Element a : elements) { - String name = a.text(); - if (filterCount) name = removeBrackets(name); - Attribute attribute = new Attribute(type, name, a.attr("href"), site); + for (Element a : elements) parseAttribute(map, type, a, filterCount, site); + } + + public static void parseAttribute(AttributeMap map, AttributeType type, Element element, boolean filterCount, Site site) { + String name = element.text(); + if (filterCount) name = removeBrackets(name); + Attribute attribute = new Attribute(type, name, element.attr("href"), site); - map.add(attribute); - } + map.add(attribute); } static ImageFile urlToImageFile(@Nonnull String imgUrl, int order) { @@ -60,4 +64,8 @@ static List urlsToImageFiles(@Nonnull List imgUrls) { return result; } + + static void signalProgress(int current, int max) { + EventBus.getDefault().post(new DownloadPreparationEvent(current, max)); + } } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/PururinParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/PururinParser.java index f090b08647..53c8e31ea6 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/PururinParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/PururinParser.java @@ -5,6 +5,7 @@ import org.jsoup.nodes.Document; +import java.io.File; import java.util.ArrayList; import java.util.List; @@ -18,7 +19,7 @@ */ public class PururinParser extends BaseParser { - private final static String IMAGE_PATH = "//cdn.pururin.io/assets/images/data/"; + private static final String IMAGE_PATH = "//cdn.pururin.io/assets/images/data/"; private class PururinInfo { @Expose @@ -46,7 +47,7 @@ protected List parseImages(Content content) throws Exception { // 2- Get imagePath from app.js => it is constant anyway, and app.js is 3 MB long => put it there as a const for (int i = 0; i < content.getQtyPages(); i++) { - result.add(protocol + IMAGE_PATH + info.id + "/" + (i + 1) + "." + info.image_extension); + result.add(protocol + IMAGE_PATH + info.id + File.separator + (i + 1) + "." + info.image_extension); } } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/TsuminoParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/TsuminoParser.java index 39662a04b3..b8601b5e3d 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/TsuminoParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/TsuminoParser.java @@ -19,6 +19,7 @@ import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -26,6 +27,7 @@ import java.util.Map; import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.util.HttpHelper; import timber.log.Timber; import static me.devsaki.hentoid.enums.Site.TSUMINO; @@ -47,7 +49,9 @@ protected List parseImages(Content content) throws Exception { Elements contents = doc.select("#image-container"); if (null != contents) { - String dataUrl, dataOpt, dataObj; + String dataUrl; + String dataOpt; + String dataObj; dataUrl = contents.attr("data-url"); dataOpt = contents.attr("data-opt"); @@ -75,22 +79,22 @@ private static String sendPostRequest(String dataUrl, String dataOpt) { HttpURLConnection http = null; try { - http = (HttpURLConnection) ((new URL(url).openConnection())); + http = (HttpURLConnection) (new URL(url).openConnection()); http.setDoOutput(true); http.setRequestProperty("Content-Type", "application/json"); http.setRequestProperty("Accept", "application/json"); - http.setRequestProperty("Cookie", cookie); + http.setRequestProperty(HttpHelper.HEADER_COOKIE_KEY, cookie); http.setRequestMethod("POST"); http.connect(); - try (OutputStream stream = http.getOutputStream(); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream, "UTF-8"))) { + try (OutputStream stream = http.getOutputStream(); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8))) { writer.write(dataJson); } StringBuilder builder = new StringBuilder(); String line; try (BufferedReader br = new BufferedReader(new InputStreamReader( - http.getInputStream(), "UTF-8"))) { + http.getInputStream(), StandardCharsets.UTF_8))) { while ((line = br.readLine()) != null) builder.append(line); } return builder.toString(); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/ASMHentaiContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/ASMHentaiContent.java index 370fff58c7..ab73df8e0e 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/ASMHentaiContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/ASMHentaiContent.java @@ -4,19 +4,22 @@ import java.util.List; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.ParseHelper; import me.devsaki.hentoid.util.AttributeMap; import pl.droidsonroids.jspoon.annotation.Selector; -public class ASMHentaiContent { - @Selector(value = "head [rel=canonical]", attr="href", defValue = "") +public class ASMHentaiContent implements ContentParser { + @Selector(value = "head [rel=canonical]", attr = "href", defValue = "") private String pageUrl; - @Selector(value = "div.cover a", attr="href", defValue = "") + @Selector(value = "div.cover a", attr = "href", defValue = "") private String galleryUrl; - @Selector(value = "div.cover a img", attr="src") + @Selector(value = "div.cover a img", attr = "src") private String coverUrl; @Selector("div.info h1:first-child") private String title; @@ -34,21 +37,18 @@ public class ASMHentaiContent { private List languages; - private String getProtocol() - { - return pageUrl.startsWith("https") ? "https" : "http"; - } - - public Content toContent() - { + public Content toContent(@Nonnull String url) { Content result = new Content(); - if (pageUrl.isEmpty()) return result.setSite(Site.ASMHENTAI); + String theUrl = pageUrl.isEmpty() ? url : pageUrl; + if (theUrl.isEmpty()) + return result.setSite(Site.ASMHENTAI).setStatus(StatusContent.IGNORED); - result.setSite(pageUrl.toLowerCase().contains("comics") ? Site.ASMHENTAI_COMICS : Site.ASMHENTAI); - if (galleryUrl.isEmpty()) return result; + result.setSite(theUrl.toLowerCase().contains("comics") ? Site.ASMHENTAI_COMICS : Site.ASMHENTAI); + if (galleryUrl.isEmpty()) return result.setStatus(StatusContent.IGNORED); result.setUrl(galleryUrl.substring(0, galleryUrl.length() - 2).replace("/gallery", "")); - result.setCoverImageUrl(getProtocol() + "://"+ coverUrl); + result.setCoverImageUrl("https:" + coverUrl); + result.setTitle(title); result.setQtyPages(Integer.parseInt(pages.get(0).replace("Pages: ", ""))); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/ContentParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/ContentParser.java new file mode 100644 index 0000000000..e61e755ec8 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/ContentParser.java @@ -0,0 +1,9 @@ +package me.devsaki.hentoid.parsers.content; + +import javax.annotation.Nonnull; + +import me.devsaki.hentoid.database.domains.Content; + +public interface ContentParser { + Content toContent(@Nonnull String url); +} diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/DummyContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/DummyContent.java new file mode 100644 index 0000000000..c04803561d --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/DummyContent.java @@ -0,0 +1,24 @@ +package me.devsaki.hentoid.parsers.content; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.enums.StatusContent; +import pl.droidsonroids.jspoon.annotation.Selector; + +public class DummyContent implements ContentParser { + + @Selector("div.info h1:first-child") + private String title; + + @Nullable + public Content toContent(@Nonnull String url) { + Content result = new Content(); + + result.setSite(Site.NONE).setStatus(StatusContent.IGNORED); + + return result; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/EHentaiGalleriesMetadata.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/EHentaiGalleriesMetadata.java index d9b4a47134..1745340ae9 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/EHentaiGalleriesMetadata.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/EHentaiGalleriesMetadata.java @@ -4,15 +4,16 @@ import java.util.List; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.database.domains.Content; public class EHentaiGalleriesMetadata { @Expose - public List gmetadata; + private List gmetadata; - public Content toContent() - { - return (gmetadata != null && gmetadata.size() > 0) ? gmetadata.get(0).toContent() : new Content(); + public Content toContent(@Nonnull String url) { + return (gmetadata != null && !gmetadata.isEmpty()) ? gmetadata.get(0).toContent(url) : new Content(); } } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/EHentaiGalleryMetadata.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/EHentaiGalleryMetadata.java index e51ef80b9f..db5a754a85 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/EHentaiGalleryMetadata.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/EHentaiGalleryMetadata.java @@ -4,6 +4,8 @@ import java.util.List; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.database.domains.Attribute; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.AttributeType; @@ -45,7 +47,7 @@ public class EHentaiGalleryMetadata { public List tags; - public Content toContent() { + public Content toContent(@Nonnull String url) { Content result = new Content(); result.setSite(Site.EHENTAI); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/FakkuContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/FakkuContent.java index 602b907a55..9abaa21087 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/FakkuContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/FakkuContent.java @@ -4,16 +4,18 @@ import java.util.List; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.ParseHelper; import me.devsaki.hentoid.util.AttributeMap; import pl.droidsonroids.jspoon.annotation.Selector; -public class FakkuContent { +public class FakkuContent implements ContentParser { @Selector(value = "head [property=og:url]", attr = "content", defValue = "") private String galleryUrl; @Selector(value = "head [property=og:image]", attr = "content") @@ -32,21 +34,22 @@ public class FakkuContent { private List greenButton; @Nullable - public Content toContent() { + public Content toContent(@Nonnull String url) { if (greenButton != null) { // Check if book is available for (Element e : greenButton) { if (e.text().toLowerCase().contains("subscribe") || e.text().toLowerCase().contains("purchase")) - return null; + return new Content().setStatus(StatusContent.IGNORED); } } Content result = new Content(); result.setSite(Site.FAKKU2); - if (galleryUrl.isEmpty()) return result; + String theUrl = galleryUrl.isEmpty() ? url : galleryUrl; + if (theUrl.isEmpty()) return result.setStatus(StatusContent.IGNORED); - result.setUrl(galleryUrl.replace(Site.FAKKU2.getUrl() + "/hentai/", "")); + result.setUrl(theUrl.replace(Site.FAKKU2.getUrl() + "/hentai/", "")); result.setCoverImageUrl(coverUrl); result.setTitle(title); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/HentaiCafeContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/HentaiCafeContent.java index 9021edd864..8322bcba30 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/HentaiCafeContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/HentaiCafeContent.java @@ -4,14 +4,17 @@ import java.util.List; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.ParseHelper; import me.devsaki.hentoid.util.AttributeMap; import pl.droidsonroids.jspoon.annotation.Selector; -public class HentaiCafeContent { +public class HentaiCafeContent implements ContentParser { @Selector(value = "div.x-main.full article", attr = "id", defValue = "") private String galleryUrl; @Selector(value = "div.entry-content img", attr = "src") @@ -24,13 +27,14 @@ public class HentaiCafeContent { private List tags; - public Content toContent() { + public Content toContent(@Nonnull String url) { Content result = new Content(); result.setSite(Site.HENTAICAFE); - if (galleryUrl.isEmpty()) return result; + String theUrl = galleryUrl.isEmpty() ? url : galleryUrl; + if (theUrl.isEmpty()) return result.setStatus(StatusContent.IGNORED); - result.setUrl(galleryUrl.replace("post-", "/?p=")); + result.setUrl(theUrl.replace("post-", "/?p=")); String coverUrl = coverImg.attr("src"); if (coverUrl.isEmpty()) coverUrl = coverImg.attr("data-cfsrc"); // Cloudflare-served image diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/HitomiContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/HitomiContent.java index 81be2f5cfe..624b38c5a4 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/HitomiContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/HitomiContent.java @@ -4,14 +4,17 @@ import java.util.List; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.ParseHelper; import me.devsaki.hentoid.util.AttributeMap; import pl.droidsonroids.jspoon.annotation.Selector; -public class HitomiContent { +public class HitomiContent implements ContentParser { @Selector(value = "h1 a[href*='/reader/']", attr="href", defValue = "") private String galleryUrl; @Selector(value = ".cover img", attr="src") @@ -36,13 +39,14 @@ public class HitomiContent { private List categories; - public Content toContent() - { + public Content toContent(@Nonnull String url) { Content result = new Content(); - if (galleryUrl.isEmpty()) return result; + + String theUrl = galleryUrl.isEmpty() ? url : galleryUrl; + if (theUrl.isEmpty()) return result.setStatus(StatusContent.IGNORED); result.setSite(Site.HITOMI); - result.setUrl(galleryUrl.replace("/reader", "")); + result.setUrl(theUrl.replace("/reader", "")); result.setCoverImageUrl("https:"+ coverUrl); result.setTitle(title); result.setQtyPages(pages.size()); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/MusesContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/MusesContent.java new file mode 100644 index 0000000000..8439eb7c87 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/MusesContent.java @@ -0,0 +1,144 @@ +package me.devsaki.hentoid.parsers.content; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.database.domains.ImageFile; +import me.devsaki.hentoid.enums.AttributeType; +import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.enums.StatusContent; +import me.devsaki.hentoid.parsers.ParseHelper; +import me.devsaki.hentoid.util.AttributeMap; +import me.devsaki.hentoid.util.Helper; +import me.devsaki.hentoid.util.HttpHelper; +import pl.droidsonroids.jspoon.annotation.Selector; +import timber.log.Timber; + +public class MusesContent implements ContentParser { + @Selector(value = "head [rel=canonical]", attr = "href", defValue = "") + private String galleryUrl; + @Selector(value = ".top-menu-breadcrumb a") + private List breadcrumbs; + @Selector(value = ".gallery img", attr = "data-src", defValue = "") + private List thumbs; + @Selector(value = ".gallery a", attr = "href", defValue = "") + private List thumbLinks; + + private static final List nonLegitPublishers = new ArrayList<>(); + private static final List publishersWithAuthors = new ArrayList<>(); + + static { + nonLegitPublishers.add("various authors"); + nonLegitPublishers.add("hentai and manga english"); + + publishersWithAuthors.add("various authors"); + publishersWithAuthors.add("fakku comics"); + publishersWithAuthors.add("hentai and manga english"); + publishersWithAuthors.add("renderotica comics"); + publishersWithAuthors.add("tg comics"); + publishersWithAuthors.add("affect3d comics"); + publishersWithAuthors.add("johnpersons.com comics"); + } + + @Nullable + public Content toContent(@Nonnull String url) { + // Gallery pages are the only ones whose gallery links end with numbers + // The others are album lists + for (int i = 0; i < thumbLinks.size(); i++) { + if (!thumbLinks.get(i).endsWith("/" + (i + 1))) + return new Content().setStatus(StatusContent.IGNORED); + } + + Content result = new Content(); + + result.setSite(Site.MUSES); + String theUrl = galleryUrl.isEmpty() ? url : galleryUrl; + if (theUrl.isEmpty() || thumbs.isEmpty()) return result.setStatus(StatusContent.IGNORED); + + result.setUrl(theUrl.replace(Site.MUSES.getUrl(), "")); + result.setCoverImageUrl(Site.MUSES.getUrl() + thumbs.get(0)); + + // == Circle (publisher), Artist and Series + AttributeMap attributes = new AttributeMap(); + + if (breadcrumbs.size() > 1) { + // Default : book title is the last breadcrumb + String bookTitle = Helper.capitalizeString(breadcrumbs.get(breadcrumbs.size() - 1).text()); + + if (breadcrumbs.size() > 2) { + // Element 1 is always the publisher (using CIRCLE as publisher never appears on the Hentoid UI) + String publisher = breadcrumbs.get(1).text().toLowerCase(); + if (!nonLegitPublishers.contains(publisher)) + ParseHelper.parseAttribute(attributes, AttributeType.CIRCLE, breadcrumbs.get(1), false, Site.MUSES); + + if (breadcrumbs.size() > 3) { + // Element 2 is either the author or the series, depending on the publisher + AttributeType type = AttributeType.SERIE; + if (publishersWithAuthors.contains(publisher)) type = AttributeType.ARTIST; + ParseHelper.parseAttribute(attributes, type, breadcrumbs.get(2), false, Site.MUSES); + // Add series to book title if it isn't there already + if (AttributeType.SERIE == type) { + String series = breadcrumbs.get(2).text(); + if (!bookTitle.toLowerCase().startsWith(series.toLowerCase())) + bookTitle = series + " - " + bookTitle; + } + + if (breadcrumbs.size() > 4) { + // All that comes after element 2 contributes to the book title + boolean first = true; + StringBuilder bookTitleBuilder = new StringBuilder(); + for (int i = 3; i < breadcrumbs.size() - 1; i++) { + if (first) first = false; + else bookTitleBuilder.append(" - "); + bookTitleBuilder.append(breadcrumbs.get(i).text()); + } + bookTitle = bookTitleBuilder.toString(); + } + } + } + result.setTitle(bookTitle); + } + + + result.setQtyPages(thumbs.size()); // We infer there are as many thumbs as actual book pages on the gallery summary webpage + + String[] thumbParts; + int index = 1; + List images = new ArrayList<>(); + for (String s : thumbs) { + thumbParts = s.split("/"); + if (thumbParts.length > 3) { + thumbParts[2] = "fl"; // Large dimensions; there's also a medium variant available (fm) + String imgUrl = Site.MUSES.getUrl() + "/" + thumbParts[1] + "/" + thumbParts[2] + "/" + thumbParts[3]; + images.add(new ImageFile(index, imgUrl, StatusContent.SAVED)); // We infer actual book page images have the same format as their thumbs + index++; + } + } + result.addImageFiles(images); + + // Tags are not shown on the album page, but on the picture page (!) + try { + Document doc = HttpHelper.getOnlineDocument(Site.MUSES.getUrl() + thumbLinks.get(0)); + if (doc != null) { + Elements elements = doc.select(".album-tags a[href*='/search/tag']"); + if (!elements.isEmpty()) + ParseHelper.parseAttributes(attributes, AttributeType.TAG, elements, true, Site.MUSES); + } + } catch (IOException e) { + Timber.e(e); + } + result.addAttributes(attributes); + + + return result; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/NexusContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/NexusContent.java new file mode 100644 index 0000000000..cfc57fb2bc --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/NexusContent.java @@ -0,0 +1,63 @@ +package me.devsaki.hentoid.parsers.content; + +import org.jsoup.nodes.Element; + +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.enums.AttributeType; +import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.enums.StatusContent; +import me.devsaki.hentoid.parsers.ParseHelper; +import me.devsaki.hentoid.util.AttributeMap; +import pl.droidsonroids.jspoon.annotation.Selector; + +public class NexusContent implements ContentParser { + @Selector(value = "head [property=og:url]", attr = "content", defValue = "") + private String galleryUrl; + @Selector(value = "head [property=og:image]", attr = "content") + private String coverUrl; + @Selector("h1.title") + private String title; + @Selector("table.view-page-details") + private List information; + @Selector(value = "table.view-page-details a[href*='q=artist']") + private List artists; + @Selector(value = "table.view-page-details a[href*='q=tag']") + private List tags; + @Selector(value = "table.view-page-details a[href*='q=parody']") + private List series; + @Selector(value = "table.view-page-details a[href*='q=language']") + private List language; + @Selector(value = ".card-image img") + private List thumbs; + + @Nullable + public Content toContent(@Nonnull String url) { + Content result = new Content(); + + result.setSite(Site.NEXUS); + String theUrl = galleryUrl.isEmpty() ? url : galleryUrl; + if (theUrl.isEmpty()) return result.setStatus(StatusContent.IGNORED); + + result.setUrl(theUrl.replace(Site.NEXUS.getUrl() + "/view", "")); + result.setCoverImageUrl(coverUrl); + result.setTitle(title); + + AttributeMap attributes = new AttributeMap(); + + ParseHelper.parseAttributes(attributes, AttributeType.ARTIST, artists, true, Site.NEXUS); + ParseHelper.parseAttributes(attributes, AttributeType.TAG, tags, true, Site.NEXUS); + ParseHelper.parseAttributes(attributes, AttributeType.SERIE, series, true, Site.NEXUS); + ParseHelper.parseAttributes(attributes, AttributeType.LANGUAGE, language, true, Site.NEXUS); + + result.addAttributes(attributes); + + result.setQtyPages(thumbs.size()); // We infer there are as many thumbs as actual book pages on the gallery summary webpage + + return result; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/NhentaiContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/NhentaiContent.java index add05237b0..c941a9f2ab 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/NhentaiContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/NhentaiContent.java @@ -5,6 +5,8 @@ import java.util.ArrayList; import java.util.List; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.database.domains.ImageFile; import me.devsaki.hentoid.enums.AttributeType; @@ -16,7 +18,7 @@ import pl.droidsonroids.jspoon.annotation.Selector; // NHentai API reference : https://github.com/NHMoeDev/NHentai-android/issues/27 -public class NhentaiContent { +public class NhentaiContent implements ContentParser { @Selector(value = "#bigcontainer #cover a", attr = "href", defValue = "") private String galleryUrl; @@ -44,13 +46,14 @@ public class NhentaiContent { private List thumbs; - public Content toContent() { + public Content toContent(@Nonnull String url) { Content result = new Content(); result.setSite(Site.NHENTAI); - if (galleryUrl.isEmpty()) return result; + String theUrl = galleryUrl.isEmpty() ? url : galleryUrl; + if (theUrl.isEmpty()) return result.setStatus(StatusContent.IGNORED); - result.setUrl(galleryUrl.replace("/g", "").replace("1/", "")); + result.setUrl(theUrl.replace("/g", "").replace("1/", "")); result.setCoverImageUrl(coverUrl); result.setTitle(title); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/PandaContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/PandaContent.java deleted file mode 100644 index 85a09dadec..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/PandaContent.java +++ /dev/null @@ -1,46 +0,0 @@ -package me.devsaki.hentoid.parsers.content; - -import org.jsoup.nodes.Element; - -import java.util.List; - -import me.devsaki.hentoid.database.domains.Attribute; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.enums.AttributeType; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.util.AttributeMap; -import pl.droidsonroids.jspoon.annotation.Selector; - -public class PandaContent { - @Selector(value = "div#imgholder a img", attr = "src", defValue = "") - private String coverUrl; - @Selector("div#mangainfo div h1") - private String title; - @Selector(value = "select#pageMenu option", attr = "value") - private List pages; - @Selector("div#mangainfo div h2 a") - private Element series; - - - public Content toContent() { - Content result = new Content(); - result.setSite(Site.PANDA); - if (coverUrl.isEmpty()) return result; - - if (pages != null && pages.size() > 0) { - result.setUrl(pages.get(0)); - result.setCoverImageUrl(coverUrl); - result.setTitle(title); - String lastPage = pages.get(pages.size() - 1); - int qtyPages = Integer.parseInt(lastPage.substring(lastPage.lastIndexOf('/') + 1)); - result.setQtyPages(qtyPages); - - AttributeMap attributes = new AttributeMap(); - Attribute attribute = new Attribute(AttributeType.SERIE, series.text().substring(0, series.text().toLowerCase().lastIndexOf("manga") - 1), series.attr("href"), Site.PANDA); - attributes.add(attribute); - result.addAttributes(attributes); - } - - return result; - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/PururinContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/PururinContent.java index 809f8f206a..a0df396158 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/PururinContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/PururinContent.java @@ -4,14 +4,17 @@ import java.util.List; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.ParseHelper; import me.devsaki.hentoid.util.AttributeMap; import pl.droidsonroids.jspoon.annotation.Selector; -public class PururinContent { +public class PururinContent implements ContentParser { @Selector(value = "head [property=og:url]", attr = "content", defValue = "") private String galleryUrl; @Selector(value = "head [property=og:image]", attr = "content") @@ -40,15 +43,16 @@ private String getProtocol() { return galleryUrl.startsWith("https") ? "https" : "http"; } - public Content toContent() { + public Content toContent(@Nonnull String url) { Content result = new Content(); result.setSite(Site.PURURIN); - if (galleryUrl.isEmpty()) return result; + String theUrl = galleryUrl.isEmpty() ? url : galleryUrl; + if (theUrl.isEmpty()) return result.setStatus(StatusContent.IGNORED); - result.setUrl(galleryUrl.replace(getProtocol() + "://pururin.io/gallery", "")); + result.setUrl(theUrl.replace(getProtocol() + "://pururin.io/gallery", "")); result.setCoverImageUrl(getProtocol() + ":" + coverUrl); - result.setTitle(title.size() > 0 ? title.get(0) : ""); + result.setTitle(!title.isEmpty() ? title.get(0) : ""); int qtyPages = 0; boolean pagesFound = false; for (String s : pages) { diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/TsuminoContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/TsuminoContent.java index e89155542d..47e6e45b95 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/TsuminoContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/TsuminoContent.java @@ -4,23 +4,26 @@ import java.util.List; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.ParseHelper; import me.devsaki.hentoid.util.AttributeMap; import pl.droidsonroids.jspoon.annotation.Selector; import static me.devsaki.hentoid.enums.Site.TSUMINO; -public class TsuminoContent { +public class TsuminoContent implements ContentParser { @Selector(value = "div.book-page-cover a", attr = "href", defValue = "") private String galleryUrl; @Selector(value = "img.book-page-image", attr = "src", defValue = "") private String coverUrl; @Selector("div#Title") private String title; - @Selector(value="div#Pages", defValue = "") + @Selector(value = "div#Pages", defValue = "") private String pages; @Selector(value = "div#Artist a") private List artists; @@ -36,13 +39,14 @@ public class TsuminoContent { private List categories; - public Content toContent() { + public Content toContent(@Nonnull String url) { Content result = new Content(); result.setSite(Site.TSUMINO); - if (galleryUrl.isEmpty()) return result; + String theUrl = galleryUrl.isEmpty() ? url : galleryUrl; + if (theUrl.isEmpty()) return result.setStatus(StatusContent.IGNORED); - result.setUrl(galleryUrl.replace("/Read/View", "")); + result.setUrl(theUrl.replace("/Read/View", "")); result.setCoverImageUrl(TSUMINO.getUrl() + coverUrl); result.setTitle(title); result.setQtyPages((pages.length() > 0) ? Integer.parseInt(pages) : 0); diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/ASMComicsServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/ASMComicsServer.java deleted file mode 100644 index 21e2219b33..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/ASMComicsServer.java +++ /dev/null @@ -1,28 +0,0 @@ -package me.devsaki.hentoid.retrofit; - -import io.reactivex.Single; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.parsers.content.ASMHentaiContent; -import me.devsaki.hentoid.util.OkHttpClientSingleton; -import pl.droidsonroids.retrofit2.JspoonConverterFactory; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.http.GET; -import retrofit2.http.Path; - -public class ASMComicsServer { - - public static final Api API = new Retrofit.Builder() - .baseUrl(Site.ASMHENTAI_COMICS.getUrl()) - .client(OkHttpClientSingleton.getInstance()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - .addConverterFactory(JspoonConverterFactory.create()) - .build() - .create(Api.class); - - public interface Api { - - @GET("/g/{id}/") - Single getGalleryMetadata(@Path("id") String contentId); - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/ASMHentaiServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/ASMHentaiServer.java deleted file mode 100644 index 061dab70d1..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/ASMHentaiServer.java +++ /dev/null @@ -1,28 +0,0 @@ -package me.devsaki.hentoid.retrofit; - -import io.reactivex.Single; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.parsers.content.ASMHentaiContent; -import me.devsaki.hentoid.util.OkHttpClientSingleton; -import pl.droidsonroids.retrofit2.JspoonConverterFactory; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.http.GET; -import retrofit2.http.Path; - -public class ASMHentaiServer { - - public static final Api API = new Retrofit.Builder() - .baseUrl(Site.ASMHENTAI.getUrl()) - .client(OkHttpClientSingleton.getInstance()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - .addConverterFactory(JspoonConverterFactory.create()) - .build() - .create(Api.class); - - public interface Api { - - @GET("/g/{id}/") - Single getGalleryMetadata(@Path("id") String contentId); - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/FakkuServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/FakkuServer.java deleted file mode 100644 index 21572e9688..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/FakkuServer.java +++ /dev/null @@ -1,29 +0,0 @@ -package me.devsaki.hentoid.retrofit; - -import io.reactivex.Single; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.parsers.content.FakkuContent; -import me.devsaki.hentoid.util.OkHttpClientSingleton; -import pl.droidsonroids.retrofit2.JspoonConverterFactory; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.http.GET; -import retrofit2.http.Header; -import retrofit2.http.Path; - -public class FakkuServer { - - public static final Api API = new Retrofit.Builder() - .baseUrl(Site.FAKKU2.getUrl()) - .client(OkHttpClientSingleton.getInstance()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - .addConverterFactory(JspoonConverterFactory.create()) - .build() - .create(Api.class); - - public interface Api { - - @GET("/hentai/{id1}") - Single getGalleryMetadata(@Path("id1") String contentId1, @Header("cookie") String cookie); - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/NhentaiServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/GithubServer.java similarity index 52% rename from app/src/main/java/me/devsaki/hentoid/retrofit/NhentaiServer.java rename to app/src/main/java/me/devsaki/hentoid/retrofit/GithubServer.java index 4e41608476..d800f6f301 100644 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/NhentaiServer.java +++ b/app/src/main/java/me/devsaki/hentoid/retrofit/GithubServer.java @@ -1,30 +1,33 @@ package me.devsaki.hentoid.retrofit; +import java.util.List; + import io.reactivex.Single; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.parsers.content.NhentaiContent; import me.devsaki.hentoid.util.OkHttpClientSingleton; -import pl.droidsonroids.retrofit2.JspoonConverterFactory; +import me.devsaki.hentoid.viewholders.GitHubRelease; import retrofit2.Retrofit; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.converter.gson.GsonConverterFactory; import retrofit2.http.GET; -import retrofit2.http.Path; -// NHentai API reference : https://github.com/NHMoeDev/NHentai-android/issues/27 -public class NhentaiServer { +public class GithubServer { + + private static final String GITHUB_BASE_URL = "https://api.github.com/repos/avluis/Hentoid/"; public static final Api API = new Retrofit.Builder() - .baseUrl(Site.NHENTAI.getUrl()) + .baseUrl(GITHUB_BASE_URL) .client(OkHttpClientSingleton.getInstance()) .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - .addConverterFactory(JspoonConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) .build() .create(Api.class); public interface Api { - @GET("/g/{id}/") - Single getGalleryMetadata(@Path("id") String contentId); + @GET("releases") + Single> getReleases(); + + @GET("releases/latest") + Single getLatestRelease(); } } diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/HentaiCafeServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/HentaiCafeServer.java deleted file mode 100644 index 9bd1e5668f..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/HentaiCafeServer.java +++ /dev/null @@ -1,28 +0,0 @@ -package me.devsaki.hentoid.retrofit; - -import io.reactivex.Single; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.parsers.content.HentaiCafeContent; -import me.devsaki.hentoid.util.OkHttpClientSingleton; -import pl.droidsonroids.retrofit2.JspoonConverterFactory; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.http.GET; -import retrofit2.http.Path; - -public class HentaiCafeServer { - - public static final Api API = new Retrofit.Builder() - .baseUrl(Site.HENTAICAFE.getUrl()) - .client(OkHttpClientSingleton.getInstance()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - .addConverterFactory(JspoonConverterFactory.create()) - .build() - .create(Api.class); - - public interface Api { - - @GET("/{id}/") - Single getGalleryMetadata(@Path("id") String contentId); - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/HitomiServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/HitomiServer.java deleted file mode 100644 index 269c3bbf36..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/HitomiServer.java +++ /dev/null @@ -1,28 +0,0 @@ -package me.devsaki.hentoid.retrofit; - -import io.reactivex.Single; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.parsers.content.HitomiContent; -import me.devsaki.hentoid.util.OkHttpClientSingleton; -import pl.droidsonroids.retrofit2.JspoonConverterFactory; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.http.GET; -import retrofit2.http.Path; - -public class HitomiServer { - - public static final Api API = new Retrofit.Builder() - .baseUrl(Site.HITOMI.getUrl()) - .client(OkHttpClientSingleton.getInstance()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - .addConverterFactory(JspoonConverterFactory.create()) - .build() - .create(Api.class); - - public interface Api { - - @GET("/galleries/{id}") - Single getGalleryMetadata(@Path("id") String contentId); - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/PandaServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/PandaServer.java deleted file mode 100644 index 19a55feacb..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/PandaServer.java +++ /dev/null @@ -1,28 +0,0 @@ -package me.devsaki.hentoid.retrofit; - -import io.reactivex.Single; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.parsers.content.PandaContent; -import me.devsaki.hentoid.util.OkHttpClientSingleton; -import pl.droidsonroids.retrofit2.JspoonConverterFactory; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.http.GET; -import retrofit2.http.Path; - -public class PandaServer { - - public static final Api API = new Retrofit.Builder() - .baseUrl(Site.PANDA.getUrl()) - .client(OkHttpClientSingleton.getInstance()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - .addConverterFactory(JspoonConverterFactory.create()) - .build() - .create(Api.class); - - public interface Api { - - @GET("/{id1}/{id2}") - Single getGalleryMetadata(@Path("id1") String contentId1, @Path("id2") String contentId2); - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/PururinServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/PururinServer.java deleted file mode 100644 index f21f9eaf30..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/PururinServer.java +++ /dev/null @@ -1,28 +0,0 @@ -package me.devsaki.hentoid.retrofit; - -import io.reactivex.Single; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.parsers.content.PururinContent; -import me.devsaki.hentoid.util.OkHttpClientSingleton; -import pl.droidsonroids.retrofit2.JspoonConverterFactory; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.http.GET; -import retrofit2.http.Path; - -public class PururinServer { - - public static final Api API = new Retrofit.Builder() - .baseUrl(Site.PURURIN.getUrl()) - .client(OkHttpClientSingleton.getInstance()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - .addConverterFactory(JspoonConverterFactory.create()) - .build() - .create(Api.class); - - public interface Api { - - @GET("/gallery/{id1}/{id2}") - Single getGalleryMetadata(@Path("id1") String contentId1, @Path("id2") String contentId2); - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/TsuminoServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/TsuminoServer.java deleted file mode 100644 index a643ae2ccb..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/TsuminoServer.java +++ /dev/null @@ -1,29 +0,0 @@ -package me.devsaki.hentoid.retrofit; - -import io.reactivex.Single; -import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.parsers.content.TsuminoContent; -import me.devsaki.hentoid.util.OkHttpClientSingleton; -import pl.droidsonroids.retrofit2.JspoonConverterFactory; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.http.GET; -import retrofit2.http.Path; - -public class TsuminoServer { - - public static final Api API = new Retrofit.Builder() - .baseUrl(Site.TSUMINO.getUrl()) - .client(OkHttpClientSingleton.getInstance()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - .addConverterFactory(JspoonConverterFactory.create()) - .client(OkHttpClientSingleton.getInstance()) - .build() - .create(Api.class); - - public interface Api { - - @GET("/Book/Info/{id1}") - Single getGalleryMetadata(@Path("id1") String contentId1); - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/EHentaiServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/sources/EHentaiServer.java similarity index 89% rename from app/src/main/java/me/devsaki/hentoid/retrofit/EHentaiServer.java rename to app/src/main/java/me/devsaki/hentoid/retrofit/sources/EHentaiServer.java index 1731b13bc2..87f3b2289c 100644 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/EHentaiServer.java +++ b/app/src/main/java/me/devsaki/hentoid/retrofit/sources/EHentaiServer.java @@ -1,4 +1,4 @@ -package me.devsaki.hentoid.retrofit; +package me.devsaki.hentoid.retrofit.sources; import io.reactivex.Single; import me.devsaki.hentoid.parsers.content.EHentaiGalleriesMetadata; @@ -12,7 +12,7 @@ public class EHentaiServer { - private final static String API_URL = "http://e-hentai.org/"; + private static final String API_URL = "http://e-hentai.org/"; public static final Api API = new Retrofit.Builder() .baseUrl(API_URL) diff --git a/app/src/main/java/me/devsaki/hentoid/retrofit/MikanServer.java b/app/src/main/java/me/devsaki/hentoid/retrofit/sources/MikanServer.java similarity index 93% rename from app/src/main/java/me/devsaki/hentoid/retrofit/MikanServer.java rename to app/src/main/java/me/devsaki/hentoid/retrofit/sources/MikanServer.java index 1afe04b4e1..7452c00593 100644 --- a/app/src/main/java/me/devsaki/hentoid/retrofit/MikanServer.java +++ b/app/src/main/java/me/devsaki/hentoid/retrofit/sources/MikanServer.java @@ -1,4 +1,4 @@ -package me.devsaki.hentoid.retrofit; +package me.devsaki.hentoid.retrofit.sources; import java.util.Map; @@ -17,7 +17,7 @@ public class MikanServer { - private final static String MIKAN_BASE_URL = "https://api.initiate.host/v1/"; + private static final String MIKAN_BASE_URL = "https://api.initiate.host/v1/"; public static final Api API = new Retrofit.Builder() .baseUrl(MIKAN_BASE_URL) diff --git a/app/src/main/java/me/devsaki/hentoid/services/ContentDownloadService.java b/app/src/main/java/me/devsaki/hentoid/services/ContentDownloadService.java index d3759eeb73..d961cdf5bd 100644 --- a/app/src/main/java/me/devsaki/hentoid/services/ContentDownloadService.java +++ b/app/src/main/java/me/devsaki/hentoid/services/ContentDownloadService.java @@ -7,6 +7,9 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.util.SparseIntArray; +import android.webkit.MimeTypeMap; + +import androidx.documentfile.provider.DocumentFile; import com.android.volley.AuthFailureError; import com.android.volley.NetworkError; @@ -51,13 +54,14 @@ import me.devsaki.hentoid.notification.download.DownloadProgressNotification; import me.devsaki.hentoid.notification.download.DownloadSuccessNotification; import me.devsaki.hentoid.notification.download.DownloadWarningNotification; -import me.devsaki.hentoid.parsers.ContentParser; import me.devsaki.hentoid.parsers.ContentParserFactory; +import me.devsaki.hentoid.parsers.ImageListParser; import me.devsaki.hentoid.util.FileHelper; +import me.devsaki.hentoid.util.HttpHelper; import me.devsaki.hentoid.util.JsonHelper; -import me.devsaki.hentoid.util.MimeTypes; import me.devsaki.hentoid.util.Preferences; import me.devsaki.hentoid.util.notification.NotificationManager; +import me.devsaki.hentoid.util.notification.ServiceNotificationManager; import timber.log.Timber; @@ -69,11 +73,13 @@ public class ContentDownloadService extends IntentService { private ObjectBoxDB db; // Hentoid database - private NotificationManager notificationManager; + private ServiceNotificationManager notificationManager; private NotificationManager warningNotificationManager; private boolean downloadCanceled; // True if a Cancel event has been processed; false by default private boolean downloadSkipped; // True if a Skip event has been processed; false by default + private RequestQueueManager requestQueueManager = null; + public ContentDownloadService() { super(ContentDownloadService.class.getName()); } @@ -83,10 +89,11 @@ public void onCreate() { super.onCreate(); db = ObjectBoxDB.getInstance(this); - notificationManager = new NotificationManager(this, 0); + notificationManager = new ServiceNotificationManager(this, 1); notificationManager.cancel(); + notificationManager.startForeground(new DownloadProgressNotification("Starting download", 0, 0)); - warningNotificationManager = new NotificationManager(this, 1); + warningNotificationManager = new NotificationManager(this, 2); warningNotificationManager.cancel(); EventBus.getDefault().register(this); @@ -147,6 +154,8 @@ private Content downloadFirstInQueue() { return null; } + downloadCanceled = false; + downloadSkipped = false; db.deleteErrorRecords(content.getId()); boolean hasError = false; @@ -177,13 +186,30 @@ private Content downloadFirstInQueue() { return null; } + // Could have been canceled while preparing the download + if (downloadCanceled || downloadSkipped) return null; + File dir = FileHelper.createContentDownloadDir(this, content); if (!dir.exists()) { String title = content.getTitle(); String absolutePath = dir.getAbsolutePath(); - Timber.w("Directory could not be created: %s.", absolutePath); + + // Log everywhere + String message = String.format("Directory could not be created: %s.", absolutePath); + Timber.w(message); + logErrorRecord(content.getId(), ErrorType.IO, content.getUrl(), "Destination folder", message); warningNotificationManager.notify(new DownloadWarningNotification(title, absolutePath)); - // Download _will_ continue and images _will_ fail, so that user can retry downloading later + + // No sense in waiting for every image to be downloaded in error state (terrible waste of network resources) + // => Create all images, flag them as failed as well as the book + for (ImageFile img : images) { + if (img.getStatus().equals(StatusContent.SAVED)) { + img.setStatus(StatusContent.ERROR); + db.updateImageFileStatusAndParams(img); + } + } + completeDownload(content, 0, images.size()); + return null; } String fileRoot = Preferences.getRootFolderName(); @@ -196,8 +222,6 @@ private Content downloadFirstInQueue() { HentoidApp.trackDownloadEvent("Added"); Timber.i("Downloading '%s' [%s]", content.getTitle(), content.getId()); - downloadCanceled = false; - downloadSkipped = false; // Reset ERROR status of images to count them as "to be downloaded" (in DB and in memory) for (ImageFile img : images) { @@ -210,10 +234,11 @@ private Content downloadFirstInQueue() { // Queue image download requests ImageFile cover = new ImageFile().setName("thumb").setUrl(content.getCoverImageUrl()); Site site = content.getSite(); - RequestQueueManager.getInstance(this, content.getSite().isAllowParallelDownloads()).queueRequest(buildDownloadRequest(cover, dir, site.canKnowHentoidAgent(), site.hasImageProcessing())); + requestQueueManager = RequestQueueManager.getInstance(this, site.isAllowParallelDownloads()); + requestQueueManager.queueRequest(buildDownloadRequest(cover, dir, site.canKnowHentoidAgent(), site.hasImageProcessing())); for (ImageFile img : images) { if (img.getStatus().equals(StatusContent.SAVED)) - RequestQueueManager.getInstance().queueRequest(buildDownloadRequest(img, dir, site.canKnowHentoidAgent(), site.hasImageProcessing())); + requestQueueManager.queueRequest(buildDownloadRequest(img, dir, site.canKnowHentoidAgent(), site.hasImageProcessing())); } return content; @@ -228,7 +253,8 @@ private Content downloadFirstInQueue() { */ private void watchProgress(Content content) { boolean isDone; - int pagesOK, pagesKO; + int pagesOK; + int pagesKO; List images = content.getImageFiles(); ContentQueueManager contentQueueManager = ContentQueueManager.getInstance(); @@ -272,7 +298,6 @@ private void completeDownload(Content content, int pagesOK, int pagesKO) { ContentQueueManager contentQueueManager = ContentQueueManager.getInstance(); if (!downloadCanceled && !downloadSkipped) { - File dir = FileHelper.createContentDownloadDir(this, content); List images = content.getImageFiles(); int nbImages = (null == images) ? 0 : images.size(); @@ -287,15 +312,24 @@ private void completeDownload(Content content, int pagesOK, int pagesKO) { // Mark content as downloaded content.setDownloadDate(new Date().getTime()); content.setStatus((0 == pagesKO && !hasError) ? StatusContent.DOWNLOADED : StatusContent.ERROR); - // Clear download params from content and images - content.setDownloadParams(""); + // Clear download params from content + if (0 == pagesKO && !hasError) content.setDownloadParams(""); db.insertContent(content); // Save JSON file + File dir = FileHelper.createContentDownloadDir(this, content); if (dir.exists()) { try { - JsonHelper.saveJson(content.preJSONExport(), dir); + File jsonFile = JsonHelper.createJson(content.preJSONExport(), dir); + // Cache its URI to the newly created content + DocumentFile jsonDocFile = FileHelper.getDocumentFile(jsonFile, false); + if (jsonDocFile != null) { + content.setJsonUri(jsonDocFile.getUri().toString()); + db.insertContent(content); + } else { + Timber.w("JSON file could not be cached for %s", content.getTitle()); + } } catch (IOException e) { Timber.e(e, "I/O Error saving JSON: %s", content.getTitle()); } @@ -349,8 +383,8 @@ private void completeDownload(Content content, int pagesOK, int pagesKO) { */ private List fetchImageURLs(Content content) throws Exception { List imgs; - // Use ContentParser to query the source - ContentParser parser = ContentParserFactory.getInstance().getParser(content); + // Use ImageListParser to query the source + ImageListParser parser = ContentParserFactory.getInstance().getImageListParser(content); imgs = parser.parseImageList(content); if (imgs.isEmpty()) @@ -380,10 +414,10 @@ private Request buildDownloadRequest( }.getType(); Map downloadParams = new Gson().fromJson(downloadParamsStr, type); - if (downloadParams.containsKey("cookie")) - headers.put("cookie", downloadParams.get("cookie")); - if (downloadParams.containsKey("referer")) - headers.put("referer", downloadParams.get("referer")); + if (downloadParams.containsKey(HttpHelper.HEADER_COOKIE_KEY)) + headers.put(HttpHelper.HEADER_COOKIE_KEY, downloadParams.get(HttpHelper.HEADER_COOKIE_KEY)); + if (downloadParams.containsKey(HttpHelper.HEADER_REFERER_KEY)) + headers.put(HttpHelper.HEADER_REFERER_KEY, downloadParams.get(HttpHelper.HEADER_REFERER_KEY)); } return new InputStreamVolleyRequest( @@ -443,16 +477,21 @@ private void onRequestError(VolleyError error, ImageFile img) { private static byte[] processImage(String downloadParamsStr, byte[] binaryContent) throws InvalidParameterException { Type type = new TypeToken>() { }.getType(); - Map downloadParams = new Gson().fromJson(downloadParamsStr, type); - if (!downloadParams.containsKey("pageInfo")) { + Map downloadParams = new Gson().fromJson(downloadParamsStr, type); + if (!downloadParams.containsKey("pageInfo")) throw new InvalidParameterException("No pageInfo"); - } + + String pageInfoValue = downloadParams.get("pageInfo"); + if (null == pageInfoValue) throw new InvalidParameterException("PageInfo is null"); + + if (pageInfoValue.equals("unprotected")) + return binaryContent; // Free content, picture is not protected // byte[] imgData = Base64.decode(binaryContent, Base64.DEFAULT); Bitmap sourcePicture = BitmapFactory.decodeByteArray(binaryContent, 0, binaryContent.length); - PageInfo page = new Gson().fromJson(downloadParams.get("pageInfo"), PageInfo.class); + PageInfo page = new Gson().fromJson(pageInfoValue, PageInfo.class); Bitmap.Config conf = Bitmap.Config.ARGB_8888; Bitmap destPicture = Bitmap.createBitmap(page.width, page.height, conf); Canvas destCanvas = new Canvas(destPicture); @@ -509,7 +548,8 @@ private static void processAndSaveImage(ImageFile img, File dir, String contentT * @throws IOException IOException if image cannot be saved at given location */ private static void saveImage(String fileName, File dir, String contentType, byte[] binaryContent) throws IOException { - File file = new File(dir, fileName + "." + MimeTypes.getExtensionFromMimeType(contentType)); + String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(contentType); + File file = new File(dir, fileName + "." + ext); FileHelper.saveBinaryInFile(file, binaryContent); } @@ -536,19 +576,19 @@ public void onDownloadEvent(DownloadEvent event) { switch (event.eventType) { case DownloadEvent.EV_PAUSE: db.updateContentStatus(StatusContent.DOWNLOADING, StatusContent.PAUSED); - RequestQueueManager.getInstance().cancelQueue(); + requestQueueManager.cancelQueue(); ContentQueueManager.getInstance().pauseQueue(); notificationManager.cancel(); break; case DownloadEvent.EV_CANCEL: - RequestQueueManager.getInstance().cancelQueue(); + requestQueueManager.cancelQueue(); downloadCanceled = true; // Tracking Event (Download Canceled) HentoidApp.trackDownloadEvent("Cancelled"); break; case DownloadEvent.EV_SKIP: db.updateContentStatus(StatusContent.DOWNLOADING, StatusContent.PAUSED); - RequestQueueManager.getInstance().cancelQueue(); + requestQueueManager.cancelQueue(); downloadSkipped = true; // Tracking Event (Download Skipped) HentoidApp.trackDownloadEvent("Skipped"); @@ -558,7 +598,7 @@ public void onDownloadEvent(DownloadEvent event) { } } - public void logErrorRecord(long contentId, ErrorType type, String url, String contentPart, String description) { + private void logErrorRecord(long contentId, ErrorType type, String url, String contentPart, String description) { ErrorRecord record = new ErrorRecord(contentId, type, url, contentPart, description); if (contentId > 0) db.insertErrorRecord(record); } diff --git a/app/src/main/java/me/devsaki/hentoid/services/ContentQueueManager.java b/app/src/main/java/me/devsaki/hentoid/services/ContentQueueManager.java index 06fea0869b..329dfb1777 100644 --- a/app/src/main/java/me/devsaki/hentoid/services/ContentQueueManager.java +++ b/app/src/main/java/me/devsaki/hentoid/services/ContentQueueManager.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.Intent; +import android.os.Build; /** * Created by Robb_w on 2018/04 @@ -49,7 +50,11 @@ public void unpauseQueue() { public void resumeQueue(Context context) { Intent intent = new Intent(Intent.ACTION_SYNC, null, context, ContentDownloadService.class); - context.startService(intent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent); + } else { + context.startService(intent); + } isQueueActive = true; } diff --git a/app/src/main/java/me/devsaki/hentoid/services/DatabaseMaintenanceService.java b/app/src/main/java/me/devsaki/hentoid/services/DatabaseMaintenanceService.java index d9e23603d1..60f51894ce 100644 --- a/app/src/main/java/me/devsaki/hentoid/services/DatabaseMaintenanceService.java +++ b/app/src/main/java/me/devsaki/hentoid/services/DatabaseMaintenanceService.java @@ -4,9 +4,12 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; -import android.support.annotation.Nullable; + +import androidx.annotation.Nullable; import me.devsaki.hentoid.database.DatabaseMaintenance; +import me.devsaki.hentoid.notification.maintenance.MaintenanceNotification; +import me.devsaki.hentoid.util.notification.ServiceNotificationManager; import timber.log.Timber; /** @@ -14,7 +17,8 @@ */ public class DatabaseMaintenanceService extends IntentService { - private static boolean running; + private ServiceNotificationManager notificationManager; + public DatabaseMaintenanceService() { super(DatabaseMaintenanceService.class.getName()); @@ -24,20 +28,19 @@ public static Intent makeIntent(Context context) { return new Intent(context, DatabaseMaintenanceService.class); } - public static boolean isRunning() { - return running; - } - @Override public void onCreate() { super.onCreate(); - running = true; + + notificationManager = new ServiceNotificationManager(this, 1); + notificationManager.startForeground(new MaintenanceNotification("Performing maintenance")); + Timber.i("Service created"); } @Override public void onDestroy() { - running = false; + notificationManager.cancel(); Timber.i("Service destroyed"); super.onDestroy(); diff --git a/app/src/main/java/me/devsaki/hentoid/services/DatabaseMigrationService.java b/app/src/main/java/me/devsaki/hentoid/services/DatabaseMigrationService.java index 0c6599cd7f..bf5b8e13b9 100644 --- a/app/src/main/java/me/devsaki/hentoid/services/DatabaseMigrationService.java +++ b/app/src/main/java/me/devsaki/hentoid/services/DatabaseMigrationService.java @@ -4,11 +4,12 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; -import android.support.annotation.Nullable; import android.util.Log; import android.util.SparseArray; import android.util.SparseIntArray; +import androidx.annotation.Nullable; + import org.greenrobot.eventbus.EventBus; import java.io.File; @@ -21,18 +22,21 @@ import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.events.ImportEvent; +import me.devsaki.hentoid.notification.maintenance.MaintenanceNotification; import me.devsaki.hentoid.util.Consts; import me.devsaki.hentoid.util.LogUtil; +import me.devsaki.hentoid.util.notification.ServiceNotificationManager; import timber.log.Timber; /** - * Service responsible for migrating the oldHentoidDB to the ObjectBoxDB + * Service responsible for migrating the old HentoidDB to ObjectBoxDB * * @see UpdateCheckService */ public class DatabaseMigrationService extends IntentService { - private static boolean running; + private ServiceNotificationManager notificationManager; + public DatabaseMigrationService() { super(DatabaseMigrationService.class.getName()); @@ -42,21 +46,19 @@ public static Intent makeIntent(Context context) { return new Intent(context, DatabaseMigrationService.class); } - public static boolean isRunning() { - return running; - } - @Override public void onCreate() { super.onCreate(); - running = true; + notificationManager = new ServiceNotificationManager(this, 1); + notificationManager.startForeground(new MaintenanceNotification("Performing database migration")); + Timber.w("Service created"); } @Override public void onDestroy() { - running = false; + notificationManager.cancel(); Timber.w("Service destroyed"); super.onDestroy(); @@ -99,6 +101,7 @@ private void cleanUpDB() { /** * Migrate HentoidDB books to ObjectBoxDB */ + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) private void migrate() { int booksOK = 0; int booksKO = 0; diff --git a/app/src/main/java/me/devsaki/hentoid/services/ImportService.java b/app/src/main/java/me/devsaki/hentoid/services/ImportService.java index ced024e619..ab833d4f81 100644 --- a/app/src/main/java/me/devsaki/hentoid/services/ImportService.java +++ b/app/src/main/java/me/devsaki/hentoid/services/ImportService.java @@ -4,10 +4,11 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; -import android.support.annotation.CheckResult; -import android.support.annotation.Nullable; import android.util.Log; +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; + import org.greenrobot.eventbus.EventBus; import java.io.File; @@ -70,6 +71,7 @@ public void onCreate() { running = true; notificationManager = new ServiceNotificationManager(this, NOTIFICATION_ID); notificationManager.cancel(); + notificationManager.startForeground(new ImportStartNotification()); Timber.w("Service created"); } @@ -77,6 +79,7 @@ public void onCreate() { @Override public void onDestroy() { running = false; + notificationManager.cancel(); Timber.w("Service destroyed"); super.onDestroy(); @@ -136,7 +139,6 @@ private void startImport(boolean rename, boolean cleanNoJSON, boolean cleanNoIma Content content = null; List log = new ArrayList<>(); - notificationManager.startForeground(new ImportStartNotification()); File rootFolder = new File(Preferences.getRootFolderName()); // 1st pass : count subfolders of every site folder @@ -181,8 +183,8 @@ private void startImport(boolean rename, boolean cleanNoJSON, boolean cleanNoIma if (rename) { String canonicalBookDir = FileHelper.formatDirPath(content); - String[] currentPathParts = folder.getAbsolutePath().split("/"); - String currentBookDir = "/" + currentPathParts[currentPathParts.length - 2] + "/" + currentPathParts[currentPathParts.length - 1]; + String[] currentPathParts = folder.getAbsolutePath().split(File.separator); + String currentBookDir = File.separator + currentPathParts[currentPathParts.length - 2] + File.separator + currentPathParts[currentPathParts.length - 1]; if (!canonicalBookDir.equals(currentBookDir)) { String settingDir = Preferences.getRootFolderName(); @@ -198,6 +200,7 @@ private void startImport(boolean rename, boolean cleanNoJSON, boolean cleanNoIma } } } + // TODO : Populate images when data is loaded from old JSONs (DoujinBuilder object) ObjectBoxDB.getInstance(this).insertContent(content); trace(Log.INFO, log, "Import book OK : %s", folder.getAbsolutePath()); } else { // JSON not found @@ -273,13 +276,13 @@ private static Content importJson(File folder) throws JSONParseException { return null; } - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) private static List from(List urlBuilders, Site site) { List attributes = null; if (urlBuilders == null) { return null; } - if (urlBuilders.size() > 0) { + if (!urlBuilders.isEmpty()) { attributes = new ArrayList<>(); for (URLBuilder urlBuilder : urlBuilders) { Attribute attribute = from(urlBuilder, AttributeType.TAG, site); @@ -292,7 +295,7 @@ private static List from(List urlBuilders, Site site) { return attributes; } - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) private static Attribute from(URLBuilder urlBuilder, AttributeType type, Site site) { if (urlBuilder == null) { return null; @@ -310,7 +313,7 @@ private static Attribute from(URLBuilder urlBuilder, AttributeType type, Site si } @CheckResult - @SuppressWarnings("deprecation") + @SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod"}) private static Content importJsonLegacy(File json) throws JSONParseException { try { DoujinBuilder doujinBuilder = @@ -351,7 +354,7 @@ private static Content importJsonLegacy(File json) throws JSONParseException { String fileRoot = Preferences.getRootFolderName(); contentV2.setStorageFolder(json.getAbsoluteFile().getParent().substring(fileRoot.length())); - JsonHelper.saveJson(contentV2.preJSONExport(), json.getAbsoluteFile().getParentFile()); + JsonHelper.createJson(contentV2.preJSONExport(), json.getAbsoluteFile().getParentFile()); return contentV2; } catch (Exception e) { @@ -374,7 +377,7 @@ private static Content importJsonV1(File json) throws JSONParseException { String fileRoot = Preferences.getRootFolderName(); contentV2.setStorageFolder(json.getAbsoluteFile().getParent().substring(fileRoot.length())); - JsonHelper.saveJson(contentV2.preJSONExport(), json.getAbsoluteFile().getParentFile()); + JsonHelper.createJson(contentV2.preJSONExport(), json.getAbsoluteFile().getParentFile()); return contentV2; } catch (Exception e) { diff --git a/app/src/main/java/me/devsaki/hentoid/services/RequestQueueManager.java b/app/src/main/java/me/devsaki/hentoid/services/RequestQueueManager.java index 1946416230..5c85de1dda 100644 --- a/app/src/main/java/me/devsaki/hentoid/services/RequestQueueManager.java +++ b/app/src/main/java/me/devsaki/hentoid/services/RequestQueueManager.java @@ -29,7 +29,7 @@ */ public class RequestQueueManager implements RequestQueue.RequestFinishedListener { private static RequestQueueManager mInstance; // Instance of the singleton - private static Boolean allowParallelDownloads = null; // True if current instance has anti-parallel mode on + private static Boolean allowParallelDownloads = null; // True if current instance can download from the same IP with multiple simultaneous connexions private static final int TIMEOUT_MS = 15000; private RequestQueue mRequestQueue; // Volley download request queue @@ -70,10 +70,6 @@ private static int getMemoryClass(Context context) { return activityManager.getMemoryClass(); } - public static synchronized RequestQueueManager getInstance() { - return getInstance(null, true); - } - @SuppressWarnings("unchecked") public static synchronized RequestQueueManager getInstance(Context context, boolean allowParallelDownloads) { if (context != null && (mInstance == null || (null == RequestQueueManager.allowParallelDownloads || RequestQueueManager.allowParallelDownloads != allowParallelDownloads))) { diff --git a/app/src/main/java/me/devsaki/hentoid/services/UpdateCheckService.java b/app/src/main/java/me/devsaki/hentoid/services/UpdateCheckService.java index 5d207e84ee..b8a8d654c5 100644 --- a/app/src/main/java/me/devsaki/hentoid/services/UpdateCheckService.java +++ b/app/src/main/java/me/devsaki/hentoid/services/UpdateCheckService.java @@ -4,11 +4,14 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.widget.Toast; +import org.greenrobot.eventbus.EventBus; + import io.reactivex.disposables.Disposable; import me.devsaki.hentoid.BuildConfig; +import me.devsaki.hentoid.events.UpdateEvent; import me.devsaki.hentoid.model.UpdateInfoJson; import me.devsaki.hentoid.notification.update.UpdateAvailableNotification; import me.devsaki.hentoid.notification.update.UpdateCheckNotification; @@ -55,11 +58,14 @@ public IBinder onBind(Intent intent) { @Override public void onCreate() { notificationManager = new ServiceNotificationManager(this, NOTIFICATION_ID); + notificationManager.startForeground(new UpdateCheckNotification()); + Timber.w("Service created"); } @Override public void onDestroy() { + notificationManager.cancel(); if (disposable != null) disposable.dispose(); Timber.w("Service destroyed"); } @@ -80,8 +86,6 @@ public int onStartCommand(Intent intent, int flags, int startId) { } private void checkForUpdates() { - notificationManager.startForeground(new UpdateCheckNotification()); - disposable = UpdateServer.API.getUpdateInfo() .retry(3) .observeOn(mainThread()) @@ -94,6 +98,7 @@ private void onCheckSuccess(UpdateInfoJson updateInfoJson) { stopForeground(true); String updateUrl = updateInfoJson.getUpdateUrl(); + EventBus.getDefault().postSticky(new UpdateEvent(true)); notificationManager.notify(new UpdateAvailableNotification(updateUrl)); } else { if (shouldShowToast) { diff --git a/app/src/main/java/me/devsaki/hentoid/services/UpdateDownloadService.java b/app/src/main/java/me/devsaki/hentoid/services/UpdateDownloadService.java index 3abfe81c73..0bb5d8b423 100644 --- a/app/src/main/java/me/devsaki/hentoid/services/UpdateDownloadService.java +++ b/app/src/main/java/me/devsaki/hentoid/services/UpdateDownloadService.java @@ -6,7 +6,7 @@ import android.net.Uri; import android.os.Handler; import android.os.IBinder; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import com.thin.downloadmanager.DownloadRequest; import com.thin.downloadmanager.DownloadStatusListenerV1; @@ -57,7 +57,10 @@ public static boolean isRunning() { public void onCreate() { running = true; downloadManager = new ThinDownloadManager(); + notificationManager = new ServiceNotificationManager(this, NOTIFICATION_ID); + notificationManager.startForeground(new UpdateProgressNotification(INDETERMINATE)); + progressHandler = new Handler(); Timber.w("Service created"); } @@ -65,6 +68,7 @@ public void onCreate() { @Override public void onDestroy() { running = false; + notificationManager.cancel(); downloadManager.release(); Timber.w("Service destroyed"); } @@ -85,8 +89,6 @@ public int onStartCommand(Intent intent, int flags, int startId) { private void downloadUpdate(Uri updateUri) { Timber.w("Starting download"); - notificationManager.startForeground(new UpdateProgressNotification(INDETERMINATE)); - File apkFile = new File(getExternalCacheDir(), "hentoid.apk"); Uri destinationUri = Uri.fromFile(apkFile); diff --git a/app/src/main/java/me/devsaki/hentoid/timber/CrashlyticsTree.java b/app/src/main/java/me/devsaki/hentoid/timber/CrashlyticsTree.java index 1054dbc6dc..9dc7eb48cc 100644 --- a/app/src/main/java/me/devsaki/hentoid/timber/CrashlyticsTree.java +++ b/app/src/main/java/me/devsaki/hentoid/timber/CrashlyticsTree.java @@ -1,6 +1,6 @@ package me.devsaki.hentoid.timber; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import android.util.Log; import com.crashlytics.android.Crashlytics; diff --git a/app/src/main/java/me/devsaki/hentoid/ui/CarouselDecorator.java b/app/src/main/java/me/devsaki/hentoid/ui/CarouselDecorator.java index f49e73d926..7623c6e542 100644 --- a/app/src/main/java/me/devsaki/hentoid/ui/CarouselDecorator.java +++ b/app/src/main/java/me/devsaki/hentoid/ui/CarouselDecorator.java @@ -1,11 +1,11 @@ package me.devsaki.hentoid.ui; import android.content.Context; -import android.support.annotation.LayoutRes; -import android.support.annotation.NonNull; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.LinearSnapHelper; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.LinearSnapHelper; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/java/me/devsaki/hentoid/ui/CompoundAdapter.java b/app/src/main/java/me/devsaki/hentoid/ui/CompoundAdapter.java deleted file mode 100644 index 80f4306bc0..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/ui/CompoundAdapter.java +++ /dev/null @@ -1,112 +0,0 @@ -package me.devsaki.hentoid.ui; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.SimpleAdapter; -import android.widget.TextView; - -import java.util.List; -import java.util.Map; - -/** - * Created by avluis on 04/17/2016. - * Custom implementation of Simple Adapter for our TextView compound drawable. - */ -public class CompoundAdapter extends SimpleAdapter implements SimpleAdapter.ViewBinder { - - private final LayoutInflater mInflater; - private final int mResource; - private final String[] mFrom; - private final int[] mTo; - private final List> mData; - private ViewBinder mViewBinder; - - protected CompoundAdapter(Context context, List> data, int resource, - String[] from, int[] to) { - super(context, data, resource, from, to); - - mResource = resource; - mFrom = from; - mTo = to; - mData = data; - - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - private View createViewFromResource(LayoutInflater inflater, int position, View convertView, - ViewGroup parent, int resource) { - View v; - if (convertView == null) { - v = inflater.inflate(resource, parent, false); - } else { - v = convertView; - } - - bindView(position, v); - - return v; - } - - private void bindView(int position, View view) { - final Map dataSet = mData.get(position); - if (dataSet == null) { - return; - } - - final ViewBinder binder = mViewBinder; - final int[] to = mTo; - final int count = to.length; - - for (int i = 0; i < count; i++) { - final View v = view.findViewById(to[i]); - if (v != null) { - final Object data = dataSet.get(mFrom[i]); - String IMAGE_KEY = DrawerMenuContents.FIELD_ICON; - final Object imageData = dataSet.get(IMAGE_KEY); - String text = data == null ? "" : data.toString(); - - int resourceId = (Integer) imageData; - - if (text == null) { - text = ""; - } - - boolean bound = false; - if (binder != null) { - bound = binder.setViewValue(v, data, text); - } - - if (!bound) { - setViewText((TextView) v, text); - setViewDrawable((TextView) v, resourceId); - } - } - } - } - - private void setViewDrawable(TextView view, int resource) { - view.setCompoundDrawablesWithIntrinsicBounds(resource, 0, 0, 0); - } - - @Override - public ViewBinder getViewBinder() { - return mViewBinder; - } - - @Override - public void setViewBinder(ViewBinder viewBinder) { - mViewBinder = viewBinder; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - return createViewFromResource(mInflater, position, convertView, parent, mResource); - } - - @Override - public boolean setViewValue(View view, Object data, String textRepresentation) { - return true; - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/ui/DrawerMenuContents.java b/app/src/main/java/me/devsaki/hentoid/ui/DrawerMenuContents.java deleted file mode 100644 index 95b6878ad6..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/ui/DrawerMenuContents.java +++ /dev/null @@ -1,52 +0,0 @@ -package me.devsaki.hentoid.ui; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import me.devsaki.hentoid.enums.DrawerItem; -import me.devsaki.hentoid.util.Preferences; - -public class DrawerMenuContents { - - public static final String FIELD_TITLE = "title"; - public static final String FIELD_ICON = "icon"; - - private final List activities = new ArrayList<>(); - private final ArrayList> items = new ArrayList<>(); - - public DrawerMenuContents() { - for (DrawerItem drawerItem : DrawerItem.values()) { - // Hide panda if not explicitely enabled - if (drawerItem.label.equals("PANDA") && !Preferences.isUseSfw()) continue; - - activities.add(drawerItem.activityClass); - items.add(populateDrawerItem(drawerItem.label, drawerItem.icon)); - } - } - - private Map populateDrawerItem(String title, int icon) { - HashMap item = new HashMap<>(); - item.put(FIELD_TITLE, title); - item.put(FIELD_ICON, icon); - return item; - } - - public Class getActivity(int position) { - return activities.get(position); - } - - public List> getItems() { - return items; - } - - public int getPosition(Class activityClass) { - for (int i = 0; i < activities.size(); i++) { - if (activities.get(i).equals(activityClass)) { - return i; - } - } - return -1; - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/util/AttributeCache.java b/app/src/main/java/me/devsaki/hentoid/util/AttributeCache.java index 88e33f4cb2..e3257300d6 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/AttributeCache.java +++ b/app/src/main/java/me/devsaki/hentoid/util/AttributeCache.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -24,22 +25,21 @@ */ public class AttributeCache { - private final static int EXPIRY_FILE_VERSION = 1; - private final static int COLLECTION_FILE_VERSION = 1; + private static final int EXPIRY_FILE_VERSION = 1; + private static final int COLLECTION_FILE_VERSION = 1; private static File cacheDir; private static Map collectionExpiry; private static Map> collection; - private static void init() - { + private static void init() { collectionExpiry = new HashMap<>(); collection = new HashMap<>(); cacheDir = HentoidApp.getAppContext().getExternalCacheDir(); // Load expiry dates from cache - Uri destinationUri = Uri.parse(cacheDir + "/expiries.cache"); + Uri destinationUri = Uri.parse(cacheDir + File.separator + "expiries.cache"); File file = new File(String.valueOf(destinationUri)); try { if (file.exists()) { @@ -52,15 +52,14 @@ private static void init() } } - public static List getFromCache(String key) - { + public static List getFromCache(String key) { if (null == collectionExpiry) init(); if (collectionExpiry.containsKey(key) && collectionExpiry.get(key).after(new Date())) // Cache is not expired { if (!collection.containsKey(key)) { // Load master data from cache - Uri destinationUri = Uri.parse(cacheDir + "/" + key + ".cache"); + Uri destinationUri = Uri.parse(cacheDir + File.separator + key + ".cache"); File file = new File(String.valueOf(destinationUri)); try { if (file.exists()) { @@ -73,8 +72,8 @@ public static List getFromCache(String key) } } return collection.get(key); - } - else return null; + } else + return Collections.emptyList(); } public static void setCache(String key, List value, Date expiryDateUTC) { @@ -93,7 +92,7 @@ public static void setCache(String key, List value, Date expiryDateUT collection.put(key, new ArrayList<>(value)); // Put expiry dates in cache - Uri destinationUri = Uri.parse(cacheDir + "/expiries.cache"); + Uri destinationUri = Uri.parse(cacheDir + File.separator + "expiries.cache"); File file = new File(String.valueOf(destinationUri)); try { if ((!file.exists() || file.delete()) && file.createNewFile()) @@ -106,7 +105,7 @@ public static void setCache(String key, List value, Date expiryDateUT } // Put master data in cache - destinationUri = Uri.parse(cacheDir + "/" + key + ".cache"); + destinationUri = Uri.parse(cacheDir + File.separator + key + ".cache"); file = new File(String.valueOf(destinationUri)); try { if ((!file.exists() || file.delete()) && file.createNewFile()) @@ -120,20 +119,17 @@ public static void setCache(String key, List value, Date expiryDateUT } } - private static void saveExpiriesToStream(DataOutputStream output) throws IOException - { + private static void saveExpiriesToStream(DataOutputStream output) throws IOException { output.writeInt(EXPIRY_FILE_VERSION); output.writeInt(collectionExpiry.size()); - for (String key : collectionExpiry.keySet()) - { + for (String key : collectionExpiry.keySet()) { output.writeUTF(key); output.writeLong(collectionExpiry.get(key).getTime()); } } - private static void loadExpiriesFromStream(DataInputStream input) throws IOException - { + private static void loadExpiriesFromStream(DataInputStream input) throws IOException { input.readInt(); // File version int collectionSize = input.readInt(); @@ -141,8 +137,7 @@ private static void loadExpiriesFromStream(DataInputStream input) throws IOExcep String key; Date value; - for (int i=0; i cacheCollection = collection.get(key); output.writeInt(COLLECTION_FILE_VERSION); output.writeInt(collection.size()); - for (Attribute a : cacheCollection) - { + for (Attribute a : cacheCollection) { a.saveToStream(output); } } - private static void loadCacheFromStream(String key, DataInputStream input) throws IOException - { + private static void loadCacheFromStream(String key, DataInputStream input) throws IOException { List attrs = new ArrayList<>(); input.readInt(); // File version diff --git a/app/src/main/java/me/devsaki/hentoid/util/ContentNotRemovedException.java b/app/src/main/java/me/devsaki/hentoid/util/ContentNotRemovedException.java index 9860d0d155..6ea9b3ca1d 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/ContentNotRemovedException.java +++ b/app/src/main/java/me/devsaki/hentoid/util/ContentNotRemovedException.java @@ -4,7 +4,7 @@ public class ContentNotRemovedException extends Exception { - private Content content; + private final Content content; public ContentNotRemovedException(Content content, String message) { diff --git a/app/src/main/java/me/devsaki/hentoid/util/FileHelper.java b/app/src/main/java/me/devsaki/hentoid/util/FileHelper.java index f3f9fbeab0..c7ac0b7b4a 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/FileHelper.java +++ b/app/src/main/java/me/devsaki/hentoid/util/FileHelper.java @@ -4,13 +4,17 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.WorkerThread; -import android.support.v4.content.ContextCompat; -import android.support.v4.content.FileProvider; +import android.os.Bundle; import android.webkit.MimeTypeMap; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import androidx.documentfile.provider.DocumentFile; + import org.apache.commons.io.FileUtils; import java.io.BufferedOutputStream; @@ -20,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -28,9 +33,6 @@ import javax.annotation.Nonnull; -import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; import me.devsaki.hentoid.BuildConfig; import me.devsaki.hentoid.HentoidApp; import me.devsaki.hentoid.R; @@ -51,7 +53,7 @@ */ public class FileHelper { // Note that many devices will report true (there are no guarantees of this being 'external') - public static final boolean isSDPresent = getExternalStorageState().equals(MEDIA_MOUNTED); + public static final boolean isSdPresent = getExternalStorageState().equals(MEDIA_MOUNTED); private static final String AUTHORIZED_CHARS = "[^a-zA-Z0-9.-]"; @@ -224,6 +226,10 @@ static OutputStream getOutputStream(@NonNull final File target) throws IOExcepti return FileUtil.getOutputStream(target); } + static OutputStream getOutputStream(@NonNull final DocumentFile target) throws IOException { + return FileUtil.getOutputStream(target); + } + static InputStream getInputStream(@NonNull final File target) throws IOException { return FileUtil.getInputStream(target); } @@ -270,10 +276,9 @@ private static boolean cleanDirectory(@NonNull File target) { * * @param directory directory to clean * @return true if directory has been successfully cleaned - * @throws IOException in case cleaning is unsuccessful - * @throws IllegalArgumentException if {@code directory} does not exist or is not a directory + * @throws IOException in case cleaning is unsuccessful */ - private static boolean tryCleanDirectory(@NonNull File directory) throws IOException, SecurityException { + private static boolean tryCleanDirectory(@NonNull File directory) throws IOException { File[] files = directory.listFiles(); if (files == null) throw new IOException("Failed to list content of " + directory); @@ -351,9 +356,7 @@ public static void createNoMedia() { public static void removeContent(Content content) { // If the book has just starting being downloaded and there are no complete pictures on memory yet, it has no storage folder => nothing to delete if (content.getStorageFolder().length() > 0) { - String settingDir = Preferences.getRootFolderName(); - File dir = new File(settingDir, content.getStorageFolder()); - + File dir = getContentDownloadDir(content); if (deleteQuietly(dir) || FileUtil.deleteWithSAF(dir)) { Timber.i("Directory %s removed.", dir); } else { @@ -390,6 +393,11 @@ public static File createContentDownloadDir(Context context, Content content) { return file; } + public static File getContentDownloadDir(Content content) { + String rootFolderName = Preferences.getRootFolderName(); + return new File(rootFolderName, content.getStorageFolder()); + } + /** * Format the download directory path of the given content according to current user preferences * @@ -402,37 +410,49 @@ public static String formatDirPath(Content content) { int folderNamingPreference = Preferences.getFolderNameFormat(); if (folderNamingPreference == Preferences.Constant.PREF_FOLDER_NAMING_CONTENT_AUTH_TITLE_ID) { - result = result + content.getAuthor().replaceAll(AUTHORIZED_CHARS, "_") + " - "; + result += content.getAuthor().replaceAll(AUTHORIZED_CHARS, "_") + " - "; } if (folderNamingPreference == Preferences.Constant.PREF_FOLDER_NAMING_CONTENT_AUTH_TITLE_ID || folderNamingPreference == Preferences.Constant.PREF_FOLDER_NAMING_CONTENT_TITLE_ID) { - result = result + content.getTitle().replaceAll(AUTHORIZED_CHARS, "_") + " - "; + result += content.getTitle().replaceAll(AUTHORIZED_CHARS, "_") + " - "; } - result = result + "[" + content.getUniqueSiteId() + "]"; + + // Unique content ID + String suffix = "[" + formatBookId(content) + "]"; // Truncate folder dir to something manageable for Windows // If we are to assume NTFS and Windows, then the fully qualified file, with it's drivename, path, filename, and extension, altogether is limited to 260 characters. int truncLength = Preferences.getFolderTruncationNbChars(); - if (truncLength > 0) { - if (result.length() - siteFolder.length() > truncLength) - result = result.substring(0, siteFolder.length() + truncLength - 1); - } + int titleLength = result.length() - siteFolder.length(); + if ((truncLength > 0) && ((titleLength + suffix.length()) > truncLength)) + result = result.substring(0, siteFolder.length() + truncLength - suffix.length() - 1); + + result += suffix; return result; } + @SuppressWarnings("squid:S2676") // Math.abs is used for formatting purposes only + private static String formatBookId(Content content) { + String id = content.getUniqueSiteId(); + // For certain sources (8muses, fakku), unique IDs are strings that may be very long + // => shorten them by using their hashCode + if (id.length() > 10) id = Helper.formatIntAsStr(Math.abs(id.hashCode()), 10); + return id; + } + public static File getDefaultDir(Context context, String dir) { File file; try { - file = new File(getExternalStorageDirectory() + "/" - + Consts.DEFAULT_LOCAL_DIRECTORY + "/" + dir); + file = new File(getExternalStorageDirectory() + File.separator + + Consts.DEFAULT_LOCAL_DIRECTORY + File.separator + dir); } catch (Exception e) { file = context.getDir("", Context.MODE_PRIVATE); - file = new File(file, "/" + Consts.DEFAULT_LOCAL_DIRECTORY); + file = new File(file, File.separator + Consts.DEFAULT_LOCAL_DIRECTORY); } if (!file.exists() && !FileUtil.makeDir(file)) { file = context.getDir("", Context.MODE_PRIVATE); - file = new File(file, "/" + Consts.DEFAULT_LOCAL_DIRECTORY + "/" + dir); + file = new File(file, File.separator + Consts.DEFAULT_LOCAL_DIRECTORY + File.separator + dir); if (!file.exists()) { FileUtil.makeDir(file); } @@ -441,7 +461,7 @@ public static File getDefaultDir(Context context, String dir) { return file; } - public static File getSiteDownloadDir(Context context, Site site) { + public static File getOrCreateSiteDownloadDir(Context context, Site site) { File file; String settingDir = Preferences.getRootFolderName(); String folderDir = site.getFolder(); @@ -498,19 +518,12 @@ public static String getThumb(Content content) { // NB : ideal would be to get the content-type of the resource behind coverUrl, but that's too time-consuming if (extension.isEmpty() || extension.contains("/")) extension = "jpg"; - File f = new File(Preferences.getRootFolderName(), content.getStorageFolder() + "/thumb." + extension); + File f = new File(Preferences.getRootFolderName(), content.getStorageFolder() + File.separator + "thumb." + extension); return f.exists() ? f.getAbsolutePath() : coverUrl; } - /** - * Open the given content using the viewer defined in user preferences - * - * @param context Context - * @param content Content to be opened - */ - public static void openContent(final Context context, Content content) { - Timber.d("Opening: %s from: %s", content.getTitle(), content.getStorageFolder()); - + @Nullable + public static File[] getPictureFilesFromContent(Content content) { String rootFolderName = Preferences.getRootFolderName(); File dir = new File(rootFolderName, content.getStorageFolder()); @@ -518,13 +531,10 @@ public static void openContent(final Context context, Content content) { if (isSAF() && getExtSdCardFolder(new File(rootFolderName)) == null) { Timber.d("File not found!! Exiting method."); ToastUtil.toast(R.string.sd_access_error); - return; + return null; } - ToastUtil.toast("Opening: " + content.getTitle()); - - File imageFile = null; - File[] files = dir.listFiles( + return dir.listFiles( file -> (file.isFile() && !file.getName().toLowerCase().startsWith("thumb") && ( file.getName().toLowerCase().endsWith("jpg") @@ -534,45 +544,39 @@ public static void openContent(final Context context, Content content) { ) ) ); - if (files != null && files.length > 0) { - Arrays.sort(files); - imageFile = files[0]; - } - if (imageFile == null) { - String message = context.getString(R.string.image_file_not_found) - .replace("@dir", dir.getAbsolutePath()); - ToastUtil.toast(context, message); - } else { - int readContentPreference = Preferences.getContentReadAction(); - if (readContentPreference == Preferences.Constant.PREF_READ_CONTENT_PHONE_DEFAULT_VIEWER) { - openFile(context, imageFile); - } else if (readContentPreference == Preferences.Constant.PREF_READ_CONTENT_PERFECT_VIEWER) { - openPerfectViewer(context, imageFile); - } else if (readContentPreference == Preferences.Constant.PREF_READ_CONTENT_HENTOID_VIEWER) { - openHentoidViewer(context, content, files); - } - } + } - // TODO - properly dispose this Completable (for best practices' sake, even though it hasn't triggered any leak so far) - Completable.fromRunnable(() -> updateContentReads(context, content.getId(), dir)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(); + /** + * Open the given content using the viewer defined in user preferences + * + * @param context Context + * @param content Content to be opened + */ + public static void openContent(final Context context, Content content) { + openContent(context, content, null); } - private static void updateContentReads(Context context, long contentId, File dir) { + public static void openContent(final Context context, Content content, Bundle searchParams) { + Timber.d("Opening: %s from: %s", content.getTitle(), content.getStorageFolder()); + ToastUtil.toast("Opening: " + content.getTitle()); + + openHentoidViewer(context, content, searchParams); + } + + @Nullable + public static Content updateContentReads(@Nonnull Context context, long contentId) { ObjectBoxDB db = ObjectBoxDB.getInstance(context); Content content = db.selectContentById(contentId); if (content != null) { content.increaseReads().setLastReadDate(new Date().getTime()); db.updateContentReads(content); - try { - JsonHelper.saveJson(content.preJSONExport(), dir); - } catch (IOException e) { - Timber.e(e, "Error while writing to %s", dir.getAbsolutePath()); - } + if (!content.getJsonUri().isEmpty()) FileHelper.updateJson(context, content); + else FileHelper.createJson(content); + + return content; } + return null; } /** @@ -582,11 +586,9 @@ private static void updateContentReads(Context context, long contentId, File dir * @param aFile File to be opened */ public static void openFile(Context context, File aFile) { - Intent myIntent = new Intent(Intent.ACTION_VIEW); File file = new File(aFile.getAbsolutePath()); - String extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(file).toString()); - String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - myIntent.setDataAndType(Uri.fromFile(file), mimeType); + Intent myIntent = new Intent(Intent.ACTION_VIEW, FileProvider.getUriForFile(context, AUTHORITY, file)); + myIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); try { context.startActivity(myIntent); } catch (ActivityNotFoundException e) { @@ -595,41 +597,16 @@ public static void openFile(Context context, File aFile) { } } - /** - * Open PerfectViewer telling it to display the given image - * - * @param context Context - * @param firstImage Image to be displayed - */ - private static void openPerfectViewer(Context context, File firstImage) { - try { - Intent intent = context - .getPackageManager() - .getLaunchIntentForPackage("com.rookiestudio.perfectviewer"); - if (intent != null) { - intent.setAction(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.fromFile(firstImage), "image/*"); - context.startActivity(intent); - } - } catch (Exception e) { - ToastUtil.toast(context, R.string.error_open_perfect_viewer); - } - } - /** * Open built-in image viewer telling it to display the images of the given Content * - * @param context Context - * @param content Content to be displayed - * @param imageFiles Image files to be shown + * @param context Context + * @param content Content to be displayed */ - private static void openHentoidViewer(@NonNull Context context, @NonNull Content content, @NonNull File[] imageFiles) { - List imagesLocations = new ArrayList<>(); - for (File f : imageFiles) imagesLocations.add(f.getAbsolutePath()); - + private static void openHentoidViewer(@NonNull Context context, @NonNull Content content, Bundle searchParams) { ImageViewerActivityBundle.Builder builder = new ImageViewerActivityBundle.Builder(); builder.setContentId(content.getId()); - builder.setUrisStr(imagesLocations); + if (searchParams != null) builder.setSearchParams(searchParams); Intent viewer = new Intent(context, ImageViewerActivity.class); viewer.putExtras(builder.getBundle()); @@ -644,15 +621,18 @@ private static void openHentoidViewer(@NonNull Context context, @NonNull Content * @return Extension of the given filename */ public static String getExtension(String fileName) { - return fileName.contains(".") ? fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(Locale.getDefault()) : ""; + return fileName.contains(".") ? fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(Locale.getDefault()) : ""; + } + + public static String getFileNameWithoutExtension(String fileName) { + return fileName.contains(".") ? fileName.substring(0, fileName.lastIndexOf('.')) : fileName; } public static void archiveContent(final Context context, Content content) { Timber.d("Building file list for: %s", content.getTitle()); // Build list of files - String settingDir = Preferences.getRootFolderName(); - File dir = new File(settingDir, content.getStorageFolder()); + File dir = getContentDownloadDir(content); File[] files = dir.listFiles(); if (files != null && files.length > 0) { @@ -757,6 +737,27 @@ public static boolean renameDirectory(File srcDir, File destDir) { return false; } + public static void updateJson(@Nonnull Context context, @Nonnull Content content) { + DocumentFile file = DocumentFile.fromSingleUri(context, Uri.parse(content.getJsonUri())); + if (null == file) + throw new InvalidParameterException("'" + content.getJsonUri() + "' does not refer to a valid file"); + + try { + JsonHelper.updateJson(content.preJSONExport(), file); + } catch (IOException e) { + Timber.e(e, "Error while writing to %s", content.getJsonUri()); + } + } + + public static void createJson(@Nonnull Content content) { + File dir = FileHelper.getContentDownloadDir(content); + try { + JsonHelper.createJson(content.preJSONExport(), dir); + } catch (IOException e) { + Timber.e(e, "Error while writing to %s", dir.getAbsolutePath()); + } + } + private static class AsyncUnzip extends ZipUtil.ZipTask { final Context context; // TODO - omg leak ! final File dest; @@ -773,7 +774,8 @@ protected void onPostExecute(Boolean aBoolean) { // Hentoid is FileProvider ready!! sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, AUTHORITY, dest)); - sendIntent.setType(MimeTypes.getMimeType(dest)); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileHelper.getExtension(dest.getName())); + sendIntent.setType(mimeType); context.startActivity(sendIntent); } @@ -789,4 +791,8 @@ public static void createFileWithMsg(@Nonnull String file, String msg) { } } + @Nullable + public static DocumentFile getDocumentFile(@Nonnull final File file, final boolean isDirectory) { + return FileUtil.getDocumentFile(file, isDirectory); + } } diff --git a/app/src/main/java/me/devsaki/hentoid/util/FileUtil.java b/app/src/main/java/me/devsaki/hentoid/util/FileUtil.java index 3b48b34251..77dcc334d5 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/FileUtil.java +++ b/app/src/main/java/me/devsaki/hentoid/util/FileUtil.java @@ -3,13 +3,16 @@ import android.content.Context; import android.net.Uri; import android.os.Build; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.provider.DocumentFile; +import android.webkit.MimeTypeMap; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; import org.apache.commons.io.FileUtils; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -48,17 +51,37 @@ static boolean sync(@NonNull final FileOutputStream stream) { /** * Get the DocumentFile corresponding to the given file. - * If the file does not exist, it is created. + * If the file does not exist, null is returned. * * @param file The file. * @param isDirectory flag indicating if the given file should be a directory. * @return The DocumentFile. */ @Nullable - private static DocumentFile getDocumentFile(@Nonnull final File file, final boolean isDirectory) { + static DocumentFile getDocumentFile(@Nonnull final File file, final boolean isDirectory) { + return getOrCreateDocumentFile(file, isDirectory, false); + } + + @Nullable + private static DocumentFile getOrCreateDocumentFile(@Nonnull final File file, boolean isDirectory) { + return getOrCreateDocumentFile(file, isDirectory, true); + } + + /** + * Get the DocumentFile corresponding to the given file. + * If the file does not exist, null is returned. + * + * @param file The file. + * @param isDirectory flag indicating if the given file should be a directory. + * @return The DocumentFile. + */ + @Nullable + private static DocumentFile getOrCreateDocumentFile(@Nonnull final File file, boolean isDirectory, boolean canCreate) { String baseFolder = FileHelper.getExtSdCardFolder(file); boolean returnSDRoot = false; - if (baseFolder == null) return null; + + // File is from phone memory + if (baseFolder == null) return DocumentFile.fromFile(file); String relativePath = ""; // Path of the file relative to baseFolder try { @@ -92,7 +115,7 @@ private static DocumentFile getDocumentFile(@Nonnull final File file, final bool } } - return documentFileHelper(sdStorageUri, returnSDRoot, relativePath, isDirectory); + return getOrCreateFromComponents(sdStorageUri, returnSDRoot, relativePath, isDirectory, canCreate); } /** @@ -103,10 +126,13 @@ private static DocumentFile getDocumentFile(@Nonnull final File file, final bool * @param returnRoot True if method has just to return the DocumentFile representing the given root * @param relativePath Relative path to the Document to be found/created (relative to given root) * @param isDirectory True if the given elements are supposed to be a directory; false if they are supposed to be a file + * @param canCreate Behaviour when not found : True => creates a new file/folder / False => returns null * @return DocumentFile corresponding to the given file. */ - private static DocumentFile documentFileHelper(@Nonnull Uri rootURI, boolean returnRoot, - String relativePath, boolean isDirectory) { + @Nullable + private static DocumentFile getOrCreateFromComponents(@Nonnull Uri rootURI, boolean returnRoot, + String relativePath, boolean isDirectory, + boolean canCreate) { // start with root and then parse through document tree. Context context = HentoidApp.getAppContext(); DocumentFile document = DocumentFile.fromTreeUri(context, rootURI); @@ -114,7 +140,7 @@ private static DocumentFile documentFileHelper(@Nonnull Uri rootURI, boolean ret if (null == document) return null; if (returnRoot || null == relativePath || relativePath.isEmpty()) return document; - String[] parts = relativePath.split("/"); + String[] parts = relativePath.split(File.separator); for (int i = 0; i < parts.length; i++) { DocumentFile nextDocument = document.findFile(parts[i]); // The folder might exist in its capitalized version (might happen with legacy installs from the FakkuDroid era) @@ -123,18 +149,22 @@ private static DocumentFile documentFileHelper(@Nonnull Uri rootURI, boolean ret // The folder definitely doesn't exist at all if (null == nextDocument) { - Timber.d("Document %s - part #%s : '%s' not found; creating", document.getName(), String.valueOf(i), parts[i]); - - if ((i < parts.length - 1) || isDirectory) { - nextDocument = document.createDirectory(parts[i]); - if (null == nextDocument) { - Timber.e("Failed to create subdirectory %s/%s", document.getName(), parts[i]); + if (canCreate) { + Timber.d("Document %s - part #%s : '%s' not found; creating", document.getName(), String.valueOf(i), parts[i]); + + if ((i < parts.length - 1) || isDirectory) { + nextDocument = document.createDirectory(parts[i]); + if (null == nextDocument) { + Timber.e("Failed to create subdirectory %s/%s", document.getName(), parts[i]); + } + } else { + nextDocument = document.createFile("image", parts[i]); + if (null == nextDocument) { + Timber.e("Failed to create file %s/image%s", document.getName(), parts[i]); + } } } else { - nextDocument = document.createFile("image", parts[i]); - if (null == nextDocument) { - Timber.e("Failed to create file %s/image%s", document.getName(), parts[i]); - } + return null; } } document = nextDocument; @@ -156,11 +186,10 @@ static OutputStream getOutputStream(@NonNull final File target) throws IOExcepti } catch (IOException e) { Timber.d("Could not open file (expected)"); } - try { if (Build.VERSION.SDK_INT >= LOLLIPOP) { // Storage Access Framework - DocumentFile targetDocument = getDocumentFile(target, false); + DocumentFile targetDocument = getOrCreateDocumentFile(target, false); if (targetDocument != null) { Context context = HentoidApp.getAppContext(); return context.getContentResolver().openOutputStream( @@ -175,6 +204,11 @@ static OutputStream getOutputStream(@NonNull final File target) throws IOExcepti throw new IOException("Error while attempting to get file : " + target.getAbsolutePath()); } + static OutputStream getOutputStream(@NonNull final DocumentFile target) throws FileNotFoundException { + Context context = HentoidApp.getAppContext(); + return context.getContentResolver().openOutputStream(target.getUri()); + } + static InputStream getInputStream(@NonNull final File target) throws IOException { try { return FileUtils.openInputStream(target); @@ -185,7 +219,7 @@ static InputStream getInputStream(@NonNull final File target) throws IOException try { if (Build.VERSION.SDK_INT >= LOLLIPOP) { // Storage Access Framework - DocumentFile targetDocument = getDocumentFile(target, false); + DocumentFile targetDocument = getOrCreateDocumentFile(target, false); if (targetDocument != null) { Context context = HentoidApp.getAppContext(); return context.getContentResolver().openInputStream( @@ -221,12 +255,15 @@ static boolean makeFile(@NonNull final File file) { // Try with Storage Access Framework. if (Build.VERSION.SDK_INT >= LOLLIPOP) { - DocumentFile document = getDocumentFile(file.getParentFile(), true); - // getDocumentFile implicitly creates the directory. + DocumentFile document = getOrCreateDocumentFile(file.getParentFile(), true); + // getOrCreateDocumentFile implicitly creates the directory. try { if (document != null) { - return document.createFile( - MimeTypes.getMimeType(file), file.getName()) != null; + //MimeTypes.getMimeType(file) + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FileHelper.getExtension(file.getName())); + if (null == mimeType) mimeType = "application/octet-stream"; + + return document.createFile(mimeType, file.getName()) != null; } } catch (Exception e) { return false; @@ -255,8 +292,8 @@ static boolean makeDir(@NonNull final File file) { // Try with Storage Access Framework. if (Build.VERSION.SDK_INT >= LOLLIPOP) { - DocumentFile document = getDocumentFile(file, true); - // getDocumentFile implicitly creates the directory. + DocumentFile document = getOrCreateDocumentFile(file, true); + // getOrCreateDocumentFile implicitly creates the directory. if (document != null) { return document.exists(); } @@ -277,7 +314,7 @@ static boolean deleteFile(@NonNull final File file) { static boolean deleteWithSAF(File file) { if (Build.VERSION.SDK_INT >= LOLLIPOP) { - DocumentFile document = getDocumentFile(file, true); + DocumentFile document = getOrCreateDocumentFile(file, true); if (document != null) { return document.delete(); } @@ -288,7 +325,7 @@ static boolean deleteWithSAF(File file) { static boolean renameWithSAF(File srcDir, String newName) { if (Build.VERSION.SDK_INT >= LOLLIPOP) { - DocumentFile srcDocument = getDocumentFile(srcDir, true); + DocumentFile srcDocument = getOrCreateDocumentFile(srcDir, true); if (srcDocument != null) return srcDocument.renameTo(newName); } return false; diff --git a/app/src/main/java/me/devsaki/hentoid/util/Helper.java b/app/src/main/java/me/devsaki/hentoid/util/Helper.java index 1cbb44f0ff..ff095c61cd 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/Helper.java +++ b/app/src/main/java/me/devsaki/hentoid/util/Helper.java @@ -14,15 +14,17 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.support.v4.graphics.drawable.DrawableCompat; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; import android.text.Html; import android.text.Spanned; import android.util.DisplayMetrics; import android.webkit.WebResourceResponse; import android.widget.Toast; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -32,6 +34,8 @@ import java.util.List; import java.util.Locale; +import javax.annotation.Nonnull; + import me.devsaki.hentoid.HentoidApp; import me.devsaki.hentoid.R; import me.devsaki.hentoid.activities.IntroActivity; @@ -49,8 +53,6 @@ /** * Created by avluis on 06/05/2016. * Utility class - *

- * TODO: Add additional image viewers. */ public final class Helper { @@ -143,7 +145,7 @@ public static WebResourceResponse getWebResourceResponseFromAsset(Site site, Str String pathPrefix = site.getDescription().toLowerCase(Locale.US) + "/"; String file = pathPrefix + filename; try { - File asset = new File(context.getExternalCacheDir() + "/" + file); + File asset = new File(context.getExternalCacheDir() + File.separator + file); FileInputStream stream = new FileInputStream(asset); return Helper.getUtf8EncodedWebResourceResponse(stream, type); } catch (IOException e) { @@ -178,21 +180,21 @@ public static Spanned fromHtml(String source) { public enum TYPE {JS, CSS, HTML, PLAIN} public static String capitalizeString(String s) { - if (s == null || s.length() == 0) return s; + if (s == null || s.isEmpty()) return s; else if (s.length() == 1) return s.toUpperCase(); else return s.substring(0, 1).toUpperCase() + s.toLowerCase().substring(1); } /** - * Transforms the given string to format with a given length + * Transforms the given int to format with a given length * - If the given length is shorter than the actual length of the string, it will be truncated - * - If the given length is longer than the actual length of the string, it will be right/left-padded with a given character + * - If the given length is longer than the actual length of the string, it will be left-padded with the character 0 * * @param value String to transform * @param length Target length of the final string * @return Reprocessed string of given length, according to rules documented in the method description */ - public static String compensateStringLength(int value, int length) { + public static String formatIntAsStr(int value, int length) { String result = String.valueOf(value); if (result.length() > length) { @@ -208,7 +210,7 @@ public static String buildListAsString(List list) { return buildListAsString(list, ""); } - public static String buildListAsString(List list, String valueDelimiter) { + private static String buildListAsString(List list, String valueDelimiter) { StringBuilder str = new StringBuilder(); if (list != null) { @@ -239,40 +241,6 @@ private static List extractAttributeIdsByType(List attrs, Attri return result; } - public static Uri buildSearchUri(List attributes) { - AttributeMap metadataMap = new AttributeMap(); - metadataMap.addAll(attributes); - - Uri.Builder searchUri = new Uri.Builder() - .scheme("search") - .authority("hentoid"); - - for (AttributeType attrType : metadataMap.keySet()) { - List attrs = metadataMap.get(attrType); - for (Attribute attr : attrs) - searchUri.appendQueryParameter(attrType.name(), attr.getId() + ";" + attr.getName()); - } - return searchUri.build(); - } - - public static List parseSearchUri(Uri uri) { - List result = new ArrayList<>(); - - if (uri != null) - for (String typeStr : uri.getQueryParameterNames()) { - AttributeType type = AttributeType.searchByName(typeStr); - if (type != null) - for (String attrStr : uri.getQueryParameters(typeStr)) { - String[] attrParams = attrStr.split(";"); - if (2 == attrParams.length) { - result.add(new Attribute(type, attrParams[1]).setId(Long.parseLong(attrParams[0]))); - } - } - } - - return result; - } - public static String decode64(String encodedString) { // Pure Java //byte[] decodedBytes = org.apache.commons.codec.binary.Base64.decodeBase64(encodedString); @@ -330,4 +298,26 @@ public static void openUrl(Context context, String url) { ToastUtil.toast(context, R.string.error_open, Toast.LENGTH_LONG); } } + + public static float coerceIn(float value, float min, float max) { + if (value < min) return min; + else if (value > max) return max; + else return value; + } + + public static List duplicateInputStream(@Nonnull InputStream stream, int numberDuplicates) throws IOException { + List result = new ArrayList<>(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + byte[] buffer = new byte[1024]; + int len; + while ((len = stream.read(buffer)) > -1) baos.write(buffer, 0, len); + baos.flush(); + + for (int i = 0; i < numberDuplicates; i++) + result.add(new ByteArrayInputStream(baos.toByteArray())); + + return result; + } } diff --git a/app/src/main/java/me/devsaki/hentoid/util/HttpHelper.java b/app/src/main/java/me/devsaki/hentoid/util/HttpHelper.java index 81b1a4168e..49636bda20 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/HttpHelper.java +++ b/app/src/main/java/me/devsaki/hentoid/util/HttpHelper.java @@ -1,6 +1,7 @@ package me.devsaki.hentoid.util; import android.util.Pair; +import android.webkit.WebResourceResponse; import com.google.gson.Gson; @@ -8,17 +9,21 @@ import org.jsoup.nodes.Document; import java.io.IOException; +import java.io.InputStream; import java.util.List; import javax.annotation.Nullable; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.Response; import okhttp3.ResponseBody; public class HttpHelper { private static final int TIMEOUT = 30000; // 30 seconds + public static final String HEADER_COOKIE_KEY = "cookie"; + public static final String HEADER_REFERER_KEY = "referer"; @Nullable public static Document getOnlineDocument(String url) throws IOException { @@ -27,7 +32,7 @@ public static Document getOnlineDocument(String url) throws IOException { @Nullable public static Document getOnlineDocument(String url, List> headers, boolean useHentoidAgent) throws IOException { - ResponseBody resource = getOnlineResource(url, headers, useHentoidAgent); + ResponseBody resource = getOnlineResource(url, headers, useHentoidAgent).body(); if (resource != null) { return Jsoup.parse(resource.string()); } @@ -41,7 +46,7 @@ public static T getOnlineJson(String url, Class type) throws IOException @Nullable public static T getOnlineJson(String url, List> headers, boolean useHentoidAgent, Class type) throws IOException { - ResponseBody resource = getOnlineResource(url, headers, useHentoidAgent); + ResponseBody resource = getOnlineResource(url, headers, useHentoidAgent).body(); if (resource != null) { String s = resource.string(); if (s.startsWith("{")) return new Gson().fromJson(s, type); @@ -49,15 +54,38 @@ public static T getOnlineJson(String url, List> headers return null; } - @Nullable - private static ResponseBody getOnlineResource(String url, List> headers, boolean useHentoidAgent) throws IOException { + public static Response getOnlineResource(String url, List> headers, boolean useHentoidAgent) throws IOException { OkHttpClient okHttp = OkHttpClientSingleton.getInstance(TIMEOUT); Request.Builder requestBuilder = new Request.Builder().url(url); if (headers != null) for (Pair header : headers) - requestBuilder.addHeader(header.first, header.second); + if (header.second != null) + requestBuilder.addHeader(header.first, header.second); requestBuilder.header("User-Agent", useHentoidAgent ? Consts.USER_AGENT : Consts.USER_AGENT_NEUTRAL); Request request = requestBuilder.get().build(); - return okHttp.newCall(request).execute().body(); + return okHttp.newCall(request).execute(); + } + + /** + * Convert OkHttp {@link Response} into a {@link WebResourceResponse} + * + * @param resp The OkHttp {@link Response} + * @return The {@link WebResourceResponse} + */ + public static WebResourceResponse okHttpResponseToWebResourceResponse(Response resp, InputStream is) { + final String contentTypeValue = resp.header("Content-Type"); + + if (contentTypeValue != null) { + if (contentTypeValue.indexOf("charset=") > 0) { + final String[] contentTypeAndEncoding = contentTypeValue.replace("; ", ";").split(";"); + final String contentType = contentTypeAndEncoding[0]; + final String charset = contentTypeAndEncoding[1].split("=")[1]; + return new WebResourceResponse(contentType, charset, is); + } else { + return new WebResourceResponse(contentTypeValue, null, is); + } + } else { + return new WebResourceResponse("application/octet-stream", null, is); + } } } diff --git a/app/src/main/java/me/devsaki/hentoid/util/JsonHelper.java b/app/src/main/java/me/devsaki/hentoid/util/JsonHelper.java index c02c993474..b43750a7ee 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/JsonHelper.java +++ b/app/src/main/java/me/devsaki/hentoid/util/JsonHelper.java @@ -1,26 +1,19 @@ package me.devsaki.hentoid.util; +import androidx.documentfile.provider.DocumentFile; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import org.json.JSONException; -import org.json.JSONObject; - import java.io.BufferedReader; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; -import java.nio.charset.Charset; -import javax.annotation.Nullable; +import javax.annotation.Nonnull; -import okhttp3.Call; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; import timber.log.Timber; /** @@ -29,19 +22,24 @@ */ public class JsonHelper { - public static String serializeToJson(Object o) - { + public static String serializeToJson(Object o) { Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); // convert java object to JSON format, and return as a JSON formatted string return gson.toJson(o); } - public static void saveJson(K object, File dir) throws IOException { + /** + * Serialize and save the object contents to a json file in the given directory. + * The JSON file is created if it doesn't exist + * @param object Object to be serialized and saved + * @param dir Existing folder to save the JSON file to + * @param Type of the object to save + * @throws IOException If anything happens during file I/O + */ + public static File createJson(K object, File dir) throws IOException { File file = new File(dir, Consts.JSON_FILE_NAME_V2); String json = serializeToJson(object); - try (OutputStream output = FileHelper.getOutputStream(file)) { - if (output != null) { // build byte[] bytes = json.getBytes(); @@ -53,8 +51,32 @@ public static void saveJson(K object, File dir) throws IOException { Timber.w("JSON file creation failed for %s", file.getPath()); } } - // finished - // Ignore + return file; + } + + /** + * Serialize and save the object contents to an existing file using the JSON format + * @param object Object to be serialized and saved + * @param file Existing file to save to + * @param Type of the object to save + * @throws IOException If anything happens during file I/O + */ + static void updateJson(K object, @Nonnull DocumentFile file) throws IOException { + try (OutputStream output = FileHelper.getOutputStream(file)) { + if (output != null) { + String json = serializeToJson(object); + // build + byte[] bytes = json.getBytes(); + // write + output.write(bytes); + FileHelper.sync(output); + output.flush(); + } else { + Timber.w("JSON file creation failed for %s", file.getUri()); + } + } catch (FileNotFoundException e) { + Timber.e(e); + } } public static T jsonToObject(File f, Class type) throws IOException { @@ -70,52 +92,4 @@ public static T jsonToObject(File f, Class type) throws IOException { Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); return gson.fromJson(json.toString(), type); } - - @Nullable - synchronized static JSONObject jsonReader(String jsonURL) throws IOException { - try { - Request request = new Request.Builder() - .url(jsonURL) - .addHeader("User-Agent", Consts.USER_AGENT) - .addHeader("Data-type", "application/json") - .build(); - - Call okHttpCall = OkHttpClientSingleton.getInstance().newCall(request); - - Response okHttpResponse = okHttpCall.execute(); - - int responseCode = okHttpResponse.code(); - Timber.d("HTTP Response: %s", responseCode); - if (404 == responseCode) { - return null; - } - - ResponseBody body = okHttpResponse.body(); - if (body != null) { - return new JSONObject(readInputStream(body.byteStream())); - } else { - Timber.e("JSON request body is null"); - return null; - } - } catch (JSONException e) { - Timber.e(e, "JSON file not properly formatted"); - } - - return null; - } - - private static String readInputStream(InputStream stream) throws IOException { - StringBuilder builder = new StringBuilder(stream.available()); - BufferedReader reader = new BufferedReader(new InputStreamReader(stream, - Charset.forName("UTF-8"))); - String line = reader.readLine(); - - while (line != null) { - builder.append(line); - line = reader.readLine(); - } - reader.close(); - - return builder.toString(); - } } diff --git a/app/src/main/java/me/devsaki/hentoid/util/LogUtil.java b/app/src/main/java/me/devsaki/hentoid/util/LogUtil.java index 9867eea9fc..98739d5c29 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/LogUtil.java +++ b/app/src/main/java/me/devsaki/hentoid/util/LogUtil.java @@ -1,7 +1,7 @@ package me.devsaki.hentoid.util; import android.content.Context; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import java.io.File; import java.util.List; diff --git a/app/src/main/java/me/devsaki/hentoid/util/MimeTypes.java b/app/src/main/java/me/devsaki/hentoid/util/MimeTypes.java deleted file mode 100644 index eb844cdef4..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/util/MimeTypes.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2013 Simple Explorer - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, - * MA 02110-1301, USA. - */ - -package me.devsaki.hentoid.util; - -import android.webkit.MimeTypeMap; - -import java.io.File; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; - -/** - * Created by avluis on 08/24/2016. - * Mime Types - */ -public final class MimeTypes { - - // TODO - why using this rather than android.webkit.MimeTypeMap? - - private static final String ALL_MIME_TYPES = "*/*"; - - private static final HashMap MIME_TYPES = new HashMap<>(); - - static { - MIME_TYPES.put("asm", "text/x-asm"); - MIME_TYPES.put("def", "text/plain"); - MIME_TYPES.put("in", "text/plain"); - MIME_TYPES.put("list", "text/plain"); - MIME_TYPES.put("log", "text/plain"); - MIME_TYPES.put("pl", "text/plain"); - MIME_TYPES.put("prop", "text/plain"); - MIME_TYPES.put("properties", "text/plain"); - MIME_TYPES.put("rc", "text/plain"); - - MIME_TYPES.put("epub", "application/epub+zip"); - MIME_TYPES.put("ibooks", "application/x-ibooks+zip"); - - MIME_TYPES.put("ifb", "text/calendar"); - MIME_TYPES.put("eml", "message/rfc822"); - MIME_TYPES.put("msg", "application/vnd.ms-outlook"); - - MIME_TYPES.put("ace", "application/x-ace-compressed"); - MIME_TYPES.put("bz", "application/x-bzip"); - MIME_TYPES.put("bz2", "application/x-bzip2"); - MIME_TYPES.put("cab", "application/vnd.ms-cab-compressed"); - MIME_TYPES.put("gz", "application/x-gzip"); - MIME_TYPES.put("lrf", "application/octet-stream"); - MIME_TYPES.put("jar", "application/java-archive"); - MIME_TYPES.put("xz", "application/x-xz"); - MIME_TYPES.put("Z", "application/x-compress"); - - MIME_TYPES.put("bat", "application/x-msdownload"); - MIME_TYPES.put("ksh", "text/plain"); - MIME_TYPES.put("sh", "application/x-sh"); - - MIME_TYPES.put("db", "application/octet-stream"); - MIME_TYPES.put("db3", "application/octet-stream"); - - MIME_TYPES.put("otf", "application/x-font-otf"); - MIME_TYPES.put("ttf", "application/x-font-ttf"); - MIME_TYPES.put("psf", "application/x-font-linux-psf"); - - MIME_TYPES.put("cgm", "image/cgm"); - MIME_TYPES.put("btif", "image/prs.btif"); - MIME_TYPES.put("dwg", "image/vnd.dwg"); - MIME_TYPES.put("dxf", "image/vnd.dxf"); - MIME_TYPES.put("fbs", "image/vnd.fastbidsheet"); - MIME_TYPES.put("fpx", "image/vnd.fpx"); - MIME_TYPES.put("fst", "image/vnd.fst"); - MIME_TYPES.put("mdi", "image/vnd.ms-mdi"); - MIME_TYPES.put("npx", "image/vnd.net-fpx"); - MIME_TYPES.put("xif", "image/vnd.xiff"); - MIME_TYPES.put("pct", "image/x-pict"); - MIME_TYPES.put("pic", "image/x-pict"); - MIME_TYPES.put("jpg", "image/jpeg"); - MIME_TYPES.put("png", "image/png"); - MIME_TYPES.put("gif", "image/gif"); - - MIME_TYPES.put("adp", "audio/adpcm"); - MIME_TYPES.put("au", "audio/basic"); - MIME_TYPES.put("snd", "audio/basic"); - MIME_TYPES.put("m2a", "audio/mpeg"); - MIME_TYPES.put("m3a", "audio/mpeg"); - MIME_TYPES.put("oga", "audio/ogg"); - MIME_TYPES.put("spx", "audio/ogg"); - MIME_TYPES.put("aac", "audio/x-aac"); - MIME_TYPES.put("mka", "audio/x-matroska"); - - MIME_TYPES.put("jpgv", "video/jpeg"); - MIME_TYPES.put("jpgm", "video/jpm"); - MIME_TYPES.put("jpm", "video/jpm"); - MIME_TYPES.put("mj2", "video/mj2"); - MIME_TYPES.put("mjp2", "video/mj2"); - MIME_TYPES.put("mpa", "video/mpeg"); - MIME_TYPES.put("ogv", "video/ogg"); - MIME_TYPES.put("flv", "video/x-flv"); - MIME_TYPES.put("mkv", "video/x-matroska"); - } - - static String getMimeType(File file) { - if (file.isDirectory()) { - return null; - } - - String type = ALL_MIME_TYPES; - final String extension = FileHelper.getExtension(file.getName()); - - if (!extension.isEmpty()) { - final String extensionLowerCase = extension.toLowerCase(Locale.getDefault()); - final MimeTypeMap mime = MimeTypeMap.getSingleton(); - type = mime.getMimeTypeFromExtension(extensionLowerCase); - - if (type == null) { - type = MIME_TYPES.get(extensionLowerCase); - } - } - - if (type == null) { - type = ALL_MIME_TYPES; - } - - return type; - } - - public static boolean mimeTypeMatch(String mime, String input) { - return Pattern.matches(mime.replace("*", ".*"), input); - } - - public static String getExtensionFromMimeType(String mime) { - Set> set = MIME_TYPES.entrySet(); - - for (Map.Entry entry : set) { - if (entry.getValue().equals(mime)) return entry.getKey(); - } - - return ""; - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/util/OkHttpClientSingleton.java b/app/src/main/java/me/devsaki/hentoid/util/OkHttpClientSingleton.java index 5269ed9454..5f36f8e0fc 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/OkHttpClientSingleton.java +++ b/app/src/main/java/me/devsaki/hentoid/util/OkHttpClientSingleton.java @@ -18,12 +18,13 @@ public class OkHttpClientSingleton { private static volatile SparseArray instance = new SparseArray<>(); - private static int DEFAULT_TIMEOUT = 20 * 1000; + private OkHttpClientSingleton() { } public static OkHttpClient getInstance() { + int DEFAULT_TIMEOUT = 20 * 1000; return getInstance(DEFAULT_TIMEOUT); } diff --git a/app/src/main/java/me/devsaki/hentoid/util/PermissionUtil.java b/app/src/main/java/me/devsaki/hentoid/util/PermissionUtil.java index 1e67782d9d..d86cbd2413 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/PermissionUtil.java +++ b/app/src/main/java/me/devsaki/hentoid/util/PermissionUtil.java @@ -3,16 +3,13 @@ import android.Manifest; import android.app.Activity; import android.content.Context; -import android.os.Build; -import android.support.v4.app.ActivityCompat; +import androidx.core.app.ActivityCompat; import static android.content.pm.PackageManager.PERMISSION_GRANTED; public class PermissionUtil { public static boolean requestExternalStoragePermission(Activity activity, int permissionRequestCode) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return true; - if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) { @@ -26,7 +23,6 @@ public static boolean requestExternalStoragePermission(Activity activity, int pe } public static boolean checkExternalStoragePermission(Context context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return true; return ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED; } } diff --git a/app/src/main/java/me/devsaki/hentoid/util/Preferences.java b/app/src/main/java/me/devsaki/hentoid/util/Preferences.java index fa0c0712d3..d627f8fee0 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/Preferences.java +++ b/app/src/main/java/me/devsaki/hentoid/util/Preferences.java @@ -2,11 +2,14 @@ import android.content.Context; import android.content.SharedPreferences; +import android.os.Build; import android.preference.PreferenceManager; import me.devsaki.hentoid.BuildConfig; import timber.log.Timber; +import static android.os.Build.VERSION_CODES.P; + /** * Created by Shiro on 2/21/2018. * Decorator class that wraps a SharedPreference to implement properties @@ -127,18 +130,6 @@ static boolean setRootFolderName(String rootFolderName) { .commit(); } - public static int getContentReadAction() { - return Integer.parseInt( - sharedPreferences.getString(Key.PREF_READ_CONTENT_LISTS, - Default.PREF_READ_CONTENT_ACTION + "")); - } - - public static void setContentReadAction(int contentReadAction) { - sharedPreferences.edit() - .putString(Key.PREF_READ_CONTENT_LISTS, Integer.toString(contentReadAction)) - .apply(); - } - public static int getWebViewInitialZoom() { return Integer.parseInt( sharedPreferences.getString( @@ -152,10 +143,6 @@ public static boolean getWebViewOverview() { Default.PREF_WEBVIEW_OVERRIDE_OVERVIEW_DEFAULT); } - public static boolean isUseSfw() { - return sharedPreferences.getBoolean(Key.PREF_USE_SFW, Default.PREF_USE_SFW_DEFAULT); - } - public static int getDownloadThreadCount() { return Integer.parseInt(sharedPreferences.getString(Key.PREF_DL_THREADS_QUANTITY_LISTS, Default.PREF_DL_THREADS_QUANTITY_DEFAULT + "")); @@ -234,13 +221,43 @@ public static void setViewerDisplayPageNum(boolean displayPageNum) { .apply(); } - public static boolean hasViewerChoiceBeenDisplayed() { - return sharedPreferences.getBoolean(Key.VIEWER_CHOICE_DISPLAYED, false); + public static boolean isViewerTapTransitions() { + return sharedPreferences.getBoolean(Key.PREF_VIEWER_TAP_TRANSITIONS, Default.PREF_VIEWER_TAP_TRANSITIONS); + } + + public static void setViewerTapTransitions(boolean tapTransitions) { + sharedPreferences.edit() + .putBoolean(Key.PREF_VIEWER_TAP_TRANSITIONS, tapTransitions) + .apply(); + } + + public static boolean isOpenBookInGalleryMode() { + return sharedPreferences.getBoolean(Key.PREF_VIEWER_OPEN_GALLERY, Default.PREF_VIEWER_OPEN_GALLERY); + } + + public static void setOpenBookInGalleryMode(boolean openBookInGalleryMode) { + sharedPreferences.edit() + .putBoolean(Key.PREF_VIEWER_OPEN_GALLERY, openBookInGalleryMode) + .apply(); + } + + public static int getLastKnownAppVersionCode() { + return Integer.parseInt(sharedPreferences.getString(Key.LAST_KNOWN_APP_VERSION_CODE, "0")); + } + + public static void setLastKnownAppVersionCode(int versionCode) { + sharedPreferences.edit() + .putString(Key.LAST_KNOWN_APP_VERSION_CODE, Integer.toString(versionCode)) + .apply(); + } + + public static int getDarkMode() { + return Integer.parseInt(sharedPreferences.getString(Key.DARK_MODE, Integer.toString(Default.PREF_VIEWER_DARK_MODE))); } - public static void setViewerChoiceDisplayed(boolean viewerChoice) { + public static void setDarkMode(int darkMode) { sharedPreferences.edit() - .putBoolean(Key.VIEWER_CHOICE_DISPLAYED, viewerChoice) + .putString(Key.DARK_MODE, Integer.toString(darkMode)) .apply(); } @@ -261,11 +278,8 @@ public static final class Key { static final String PREF_SD_STORAGE_URI = "pref_sd_storage_uri"; static final String PREF_FOLDER_NAMING_CONTENT_LISTS = "pref_folder_naming_content_lists"; static final String PREF_SETTINGS_FOLDER = "folder"; - static final String PREF_READ_CONTENT_LISTS = "pref_read_content_lists"; - static final String PREF_CHECK_UPDATES_LISTS = "pref_check_updates_lists"; static final String PREF_WEBVIEW_OVERRIDE_OVERVIEW_LISTS = "pref_webview_override_overview_lists"; static final String PREF_WEBVIEW_INITIAL_ZOOM_LISTS = "pref_webview_initial_zoom_lists"; - public static final String PREF_USE_SFW = "pref_use_sfw"; public static final String PREF_DL_THREADS_QUANTITY_LISTS = "pref_dl_threads_quantity_lists"; static final String PREF_FOLDER_TRUNCATION_LISTS = "pref_folder_trunc_lists"; static final String PREF_VIEWER_RESUME_LAST_LEFT = "pref_viewer_resume_last_left"; @@ -274,7 +288,10 @@ public static final class Key { public static final String PREF_VIEWER_BROWSE_MODE = "pref_viewer_browse_mode"; public static final String PREF_VIEWER_FLING_FACTOR = "pref_viewer_fling_factor"; public static final String PREF_VIEWER_DISPLAY_PAGENUM = "pref_viewer_display_pagenum"; - static final String VIEWER_CHOICE_DISPLAYED = "pref_viewer_choice_displayed"; + static final String PREF_VIEWER_TAP_TRANSITIONS = "pref_viewer_tap_transitions"; + static final String PREF_VIEWER_OPEN_GALLERY = "pref_viewer_open_gallery"; + static final String LAST_KNOWN_APP_VERSION_CODE = "last_known_app_version_code"; + public static final String DARK_MODE = "pref_dark_mode"; } // IMPORTANT : Any default value change must be mirrored in res/values/strings_settings.xml @@ -287,10 +304,7 @@ public static final class Default { static final boolean PREF_ENDLESS_SCROLL_DEFAULT = true; static final boolean PREF_HIDE_RECENT_DEFAULT = (!BuildConfig.DEBUG); // Debug apps always visible to facilitate video capture static final int PREF_FOLDER_NAMING_CONTENT_DEFAULT = Constant.PREF_FOLDER_NAMING_CONTENT_AUTH_TITLE_ID; - static final int PREF_READ_CONTENT_ACTION = Constant.PREF_READ_CONTENT_HENTOID_VIEWER; - static final boolean PREF_CHECK_UPDATES_DEFAULT = true; static final boolean PREF_WEBVIEW_OVERRIDE_OVERVIEW_DEFAULT = false; - static final boolean PREF_USE_SFW_DEFAULT = false; static final int PREF_DL_THREADS_QUANTITY_DEFAULT = Constant.DOWNLOAD_THREAD_COUNT_AUTO; static final int PREF_FOLDER_TRUNCATION_DEFAULT = Constant.TRUNCATE_FOLDER_NONE; static final boolean PREF_VIEWER_RESUME_LAST_LEFT = true; @@ -298,7 +312,10 @@ public static final class Default { static final int PREF_VIEWER_IMAGE_DISPLAY = Constant.PREF_VIEWER_DISPLAY_FIT; static final int PREF_VIEWER_BROWSE_MODE = Constant.PREF_VIEWER_BROWSE_NONE; static final boolean PREF_VIEWER_DISPLAY_PAGENUM = false; - static final int PREF_VIEWER_FLING_FACTOR = 50; + static final boolean PREF_VIEWER_TAP_TRANSITIONS = true; + static final boolean PREF_VIEWER_OPEN_GALLERY = false; + static final int PREF_VIEWER_FLING_FACTOR = 0; + static final int PREF_VIEWER_DARK_MODE = (Build.VERSION.SDK_INT > P) ? Constant.DARK_MODE_DEVICE : Constant.DARK_MODE_OFF; } // IMPORTANT : Any value change must be mirrored in res/values/array_preferences.xml @@ -319,9 +336,6 @@ public static final class Constant { static final int PREF_FOLDER_NAMING_CONTENT_ID = 0; static final int PREF_FOLDER_NAMING_CONTENT_TITLE_ID = 1; static final int PREF_FOLDER_NAMING_CONTENT_AUTH_TITLE_ID = 2; - static final int PREF_READ_CONTENT_PHONE_DEFAULT_VIEWER = 0; - static final int PREF_READ_CONTENT_PERFECT_VIEWER = 1; - public static final int PREF_READ_CONTENT_HENTOID_VIEWER = 2; static final int TRUNCATE_FOLDER_NONE = 0; public static final int PREF_VIEWER_DISPLAY_FIT = 0; public static final int PREF_VIEWER_DISPLAY_FILL = 1; @@ -333,5 +347,9 @@ public static final class Constant { public static final int PREF_VIEWER_DIRECTION_RTL = 1; public static final int PREF_VIEWER_ORIENTATION_HORIZONTAL = 0; public static final int PREF_VIEWER_ORIENTATION_VERTICAL = 1; + public static final int DARK_MODE_OFF = 0; + public static final int DARK_MODE_ON = 1; + public static final int DARK_MODE_BATTERY = 2; + public static final int DARK_MODE_DEVICE = 3; } } diff --git a/app/src/main/java/me/devsaki/hentoid/util/ShortcutHelper.java b/app/src/main/java/me/devsaki/hentoid/util/ShortcutHelper.java index 2dbe2b9db8..cb8c93e470 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/ShortcutHelper.java +++ b/app/src/main/java/me/devsaki/hentoid/util/ShortcutHelper.java @@ -7,14 +7,13 @@ import android.graphics.Bitmap; import android.graphics.drawable.Icon; import android.os.Build; -import android.support.annotation.RequiresApi; -import android.support.v4.content.ContextCompat; +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; import java.util.Arrays; import me.devsaki.hentoid.R; import me.devsaki.hentoid.activities.UnlockActivity; -import me.devsaki.hentoid.activities.websites.NhentaiActivity; import me.devsaki.hentoid.enums.Site; /** @@ -26,17 +25,12 @@ public final class ShortcutHelper { @RequiresApi(api = Build.VERSION_CODES.N_MR1) public static void buildShortcuts(Context context) { // TODO: Loop across all activities - int tint_color = ContextCompat.getColor(context, R.color.accent); + int tint_color = ContextCompat.getColor(context, R.color.secondary); Bitmap nhentaiBitmap = Helper.getBitmapFromVectorDrawable(context, R.drawable.ic_menu_nhentai); nhentaiBitmap = Helper.tintBitmap(nhentaiBitmap, tint_color); Icon nhentaiIcon = Icon.createWithBitmap(nhentaiBitmap); - /* - Intent nhentaiIntent = new Intent(context, NhentaiActivity.class); - nhentaiIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - nhentaiIntent.setAction(Intent.ACTION_VIEW); - */ Intent nhentaiIntent = UnlockActivity.wrapIntent(context, Site.NHENTAI); ShortcutInfo nhentai = new ShortcutInfo.Builder(context, "nhentai") diff --git a/app/src/main/java/me/devsaki/hentoid/util/ToastUtil.java b/app/src/main/java/me/devsaki/hentoid/util/ToastUtil.java index 02bca9d37f..e7fafd84df 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/ToastUtil.java +++ b/app/src/main/java/me/devsaki/hentoid/util/ToastUtil.java @@ -1,9 +1,9 @@ package me.devsaki.hentoid.util; import android.content.Context; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.StringRes; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; import android.widget.Toast; import java.lang.annotation.Retention; diff --git a/app/src/main/java/me/devsaki/hentoid/util/VolleyOkHttp3Stack.java b/app/src/main/java/me/devsaki/hentoid/util/VolleyOkHttp3Stack.java index 6601394e70..17ee6be7c9 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/VolleyOkHttp3Stack.java +++ b/app/src/main/java/me/devsaki/hentoid/util/VolleyOkHttp3Stack.java @@ -1,5 +1,7 @@ package me.devsaki.hentoid.util; +import androidx.annotation.Nullable; + import com.android.volley.AuthFailureError; import com.android.volley.Header; import com.android.volley.Request; @@ -76,6 +78,7 @@ private static void setConnectionParametersForRequest(okhttp3.Request.Builder bu } } + @Nullable private static RequestBody createRequestBody(Request r) throws AuthFailureError { final byte[] body = r.getBody(); if (body == null) { @@ -115,7 +118,8 @@ public HttpResponse executeRequest(Request request, Map addit private List

mapHeaders(Headers responseHeaders) { List
headers = new ArrayList<>(); for (int i = 0, len = responseHeaders.size(); i < len; i++) { - final String name = responseHeaders.name(i), value = responseHeaders.value(i); + final String name = responseHeaders.name(i); + final String value = responseHeaders.value(i); if (name != null) { headers.add(new Header(name, value)); } diff --git a/app/src/main/java/me/devsaki/hentoid/util/notification/Notification.java b/app/src/main/java/me/devsaki/hentoid/util/notification/Notification.java index e620d8efcf..83ecee2b71 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/notification/Notification.java +++ b/app/src/main/java/me/devsaki/hentoid/util/notification/Notification.java @@ -1,7 +1,7 @@ package me.devsaki.hentoid.util.notification; import android.content.Context; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; /** * Implement this and use {@link NotificationManager#notify(Notification)} diff --git a/app/src/main/java/me/devsaki/hentoid/util/notification/NotificationManager.java b/app/src/main/java/me/devsaki/hentoid/util/notification/NotificationManager.java index 1781b18e54..9d39c63b91 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/notification/NotificationManager.java +++ b/app/src/main/java/me/devsaki/hentoid/util/notification/NotificationManager.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.util.notification; import android.content.Context; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationManagerCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationManagerCompat; public class NotificationManager { diff --git a/app/src/main/java/me/devsaki/hentoid/util/notification/ServiceNotificationManager.java b/app/src/main/java/me/devsaki/hentoid/util/notification/ServiceNotificationManager.java index 50e1601b54..d3414276ff 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/notification/ServiceNotificationManager.java +++ b/app/src/main/java/me/devsaki/hentoid/util/notification/ServiceNotificationManager.java @@ -1,7 +1,7 @@ package me.devsaki.hentoid.util.notification; import android.app.Service; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; public class ServiceNotificationManager extends NotificationManager { diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/AttributeViewHolder.java b/app/src/main/java/me/devsaki/hentoid/viewholders/AttributeViewHolder.java index ad8c3a110a..20b36dd562 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/AttributeViewHolder.java +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/AttributeViewHolder.java @@ -1,7 +1,6 @@ package me.devsaki.hentoid.viewholders; -import android.support.annotation.DrawableRes; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView; import android.view.View; import android.widget.TextView; diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/DrawerItemFlex.java b/app/src/main/java/me/devsaki/hentoid/viewholders/DrawerItemFlex.java new file mode 100644 index 0000000000..ed2411c3d2 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/DrawerItemFlex.java @@ -0,0 +1,74 @@ +package me.devsaki.hentoid.viewholders; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.viewholders.FlexibleViewHolder; +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.enums.DrawerItem; + +public class DrawerItemFlex extends AbstractFlexibleItem { + + private final DrawerItem item; + private boolean flag = false; + + public DrawerItemFlex(DrawerItem item) { + this.item = item; + } + + public void setFlag(boolean flag) { + this.flag = flag; + } + + @Override + public boolean equals(Object o) { + if (o instanceof DrawerItemFlex) { + DrawerItemFlex inItem = (DrawerItemFlex) o; + return this.item.equals(inItem.item); + } + return false; + } + + @Override + public int hashCode() { + return item.hashCode(); + } + + @Override + public int getLayoutRes() { + return R.layout.item_drawer; + } + + @Override + public DrawerItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new DrawerItemViewHolder(view, adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, DrawerItemViewHolder holder, int position, List payloads) { + holder.setContent(item, flag); + } + + class DrawerItemViewHolder extends FlexibleViewHolder { + + private final TextView title; + private final ImageView icon; + + DrawerItemViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter); + title = view.findViewById(R.id.drawer_item_txt); + icon = view.findViewById(R.id.drawer_item_icon); + } + + void setContent(DrawerItem item, boolean flag) { + title.setText(String.format("%s%s", item.label, flag ? " *" : "")); + icon.setImageResource(item.icon); + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/GitHubRelease.java b/app/src/main/java/me/devsaki/hentoid/viewholders/GitHubRelease.java new file mode 100644 index 0000000000..abf73f7c88 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/GitHubRelease.java @@ -0,0 +1,141 @@ +package me.devsaki.hentoid.viewholders; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import android.view.View; +import android.widget.TextView; + +import com.google.gson.annotations.SerializedName; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import javax.annotation.Nonnull; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.viewholders.FlexibleViewHolder; +import me.devsaki.hentoid.R; + +public class GitHubRelease extends AbstractFlexibleItem { + + private final String tagName; + private final String name; + private final String description; + private final Date creationDate; + + public GitHubRelease(Struct releaseStruct) { + tagName = releaseStruct.tagName.replace("v", ""); + name = releaseStruct.name; + description = releaseStruct.body; + creationDate = releaseStruct.creationDate; + } + + public String getTagName() { + return tagName; + } + + public boolean isTagPrior(@Nonnull String tagName) { + return getIntFromTagName(this.tagName) <= getIntFromTagName(tagName); + } + + private static int getIntFromTagName(@Nonnull String tagName) { + int result = 0; + String[] parts = tagName.split("\\."); + if (parts.length > 0) result = 10000 * Integer.parseInt(parts[0].replaceAll("[^\\d]", "")); + if (parts.length > 1) result += 100 * Integer.parseInt(parts[1].replaceAll("[^\\d]", "")); + if (parts.length > 2) result += Integer.parseInt(parts[2].replaceAll("[^\\d]", "")); + + return result; + } + + @Override + public boolean equals(Object o) { + if (o instanceof GitHubRelease) { + GitHubRelease inItem = (GitHubRelease) o; + return (this.hashCode() == inItem.hashCode()); + } + return false; + } + + @Override + public int hashCode() { + return tagName.hashCode(); + } + + @Override + public int getLayoutRes() { + return R.layout.item_changelog; + } + + @Override + public ReleaseViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new ReleaseViewHolder(view, adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, ReleaseViewHolder holder, int position, List payloads) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + + holder.setTitle(name + " (" + dateFormat.format(creationDate) + ")"); + + holder.clearContent(); + // Parse content and add lines to the description + for (String s : description.split("\\r\\n")) { + s = s.trim(); + if (s.startsWith("-")) holder.addListContent(s); + else holder.addDescContent(s); + } + } + + public class ReleaseViewHolder extends FlexibleViewHolder { + + private final TextView title; + private FlexibleAdapter releaseDescriptionAdapter; + + ReleaseViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter); + title = view.findViewById(R.id.changelogReleaseTitle); + + releaseDescriptionAdapter = new FlexibleAdapter<>(null); + RecyclerView releasedDescription = view.findViewById(R.id.changelogReleaseDescription); + releasedDescription.setAdapter(releaseDescriptionAdapter); + releasedDescription.setLayoutManager(new LinearLayoutManager(view.getContext())); + } + + public void setTitle(String title) { + this.title.setText(title); + } + + void clearContent() { + releaseDescriptionAdapter.clear(); + } + + void addDescContent(String text) { + releaseDescriptionAdapter.addItem(new GitHubReleaseDescription(text, GitHubReleaseDescription.Type.DESCRIPTION)); + } + + void addListContent(String text) { + releaseDescriptionAdapter.addItem(new GitHubReleaseDescription(text, GitHubReleaseDescription.Type.LIST_ITEM)); + } + } + + public class Struct { + + @SerializedName("tag_name") + String tagName; + + @SerializedName("name") + public String name; + + @SerializedName("body") + public String body; + + @SerializedName("created_at") + Date creationDate; + } + +} diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/GitHubReleaseDescription.java b/app/src/main/java/me/devsaki/hentoid/viewholders/GitHubReleaseDescription.java new file mode 100644 index 0000000000..c1fbed1dc5 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/GitHubReleaseDescription.java @@ -0,0 +1,87 @@ +package me.devsaki.hentoid.viewholders; + +import androidx.annotation.IntDef; +import android.view.View; +import android.widget.TextView; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.viewholders.FlexibleViewHolder; +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.util.Helper; + +public class GitHubReleaseDescription extends AbstractFlexibleItem { + + @IntDef({Type.DESCRIPTION, Type.LIST_ITEM}) + @Retention(RetentionPolicy.SOURCE) + public @interface Type { + int DESCRIPTION = 0; + int LIST_ITEM = 1; + } + + private final String text; + private final @Type + int type; + + public GitHubReleaseDescription(String text, @Type int type) { + this.text = text; + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (o instanceof GitHubReleaseDescription) { + GitHubReleaseDescription inItem = (GitHubReleaseDescription) o; + return this.text.equals(inItem.text); + } + return false; + } + + @Override + public int hashCode() { + return text.hashCode(); + } + + @Override + public int getLayoutRes() { + return R.layout.item_text; + } + + @Override + public ReleaseDescriptionViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new ReleaseDescriptionViewHolder(view, adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, ReleaseDescriptionViewHolder holder, int position, List payloads) { + if (type == Type.DESCRIPTION) holder.setDescContent(text); + else if (type == Type.LIST_ITEM) holder.setListContent(text); + } + + class ReleaseDescriptionViewHolder extends FlexibleViewHolder { + + private final int DP_8; + private final TextView title; + + ReleaseDescriptionViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter); + title = view.findViewById(R.id.drawer_item_txt); + DP_8 = Helper.dpToPixel(view.getContext(), 8); + } + + void setDescContent(String text) { + title.setText(text); + title.setPadding(0, DP_8, 0, 0); + } + + void setListContent(String text) { + title.setText(text); + title.setPadding(DP_8 * 2, DP_8, 0, 0); + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileFlex.java b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileFlex.java new file mode 100644 index 0000000000..0519e82822 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileFlex.java @@ -0,0 +1,111 @@ +package me.devsaki.hentoid.viewholders; + +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; +import eu.davidea.flexibleadapter.items.IFilterable; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.viewholders.FlexibleViewHolder; +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.adapters.ImageGalleryAdapter; +import me.devsaki.hentoid.database.domains.ImageFile; + +public class ImageFileFlex extends AbstractFlexibleItem implements IFilterable { + + private final ImageFile item; + private static final RequestOptions glideRequestOptions = new RequestOptions().centerInside(); + + public ImageFileFlex(ImageFile item) { + this.item = item; + } + + public ImageFile getItem() { + return item; + } + + + @Override + public boolean equals(Object o) { + if (o instanceof ImageFileFlex) { + ImageFileFlex inItem = (ImageFileFlex) o; + return this.item.equals(inItem.item); + } + return false; + } + + + public int hashCode() { + return item.hashCode(); + } + + @Override + public int getLayoutRes() { + return R.layout.item_viewer_gallery_image; + } + + @Override + public ImageFileViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new ImageFileViewHolder(view, (ImageGalleryAdapter) adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, ImageFileViewHolder holder, int position, List payloads) { + holder.setContent(item); + } + + @Override + public boolean filter(Boolean constraint) { + return (!constraint || item.isFavourite()); + } + + public boolean isFavourite() { + return item.isFavourite(); + } + + class ImageFileViewHolder extends FlexibleViewHolder { + + private final TextView pageNumberTxt; + private final ImageView image; + private final ImageButton favouriteBtn; + private ImageFile imageFile; + + ImageFileViewHolder(View view, ImageGalleryAdapter adapter) { + super(view, adapter); + pageNumberTxt = view.findViewById(R.id.viewer_gallery_pagenumber_text); + image = view.findViewById(R.id.viewer_gallery_image); + favouriteBtn = view.findViewById(R.id.viewer_gallery_favourite_btn); + favouriteBtn.setOnClickListener(v -> onFavouriteClicked()); + } + + void setContent(ImageFile item) { + imageFile = item; + pageNumberTxt.setText(String.format("Page %s", item.getOrder())); + updateFavourite(item.isFavourite()); + Glide.with(image.getContext().getApplicationContext()) + .load(item.getAbsolutePath()) + .apply(glideRequestOptions) + .into(image); + } + + void onFavouriteClicked() { + ((ImageGalleryAdapter) mAdapter).getOnFavouriteClickListener().accept(imageFile); + } + + void updateFavourite(boolean isFavourite) { + if (isFavourite) { + favouriteBtn.setImageResource(R.drawable.ic_fav_full); + } else { + favouriteBtn.setImageResource(R.drawable.ic_fav_empty); + } + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/ImageViewerViewModel.java b/app/src/main/java/me/devsaki/hentoid/viewmodels/ImageViewerViewModel.java index 6db9aa8b0c..d2082f79a5 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewmodels/ImageViewerViewModel.java +++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/ImageViewerViewModel.java @@ -1,68 +1,317 @@ package me.devsaki.hentoid.viewmodels; import android.app.Application; -import android.arch.lifecycle.AndroidViewModel; -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; -import android.support.annotation.NonNull; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.documentfile.provider.DocumentFile; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.annimon.stream.Stream; +import com.annimon.stream.function.BooleanConsumer; +import com.annimon.stream.function.Consumer; + +import java.io.File; +import java.security.InvalidParameterException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import javax.annotation.Nonnull; + +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.database.ObjectBoxCollectionAccessor; import me.devsaki.hentoid.database.ObjectBoxDB; import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.database.domains.ImageFile; +import me.devsaki.hentoid.listener.PagedResultListener; +import me.devsaki.hentoid.util.Consts; +import me.devsaki.hentoid.util.FileHelper; +import me.devsaki.hentoid.util.Preferences; +import me.devsaki.hentoid.util.ToastUtil; +import me.devsaki.hentoid.widget.ContentSearchManager; +import timber.log.Timber; + +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static com.annimon.stream.Collectors.toList; + +public class ImageViewerViewModel extends AndroidViewModel implements PagedResultListener { -public class ImageViewerViewModel extends AndroidViewModel { + private static final String KEY_IS_SHUFFLED = "is_shuffled"; - private final MutableLiveData> images = new MutableLiveData<>(); + // Settings + private boolean isShuffled = false; // True if images have to be shuffled; false if presented in the book order + private BooleanConsumer onShuffledChangeListener; - private long contentId; - private int currentPosition; // 0-based position, as in "programmatic index" + // Collection data + private final MutableLiveData content = new MutableLiveData<>(); // Current content + private List contentIds = Collections.emptyList(); // Content Ids of the whole collection ordered according to current filter + private int currentContentIndex = -1; // Index of current content within the above list + private long loadedContentId = -1; // Content ID that has been initially loaded + + // Pictures data + private final MutableLiveData> images = new MutableLiveData<>(); // Currently displayed set of images + private final MutableLiveData startingIndex = new MutableLiveData<>(); // 0-based index of the current image + + // Technical + private ContentSearchManager searchManager = null; + private CompositeDisposable compositeDisposable = new CompositeDisposable(); public ImageViewerViewModel(@NonNull Application application) { super(application); - images.setValue(Collections.emptyList()); } @NonNull - public LiveData> getImages() { + public LiveData> getImages() { return images; } - public void setImages(List imgs) { - images.setValue(imgs); + @NonNull + public LiveData getStartingIndex() { + return startingIndex; } - public void setContentId(long contentId) { - this.contentId = contentId; + @NonNull + public LiveData getContent() { + return content; } - public void setCurrentPosition(int position) { - this.currentPosition = position; + public void setOnShuffledChangeListener(BooleanConsumer listener) { + this.onShuffledChangeListener = listener; } - public int getCurrentPosition() { - return currentPosition; + public void onSaveState(Bundle outState) { + outState.putBoolean(KEY_IS_SHUFFLED, isShuffled); } - public int getInitialPosition() { - ObjectBoxDB db = ObjectBoxDB.getInstance(getApplication().getApplicationContext()); + public void onRestoreState(@Nullable Bundle savedState) { + if (savedState == null) return; + isShuffled = savedState.getBoolean(KEY_IS_SHUFFLED, false); + } + + public void loadFromContent(long contentId) { if (contentId > 0) { - Content content = db.selectContentById(contentId); - if (content != null) return content.getLastReadPageIndex(); + ObjectBoxDB db = ObjectBoxDB.getInstance(getApplication().getApplicationContext()); + Content loadedContent = db.selectContentById(contentId); + if (loadedContent != null) + processContent(loadedContent); } - return 0; } - public void saveCurrentPosition() { + public void loadFromSearchParams(long contentId, @Nonnull Bundle bundle) { + loadedContentId = contentId; + Context ctx = getApplication().getApplicationContext(); + searchManager = new ContentSearchManager(new ObjectBoxCollectionAccessor(ctx)); + searchManager.loadFromBundle(bundle); + int contentIndex = bundle.getInt("contentIndex", -1); + if (contentIndex > -1) searchManager.setCurrentPage(contentIndex); + searchManager.searchLibraryForId(-1, this); + } + + @Override + public void onPagedResultReady(List results, long totalSelectedContent, long totalContent) { + contentIds = results; + loadFromContent(loadedContentId); + } + + @Override + public void onPagedResultFailed(Long contentId, String message) { + ToastUtil.toast("Book list loading failed"); + } + + public void setStartingIndex(int index) { + startingIndex.setValue(index); + } + + public void setImages(List imgs) { + List list = new ArrayList<>(imgs); + sortAndSetImages(list, isShuffled); + } + + public void onShuffleClick() { + isShuffled = !isShuffled; + onShuffledChangeListener.accept(isShuffled); + + List imgs = getImages().getValue(); + if (imgs != null) sortAndSetImages(imgs, isShuffled); + } + + private void sortAndSetImages(@Nonnull List imgs, boolean shuffle) + { + if (shuffle) { + Collections.shuffle(imgs); + } else { + // Sort images according to their Order + imgs = Stream.of(imgs).sortBy(ImageFile::getOrder).collect(toList()); + } + for (int i = 0; i < imgs.size(); i++) imgs.get(i).setDisplayOrder(i); + images.setValue(imgs); + } + + @Override + protected void onCleared() { + super.onCleared(); + if (searchManager != null) searchManager.dispose(); + compositeDisposable.clear(); + } + + public void savePosition(int index) { ObjectBoxDB db = ObjectBoxDB.getInstance(getApplication().getApplicationContext()); - if (contentId > 0) { - Content content = db.selectContentById(contentId); - if (content != null) { - content.setLastReadPageIndex(currentPosition); + Content theContent = content.getValue(); + if (theContent != null) { + theContent.setLastReadPageIndex(index); + db.insertContent(theContent); + } + } + + public void togglePageFavourite(ImageFile file, Consumer callback) { + compositeDisposable.add( + Single.fromCallable(() -> togglePageFavourite(getApplication().getApplicationContext(), file.getId())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> onToggleFavouriteSuccess(result, callback), + Timber::e + ) + ); + } + + private void onToggleFavouriteSuccess(ImageFile result, Consumer callback) { + List imgs = getImages().getValue(); + if (imgs != null) { + for (ImageFile img : imgs) + if (img.getId() == result.getId()) { + img.setFavourite(result.isFavourite()); // Update new state in memory + result.setDisplayOrder(img.getDisplayOrder()); // Set the display order of the item to + callback.accept(result); // Inform the view + } + } + } + + /** + * Toggles favourite flag in DB and in the content JSON + * + * @param context Context to be used for this operation + * @param imageId ID of the image whose flag to toggle + * @return ImageFile with the new state + */ + @WorkerThread + private static ImageFile togglePageFavourite(Context context, long imageId) { + ObjectBoxDB db = ObjectBoxDB.getInstance(context); + + ImageFile img = db.selectImageFile(imageId); + + if (img != null) { + img.setFavourite(!img.isFavourite()); + + // Persist it in DB + db.insertImageFile(img); + + // Persist in it JSON + Content content = img.content.getTarget(); + if (!content.getJsonUri().isEmpty()) FileHelper.updateJson(context, content); + else FileHelper.createJson(content); + + return img; + } else + throw new InvalidParameterException(String.format("Invalid image ID %s", imageId)); + } + + public void loadNextContent() { + if (currentContentIndex < contentIds.size() - 1) currentContentIndex++; + loadFromContent(contentIds.get(currentContentIndex)); + } + + public void loadPreviousContent() { + if (currentContentIndex > 0) currentContentIndex--; + loadFromContent(contentIds.get(currentContentIndex)); + } + + private void processContent(Content theContent) { + currentContentIndex = contentIds.indexOf(theContent.getId()); + theContent.setFirst(0 == currentContentIndex); + theContent.setLast(currentContentIndex == contentIds.size() - 1); + + // Load new content + File[] pictures = FileHelper.getPictureFilesFromContent(theContent); + if (pictures != null && pictures.length > 0 && theContent.getImageFiles() != null) { + List imageFiles = new ArrayList<>(theContent.getImageFiles()); + matchFilesToImageList(pictures, imageFiles); + setImages(imageFiles); + + if (Preferences.isViewerResumeLastLeft()) { + setStartingIndex(theContent.getLastReadPageIndex()); + } else { + setStartingIndex(0); + } + + // Cache JSON and record 1 more view for the new content + compositeDisposable.add( + Single.fromCallable(() -> postLoadProcessing(getApplication().getApplicationContext(), theContent)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + content::setValue, + Timber::e + ) + ); + } else { + ToastUtil.toast(R.string.no_images); + } + } + + private static void matchFilesToImageList(File[] files, List images) { + int i = 0; + while (i < images.size()) { + boolean matchFound = false; + for (File f : files) { + // Image and file name match => store absolute path + if (FileHelper.getFileNameWithoutExtension(images.get(i).getName()).equals(FileHelper.getFileNameWithoutExtension(f.getName()))) { + matchFound = true; + images.get(i).setAbsolutePath(f.getAbsolutePath()); + break; + } + } + // Image is not among detected files => remove it + if (!matchFound) { + images.remove(i); + } else i++; + } + } + + @WorkerThread + @Nullable + private static Content postLoadProcessing(@Nonnull Context context, @Nonnull Content content) { + cacheJson(context, content); + return FileHelper.updateContentReads(context, content.getId()); + } + + // Cache JSON URI in the database to speed up favouriting + // NB : Lollipop only because it must have _full_ support for SAF + @WorkerThread + private static void cacheJson(@Nonnull Context context, @Nonnull Content content) { + if (content.getJsonUri().isEmpty() && Build.VERSION.SDK_INT >= LOLLIPOP) { + File bookFolder = FileHelper.getContentDownloadDir(content); + DocumentFile file = FileHelper.getDocumentFile(new File(bookFolder, Consts.JSON_FILE_NAME_V2), false); + if (file != null) { + // Cache the URI of the JSON to the database + ObjectBoxDB db = ObjectBoxDB.getInstance(context); + content.setJsonUri(file.getUri().toString()); db.insertContent(content); + } else { + Timber.e("File not detected : %s", content.getStorageFolder()); } } } diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/SearchViewModel.java b/app/src/main/java/me/devsaki/hentoid/viewmodels/SearchViewModel.java index 6900439f4b..801ba92a34 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewmodels/SearchViewModel.java +++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/SearchViewModel.java @@ -1,13 +1,14 @@ package me.devsaki.hentoid.viewmodels; import android.app.Application; -import android.arch.lifecycle.AndroidViewModel; -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.MutableLiveData; import android.content.Context; -import android.support.annotation.NonNull; import android.util.SparseIntArray; +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + import java.util.ArrayList; import java.util.List; @@ -17,9 +18,8 @@ import me.devsaki.hentoid.database.domains.Attribute; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.AttributeType; -import me.devsaki.hentoid.listener.ContentListener; +import me.devsaki.hentoid.listener.PagedResultListener; import me.devsaki.hentoid.listener.ResultListener; -import me.devsaki.hentoid.model.State; import me.devsaki.hentoid.util.Preferences; import static java.util.Objects.requireNonNull; @@ -33,13 +33,6 @@ public class SearchViewModel extends AndroidViewModel { private final MutableLiveData selectedContent = new MutableLiveData<>(); private final MutableLiveData attributesPerType = new MutableLiveData<>(); - /** - * should only be used as a means to communicate with the view without keeping a reference to - * it, or knowing about it's lifecycle. {@link LiveData#getValue()} is rarely used due to its - * cumbersome nulllability. - */ - private final MutableLiveData stateLiveData = new MutableLiveData<>(); - /** * @see #setMode(int) */ @@ -70,20 +63,20 @@ public void onResultFailed(String message) { } } - private ContentListener contentResultListener = new ContentListener() { + private PagedResultListener contentResultListener = new PagedResultListener() { @Override - public void onContentReady(List results, long totalSelectedContent, long totalContent) { + public void onPagedResultReady(List results, long totalSelected, long total) { ContentSearchResult result = new ContentSearchResult(); - result.totalSelected = totalSelectedContent; + result.totalSelected = totalSelected; selectedContent.postValue(result); } @Override - public void onContentFailed(Content content, String message) { - ContentSearchResult result = new ContentSearchResult(); - result.success = false; - result.message = message; - selectedContent.postValue(result); + public void onPagedResultFailed(Content result, String message) { + ContentSearchResult res = new ContentSearchResult(); + res.success = false; + res.message = message; + selectedContent.postValue(res); } }; @@ -143,17 +136,6 @@ public LiveData getSelectedContentData() { return selectedContent; } - /** - * Used by the view to observe changes to this ViewModel's state. It is safe to subscribe to - * this observable before it is given an initial value. - * - * @return LiveData holding the current state - */ - @NonNull - public MutableLiveData getStateLiveData() { - return stateLiveData; - } - // === VERB METHODS public void onCategoryChanged(List category) { @@ -163,9 +145,9 @@ public void onCategoryChanged(List category) { public void onCategoryFilterChanged(String query, int pageNum, int itemsPerPage) { if (collectionAccessor.supportsAttributesPaging()) { if (collectionAccessor.supportsAvailabilityFilter()) - collectionAccessor.getPagedAttributeMasterData(category, query, selectedAttributes.getValue(), false, pageNum, itemsPerPage, Preferences.getAttributesSortOrder(), new AttributesResultListener(proposedAttributes)); + collectionAccessor.getAttributeMasterDataPaged(category, query, selectedAttributes.getValue(), false, pageNum, itemsPerPage, Preferences.getAttributesSortOrder(), new AttributesResultListener(proposedAttributes)); else - collectionAccessor.getPagedAttributeMasterData(category, query, pageNum, itemsPerPage, Preferences.getAttributesSortOrder(), new AttributesResultListener(proposedAttributes)); + collectionAccessor.getAttributeMasterDataPaged(category, query, pageNum, itemsPerPage, Preferences.getAttributesSortOrder(), new AttributesResultListener(proposedAttributes)); } else { if (collectionAccessor.supportsAvailabilityFilter()) collectionAccessor.getAttributeMasterData(category, query, selectedAttributes.getValue(), false, Preferences.getAttributesSortOrder(), new AttributesResultListener(proposedAttributes)); diff --git a/app/src/main/java/me/devsaki/hentoid/views/CircularProgressView.java b/app/src/main/java/me/devsaki/hentoid/views/CircularProgressView.java new file mode 100644 index 0000000000..f3e5067428 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/views/CircularProgressView.java @@ -0,0 +1,68 @@ +package me.devsaki.hentoid.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.TextView; + +import javax.annotation.Nullable; + +import me.devsaki.hentoid.R; + +public class CircularProgressView extends View { + private final float strokeWidth; + private Paint totalPaint; + private Paint progressPaint; + private float progress = 360; + private float total = 360; + private TextView textView; + + public CircularProgressView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + strokeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, context.getResources().getDisplayMetrics()); + + totalPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + totalPaint.setStyle(Paint.Style.STROKE); + totalPaint.setColor(getResources().getColor(R.color.transparent)); + totalPaint.setStrokeWidth(strokeWidth); + + progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + progressPaint.setStyle(Paint.Style.STROKE); + progressPaint.setColor(getResources().getColor(R.color.secondary)); + progressPaint.setStrokeWidth(strokeWidth); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + canvas.save(); + + drawProgress(canvas, (int) 360f, totalPaint); + if (total != 0 && progress != 0) + drawProgress(canvas, total == progress ? 360 : (int) ((360f / total) * progress), progressPaint); + + canvas.restore(); + } + + private void drawProgress(Canvas canvas, int total, Paint paint) { + canvas.drawArc(new RectF(strokeWidth, strokeWidth, getWidth() - strokeWidth, getHeight() - strokeWidth), -90, total, false, paint); + } + + public void setProgress(int progress) { + this.progress = progress; + invalidate(); + } + + public void setTotal(int total) { + this.total = total; + if (textView != null) + textView.setText(String.valueOf(total)); + invalidate(); + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/views/MaxHeightRecyclerView.java b/app/src/main/java/me/devsaki/hentoid/views/MaxHeightRecyclerView.java index 2f9234a789..66b95b1b2a 100644 --- a/app/src/main/java/me/devsaki/hentoid/views/MaxHeightRecyclerView.java +++ b/app/src/main/java/me/devsaki/hentoid/views/MaxHeightRecyclerView.java @@ -2,8 +2,8 @@ import android.content.Context; import android.content.res.TypedArray; -import android.support.annotation.Nullable; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; import android.util.AttributeSet; import me.devsaki.hentoid.R; diff --git a/app/src/main/java/me/devsaki/hentoid/views/MutedSubsamplingScaleImageView.java b/app/src/main/java/me/devsaki/hentoid/views/MutedSubsamplingScaleImageView.java new file mode 100644 index 0000000000..02ada4527e --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/views/MutedSubsamplingScaleImageView.java @@ -0,0 +1,28 @@ +package me.devsaki.hentoid.views; + +import android.content.Context; +import androidx.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; + +/** + * {@link SubsamplingScaleImageView} that does not listen to any touch event + */ +public class MutedSubsamplingScaleImageView extends SubsamplingScaleImageView { + + public MutedSubsamplingScaleImageView(Context context, AttributeSet attr) { + super(context, attr); + } + + public MutedSubsamplingScaleImageView(Context context) { + super(context); + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + return false; + } + +} diff --git a/app/src/main/java/me/devsaki/hentoid/views/ObservableWebView.java b/app/src/main/java/me/devsaki/hentoid/views/ObservableWebView.java index 5d636eaa32..993499f414 100644 --- a/app/src/main/java/me/devsaki/hentoid/views/ObservableWebView.java +++ b/app/src/main/java/me/devsaki/hentoid/views/ObservableWebView.java @@ -1,32 +1,54 @@ package me.devsaki.hentoid.views; +import android.annotation.TargetApi; import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; import android.util.AttributeSet; import android.webkit.WebView; /** * WebView implementation with scroll listener + * * Ref: http://stackoverflow.com/questions/14752523/ */ public class ObservableWebView extends WebView { private OnScrollChangedCallback mOnScrollChangedCallback; public ObservableWebView(final Context context) { - super(context); + super(getFixedContext(context)); } public ObservableWebView(final Context context, final AttributeSet attrs) { - super(context, attrs); + super(getFixedContext(context), attrs); } public ObservableWebView(final Context context, final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); + super(getFixedContext(context), attrs, defStyle); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public ObservableWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(getFixedContext(context), attrs, defStyleAttr, defStyleRes); + } + + @Deprecated + public ObservableWebView(Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) { + super(getFixedContext(context), attrs, defStyleAttr, privateBrowsing); + } + + // Fix for inflating on Android 5.1.1 + // https://stackoverflow.com/questions/41025200/android-view-inflateexception-error-inflating-class-android-webkit-webview + private static Context getFixedContext(Context context) { + return context.createConfigurationContext(new Configuration()); } @Override protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt) { super.onScrollChanged(l, t, oldl, oldt); - if (mOnScrollChangedCallback != null) mOnScrollChangedCallback.onScroll(l, t); + int deltaX = l - oldl; + int deltaY = t - oldt; + if (mOnScrollChangedCallback != null) mOnScrollChangedCallback.onScroll(deltaX, deltaY); } public OnScrollChangedCallback getOnScrollChangedCallback() { @@ -41,6 +63,6 @@ public void setOnScrollChangedCallback(final OnScrollChangedCallback onScrollCha * Implement in the activity/fragment/view that you want to listen to the WebView */ public interface OnScrollChangedCallback { - void onScroll(int l, int t); + void onScroll(int deltaX, int deltaY); } } diff --git a/app/src/main/java/me/devsaki/hentoid/views/ZoomableFrame.java b/app/src/main/java/me/devsaki/hentoid/views/ZoomableFrame.java new file mode 100644 index 0000000000..23e5526028 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/views/ZoomableFrame.java @@ -0,0 +1,118 @@ +package me.devsaki.hentoid.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Frame layout which contains a [WebtoonRecyclerView]. It's needed to handle touch events, + * because the recyclerview is scaled and its touch events are translated, which breaks the + * detectors. + *

+ * Credits for this go to the Tachiyomi team + */ +public class ZoomableFrame extends FrameLayout { + + private boolean enabled = true; + + + public ZoomableFrame(@NonNull Context context) { + super(context); + } + + public ZoomableFrame(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public ZoomableFrame(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + + /** + * Scale detector, either with pinch or quick scale. + */ + private ScaleGestureDetector scaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener()); + + /** + * Fling detector. + */ + private GestureDetector flingDetector = new GestureDetector(getContext(), new FlingListener()); + + /** + * Recycler view added in this frame. + */ + private ZoomableRecyclerView recycler; + + private ZoomableRecyclerView getRecycler() { + if (null == recycler && getChildCount() > 0) + recycler = (ZoomableRecyclerView) getChildAt(0); + return recycler; + } + + + /** + * Dispatches a touch event to the detectors. + */ + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (enabled) { + scaleDetector.onTouchEvent(ev); + flingDetector.onTouchEvent(ev); + } + return super.dispatchTouchEvent(ev); + } + + /** + * Scale listener used to delegate events to the recycler view. + */ + class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + if (enabled && null != getRecycler()) getRecycler().onScaleBegin(); + return enabled; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + if (enabled && null != getRecycler()) getRecycler().onScale(detector.getScaleFactor()); + return enabled; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + if (enabled && null != getRecycler()) getRecycler().onScaleEnd(); + } + } + + /** + * Fling listener used to delegate events to the recycler view. + */ + class FlingListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (enabled && null != getRecycler()) + return getRecycler().zoomFling(Math.round(velocityX), Math.round(velocityY)); + else return false; + } + + @Override + public boolean onDown(MotionEvent e) { + return enabled; + } + } + + public void enable() { + enabled = true; + } + + public void disable() { + enabled = false; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/views/ZoomableRecyclerView.java b/app/src/main/java/me/devsaki/hentoid/views/ZoomableRecyclerView.java new file mode 100644 index 0000000000..1e3c2ecbc6 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/views/ZoomableRecyclerView.java @@ -0,0 +1,422 @@ +package me.devsaki.hentoid.views; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.ViewPropertyAnimator; +import android.view.animation.DecelerateInterpolator; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.annimon.stream.function.DoubleConsumer; + +import me.devsaki.hentoid.util.Helper; +import me.devsaki.hentoid.widget.OnZoneTapListener; +import me.devsaki.hentoid.widget.ViewZoomGestureListener; +import me.devsaki.hentoid.widget.ViewZoomGestureListener.Listener; + +/** + * Zoomable RecyclerView that supports gestures + * To be used inside a {@link ZoomableFrame} + *

+ * Credits go to the Tachiyomi team + */ +public class ZoomableRecyclerView extends RecyclerView { + + private static final long ANIMATOR_DURATION_TIME = 200; + private static final float DEFAULT_RATE = 1f; + private static final float MAX_SCALE_RATE = 3f; + + private boolean isZooming = false; + private boolean atLastPosition = false; + + private boolean atFirstPosition = false; + private int halfWidth = 0; + private int halfHeight = 0; + private int firstVisibleItemPosition = 0; + private int lastVisibleItemPosition = 0; + private float currentScale = DEFAULT_RATE; + + private GestureListener listener = new GestureListener(); + private DoubleConsumer scaleListener = null; + private Detector detector = new Detector(getContext(), listener); + + private OnZoneTapListener tapListener; + private LongTapListener longTapListener; + + + public ZoomableRecyclerView(Context context) { + super(context); + } + + public ZoomableRecyclerView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public ZoomableRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + + public void setTapListener(OnZoneTapListener tapListener) { + this.tapListener = tapListener; + } + + public void setLongTapListener(LongTapListener longTapListener) { + this.longTapListener = longTapListener; + } + + public void setOnScaleListener(DoubleConsumer scaleListener) { + this.scaleListener = scaleListener; + } + + public interface LongTapListener { + boolean onListen(MotionEvent ev); + } + + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + halfWidth = MeasureSpec.getSize(widthSpec) / 2; + halfHeight = MeasureSpec.getSize(heightSpec) / 2; + super.onMeasure(widthSpec, heightSpec); + } + + + @Override + public boolean onTouchEvent(MotionEvent e) { + detector.onTouchEvent(e); + return super.onTouchEvent(e); + } + + @Override + public void onScrolled(int dx, int dy) { + super.onScrolled(dx, dy); + LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); + if (layoutManager != null) { + lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); + firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition(); + } + } + + + @TargetApi(Build.VERSION_CODES.KITKAT) + @Override + public void onScrollStateChanged(int state) { + super.onScrollStateChanged(state); + LayoutManager layoutManager = getLayoutManager(); + if (layoutManager != null) { + int visibleItemCount = layoutManager.getChildCount(); + int totalItemCount = layoutManager.getItemCount(); + atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1; + atFirstPosition = firstVisibleItemPosition == 0; + } + } + + private float getPositionX(float positionX) { + float maxPositionX = halfWidth * (currentScale - 1); + return Helper.coerceIn(positionX, -maxPositionX, maxPositionX); + } + + private float getPositionY(float positionY) { + float maxPositionY = halfHeight * (currentScale - 1); + return Helper.coerceIn(positionY, -maxPositionY, maxPositionY); + } + + public float getCurrentScale() { + return currentScale; + } + + public void resetScale() { + zoom(currentScale, DEFAULT_RATE, getX(), 0f, getY(), 0f); + } + + private void zoom( + float fromRate, + float toRate, + float fromX, + float toX, + float fromY, + float toY + ) { + isZooming = true; + AnimatorSet animatorSet = new AnimatorSet(); + + ValueAnimator translationXAnimator = ValueAnimator.ofFloat(fromX, toX); + translationXAnimator.addUpdateListener(animation -> setX((float) animation.getAnimatedValue())); + + ValueAnimator translationYAnimator = ValueAnimator.ofFloat(fromY, toY); + translationYAnimator.addUpdateListener(animation -> setY((float) animation.getAnimatedValue())); + + ValueAnimator scaleAnimator = ValueAnimator.ofFloat(fromRate, toRate); + scaleAnimator.addUpdateListener(animation -> setScaleRate((float) animation.getAnimatedValue())); + + animatorSet.playTogether(translationXAnimator, translationYAnimator, scaleAnimator); + animatorSet.setDuration(ANIMATOR_DURATION_TIME); + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.start(); + animatorSet.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + // No need to define any behaviour here + } + + @Override + public void onAnimationEnd(Animator animation) { + isZooming = false; + currentScale = toRate; + if (scaleListener != null) scaleListener.accept(currentScale); + } + + @Override + public void onAnimationCancel(Animator animation) { + // No need to define any behaviour here + } + + @Override + public void onAnimationRepeat(Animator animation) { + // No need to define any behaviour here + } + }); + } + + private boolean canMoveHorizontally() { + return (getLayoutManager().canScrollVertically() || (getLayoutManager().canScrollHorizontally() && (atFirstPosition || atLastPosition))); + } + + private boolean canMoveVertically() { + return (getLayoutManager().canScrollHorizontally() || (getLayoutManager().canScrollVertically() && (atFirstPosition || atLastPosition))); + } + + boolean zoomFling(int velocityX, int velocityY) { + if (currentScale <= 1f) return false; + + float distanceTimeFactor = 0.4f; + Float newX = null; + Float newY = null; + + if (velocityX != 0 && canMoveHorizontally()) { + float dx = (distanceTimeFactor * velocityX / 2); + newX = getPositionX(getX() + dx); + } + if (velocityY != 0 && canMoveVertically()) { + float dy = (distanceTimeFactor * velocityY / 2); + newY = getPositionY(getY() + dy); + } + + ViewPropertyAnimator animation = animate(); + if (newX != null) animation.x(newX); + if (newY != null) animation.y(newY); + animation.setInterpolator(new DecelerateInterpolator()) + .setDuration(400) + .start(); + + return true; + } + + private void zoomScrollBy(int dx, int dy) { + if (dx != 0) { + setX(getPositionX(getX() + dx)); + } + if (dy != 0) { + setY(getPositionY(getY() + dy)); + } + } + + private void setScaleRate(float rate) { + setScaleX(rate); + setScaleY(rate); + } + + void onScale(float scaleFactor) { + currentScale *= scaleFactor; + currentScale = Helper.coerceIn(currentScale, DEFAULT_RATE, MAX_SCALE_RATE); + + setScaleRate(currentScale); + + if (currentScale != DEFAULT_RATE) { + setX(getPositionX(getX())); + setY(getPositionY(getY())); + } else { + setX(0f); + setY(0f); + } + + if (scaleListener != null) scaleListener.accept(currentScale); + } + + void onScaleBegin() { + if (detector.isDoubleTapping) { + detector.isQuickScaling = true; + } + } + + void onScaleEnd() { + if (getScaleX() < DEFAULT_RATE) { + zoom(currentScale, DEFAULT_RATE, getX(), 0f, getY(), 0f); + } + } + + class GestureListener extends Listener { + @Override + public boolean onDoubleTap(MotionEvent e) { + detector.isDoubleTapping = true; + return false; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (tapListener != null) tapListener.onSingleTapConfirmedAction(e); + return false; + } + + @Override + public void onLongTapConfirmed(MotionEvent ev) { + if (longTapListener != null && longTapListener.onListen(ev)) { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } + } + + @Override + public void onDoubleTapConfirmed(MotionEvent ev) { + if (!isZooming) { + if (getScaleX() != DEFAULT_RATE) { + zoom(currentScale, DEFAULT_RATE, getX(), 0f, getY(), 0f); + } else { + float toScale = 2f; + float toX = (halfWidth - ev.getX()) * (toScale - 1); + float toY = (halfHeight - ev.getY()) * (toScale - 1); + zoom(DEFAULT_RATE, toScale, 0f, toX, 0f, toY); + } + } + } + } + + class Detector extends ViewZoomGestureListener { + + private int scrollPointerId = 0; + private int downX = 0; + private int downY = 0; + private final int touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + private boolean isZoomDragging = false; + boolean isDoubleTapping = false; + boolean isQuickScaling = false; + + Detector(Context context, Listener listener) { + super(context, listener); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + int action = ev.getActionMasked(); + int actionIndex = ev.getActionIndex(); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + motionActionDownLocal(ev); + } + break; + case MotionEvent.ACTION_POINTER_DOWN: { + motionActionPointerDown(ev, actionIndex); + } + break; + case MotionEvent.ACTION_MOVE: { + return motionActionMoveLocal(ev); + } + case MotionEvent.ACTION_UP: { + motionActionUpLocal(ev); + break; + } + case MotionEvent.ACTION_CANCEL: { + motionActionCancel(); + } + break; + default: + // Nothing to process as default + } + return super.onTouchEvent(ev); + } + + private void motionActionDownLocal(MotionEvent ev) { + scrollPointerId = ev.getPointerId(0); + downX = Math.round(ev.getX() + 0.5f); + downY = Math.round(ev.getY() + 0.5f); + } + + private void motionActionPointerDown(MotionEvent ev, int actionIndex){ + scrollPointerId = ev.getPointerId(actionIndex); + downX = Math.round(ev.getX(actionIndex) + 0.5f); + downY = Math.round(ev.getY(actionIndex) + 0.5f); + } + + private boolean motionActionMoveLocal(MotionEvent ev) { + if (isDoubleTapping && isQuickScaling) { + return true; + } + + int index = ev.findPointerIndex(scrollPointerId); + if (index < 0) { + return false; + } + + int x = Math.round(ev.getX(index) + 0.5f); + int y = Math.round(ev.getY(index) + 0.5f); + int dx = (canMoveHorizontally()) ? x - downX : 0; + int dy = (canMoveVertically()) ? y - downY : 0; + + if (!isZoomDragging && currentScale > 1f) { + boolean startScroll = false; + + if (Math.abs(dx) > touchSlop) { + if (dx < 0) { + dx += touchSlop; + } else { + dx -= touchSlop; + } + startScroll = true; + } + if (Math.abs(dy) > touchSlop) { + if (dy < 0) { + dy += touchSlop; + } else { + dy -= touchSlop; + } + startScroll = true; + } + + if (startScroll) { + isZoomDragging = true; + } + } + + if (isZoomDragging) { + zoomScrollBy(dx, dy); + } + return super.onTouchEvent(ev); + } + + private void motionActionUpLocal(MotionEvent ev) { + if (isDoubleTapping && !isQuickScaling) { + listener.onDoubleTapConfirmed(ev); + } + isZoomDragging = false; + isDoubleTapping = false; + isQuickScaling = false; + } + + private void motionActionCancel() { + isZoomDragging = false; + isDoubleTapping = false; + isQuickScaling = false; + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/widget/ContentSearchManager.java b/app/src/main/java/me/devsaki/hentoid/widget/ContentSearchManager.java new file mode 100644 index 0000000000..323b704eaa --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/widget/ContentSearchManager.java @@ -0,0 +1,142 @@ +package me.devsaki.hentoid.widget; + +import android.net.Uri; +import android.os.Bundle; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nonnull; + +import me.devsaki.hentoid.activities.bundles.SearchActivityBundle; +import me.devsaki.hentoid.collection.CollectionAccessor; +import me.devsaki.hentoid.database.domains.Attribute; +import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.enums.Language; +import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.listener.PagedResultListener; +import me.devsaki.hentoid.util.Preferences; + +public class ContentSearchManager { + + // Save state constants + private static final String KEY_SELECTED_TAGS = "selected_tags"; + private static final String KEY_FILTER_FAVOURITES = "filter_favs"; + private static final String KEY_QUERY = "query"; + private static final String KEY_SORT_ORDER = "sort_order"; + private static final String KEY_CURRENT_PAGE = "current_page"; + + private final CollectionAccessor accessor; + + // Current page of collection view (NB : In EndlessFragment, a "page" is a group of loaded books. Last page is reached when scrolling reaches the very end of the book list) + private int currentPage = 1; + // Favourite filter active + private boolean filterFavourites = false; + // Full-text query + private String query = ""; + // Current search tags + private List tags = new ArrayList<>(); + + private int contentSortOrder = Preferences.getContentSortOrder(); + + + public ContentSearchManager(CollectionAccessor accessor) { + this.accessor = accessor; + } + + public void setFilterFavourites(boolean filterFavourites) { + this.filterFavourites = filterFavourites; + } + + public boolean isFilterFavourites() { + return filterFavourites; + } + + public void setQuery(String query) { + this.query = query; + } + + public String getQuery() { + return (query != null) ? query : ""; + } + + public void setTags(List tags) { + if (tags != null) this.tags = tags; + else this.tags.clear(); + } + + public List getTags() { + return tags; + } + + public void clearSelectedSearchTags() { + if (tags != null) tags.clear(); + } + + public int getContentSortOrder() { + return contentSortOrder; + } + + public void setContentSortOrder(int contentSortOrder) { + this.contentSortOrder = contentSortOrder; + } + + public int getCurrentPage() { + return currentPage; + } + + public void setCurrentPage(int currentPage) { + this.currentPage = currentPage; + } + + public void increaseCurrentPage() { + currentPage++; + } + + public void decreaseCurrentPage() { + currentPage--; + } + + public void saveToBundle(@Nonnull Bundle outState) { + outState.putBoolean(KEY_FILTER_FAVOURITES, filterFavourites); + outState.putString(KEY_QUERY, query); + outState.putInt(KEY_SORT_ORDER, contentSortOrder); + outState.putInt(KEY_CURRENT_PAGE, currentPage); + String searchUri = SearchActivityBundle.Builder.buildSearchUri(tags).toString(); + outState.putString(KEY_SELECTED_TAGS, searchUri); + } + + public void loadFromBundle(@Nonnull Bundle state) { + filterFavourites = state.getBoolean(KEY_FILTER_FAVOURITES, false); + query = state.getString(KEY_QUERY, ""); + contentSortOrder = state.getInt(KEY_SORT_ORDER, Preferences.getContentSortOrder()); + currentPage = state.getInt(KEY_CURRENT_PAGE); + + String searchUri = state.getString(KEY_SELECTED_TAGS); + tags = SearchActivityBundle.Parser.parseSearchUri(Uri.parse(searchUri)); + } + + public void searchLibraryForContent(int booksPerPage, PagedResultListener listener) { + if (!getQuery().isEmpty()) + accessor.searchBooksUniversalPaged(getQuery(), currentPage, booksPerPage, contentSortOrder, filterFavourites, listener); // Universal search + else if (!tags.isEmpty()) + accessor.searchBooksPaged("", tags, currentPage, booksPerPage, contentSortOrder, filterFavourites, listener); // Advanced search + else + accessor.getRecentBooksPaged(Site.HITOMI, Language.ANY, currentPage, booksPerPage, contentSortOrder, filterFavourites, listener); // Default search (display recent) + // TODO : do something about these ridiculous default 1st arguments + } + + public void searchLibraryForId(int booksPerPage, PagedResultListener listener) { + if (!getQuery().isEmpty()) + accessor.searchBookIdsUniversalPaged(getQuery(), currentPage, booksPerPage, contentSortOrder, filterFavourites, listener); // Universal search + else if (!tags.isEmpty()) + accessor.searchBookIdsPaged("", tags, currentPage, booksPerPage, contentSortOrder, filterFavourites, listener); // Advanced search + else + accessor.getRecentBookIdsPaged(Site.HITOMI, Language.ANY, currentPage, booksPerPage, contentSortOrder, filterFavourites, listener); // Default search (display recent) + // TODO : do something about these ridiculous default 1st arguments + } + + public void dispose() { + accessor.dispose(); + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/widget/KeyInputDetector.java b/app/src/main/java/me/devsaki/hentoid/widget/KeyInputDetector.java deleted file mode 100644 index e07eaef40b..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/widget/KeyInputDetector.java +++ /dev/null @@ -1,49 +0,0 @@ -package me.devsaki.hentoid.widget; - -import android.view.KeyEvent; -import android.view.View; - -import java.util.Calendar; - -public class KeyInputDetector implements View.OnKeyListener { - - public interface OnKeyEventListener { - boolean onEvent(int keyCode); - } - - // PARAMETERS - private final int actionTimeFrame; - private final OnKeyEventListener listener; - private boolean enableTurbo = true; - - // INTERNALS - private long lastKeyDownEventTick = -1; - private boolean isTurbo = false; - - - public KeyInputDetector(OnKeyEventListener listener, int actionTimeFrame) { - this.actionTimeFrame = actionTimeFrame; - this.listener = listener; - } - - public void setEnableTurbo(boolean enableTurbo) { - this.enableTurbo = enableTurbo; - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_DOWN) { - long timeNow = Calendar.getInstance().getTimeInMillis(); - if (timeNow - lastKeyDownEventTick > actionTimeFrame / (isTurbo ? 2 : 1)) { - if (-1 == lastKeyDownEventTick) isTurbo = true; - if (enableTurbo) lastKeyDownEventTick = timeNow; - return listener.onEvent(keyCode); - } - return true; - } else if (event.getAction() == KeyEvent.ACTION_UP) { - lastKeyDownEventTick = -1; - isTurbo = false; - } - return false; - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/widget/OnZoneTapListener.java b/app/src/main/java/me/devsaki/hentoid/widget/OnZoneTapListener.java index 1b80b8e953..0986a4365f 100644 --- a/app/src/main/java/me/devsaki/hentoid/widget/OnZoneTapListener.java +++ b/app/src/main/java/me/devsaki/hentoid/widget/OnZoneTapListener.java @@ -1,16 +1,19 @@ package me.devsaki.hentoid.widget; import android.content.Context; -import android.support.v4.view.GestureDetectorCompat; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; +import androidx.core.view.GestureDetectorCompat; + import me.devsaki.hentoid.R; public class OnZoneTapListener implements View.OnTouchListener { - /** This view's dimensions are used to determine which zone a tap belongs to */ + /** + * This view's dimensions are used to determine which zone a tap belongs to + */ private final View view; private final GestureDetectorCompat gestureDetector; @@ -45,34 +48,26 @@ public OnZoneTapListener setOnMiddleZoneTapListener(Runnable onMiddleZoneTapList return this; } + public boolean onSingleTapConfirmedAction(MotionEvent e) { + if (e.getX() < pagerTapZoneWidth) { + onLeftZoneTapListener.run(); + } else if (e.getX() > view.getWidth() - pagerTapZoneWidth) { + onRightZoneTapListener.run(); + } else { + onMiddleZoneTapListener.run(); + } + return true; + } + @Override public boolean onTouch(View v, MotionEvent event) { return gestureDetector.onTouchEvent(event); } - private final class OnGestureListener extends GestureDetector.SimpleOnGestureListener { + private final class OnGestureListener extends GestureDetector.SimpleOnGestureListener { // TODO remove if it proves useless @Override public boolean onSingleTapConfirmed(MotionEvent e) { - if (e.getX() < pagerTapZoneWidth) { - onLeftZoneTapListener.run(); - } else if (e.getX() > view.getWidth() - pagerTapZoneWidth) { - onRightZoneTapListener.run(); - } else { - onMiddleZoneTapListener.run(); - } - return true; + return onSingleTapConfirmedAction(e); } - -// @Override -// public boolean onSingleTapUp(MotionEvent e) { -// if (e.getX() < pagerTapZoneWidth) { -// onLeftZoneTapListener.run(); -// } else if (e.getX() > view.getWidth() - pagerTapZoneWidth) { -// onRightZoneTapListener.run(); -// } else { -// onMiddleZoneTapListener.run(); -// } -// return true; -// } } } diff --git a/app/src/main/java/me/devsaki/hentoid/widget/PageSnapWidget.java b/app/src/main/java/me/devsaki/hentoid/widget/PageSnapWidget.java index 3a3c0ee2d6..db4501e7d1 100644 --- a/app/src/main/java/me/devsaki/hentoid/widget/PageSnapWidget.java +++ b/app/src/main/java/me/devsaki/hentoid/widget/PageSnapWidget.java @@ -1,8 +1,8 @@ package me.devsaki.hentoid.widget; -import android.support.annotation.NonNull; -import android.support.v7.widget.PagerSnapHelper; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.PagerSnapHelper; +import androidx.recyclerview.widget.RecyclerView; import static java.lang.Math.abs; @@ -15,31 +15,46 @@ public final class PageSnapWidget { private final RecyclerView recyclerView; - private int flingFactor; + private float flingSensitivity; + + private boolean isEnabled; + public PageSnapWidget(@NonNull RecyclerView recyclerView) { this.recyclerView = recyclerView; + setPageSnapEnabled(true); } - public PageSnapWidget setPageSnapEnabled(boolean pageSnapEnabled) { + public void setPageSnapEnabled(boolean pageSnapEnabled) { if (pageSnapEnabled) { snapHelper.attachToRecyclerView(recyclerView); + isEnabled = true; } else { snapHelper.attachToRecyclerView(null); + isEnabled = false; } - return this; } - public PageSnapWidget setFlingFactor(int flingFactor) { - this.flingFactor = flingFactor; - return this; + public boolean isPageSnapEnabled() { return isEnabled; } + + /** + * Sets the sensitivity of a fling. + * + * @param sensitivity floating point sensitivity where 0 means never fling and 1 means always + * fling. Values beyond this range will have undefined behavior. + */ + public void setFlingSensitivity(float sensitivity) { + flingSensitivity = sensitivity; } private final class SnapHelper extends PagerSnapHelper { @Override public boolean onFling(int velocityX, int velocityY) { - int thresholdVelocity = recyclerView.getMinFlingVelocity() * flingFactor; - if (abs(velocityX) >= thresholdVelocity) { + int min = recyclerView.getMinFlingVelocity(); + int max = recyclerView.getMaxFlingVelocity(); + int threshold = (int) ((max * (1.0 - flingSensitivity)) + (min * flingSensitivity)); + + if (abs(velocityX) > threshold) { return false; } return super.onFling(velocityX, velocityY); diff --git a/app/src/main/java/me/devsaki/hentoid/widget/PrefetchLinearLayoutManager.java b/app/src/main/java/me/devsaki/hentoid/widget/PrefetchLinearLayoutManager.java index 6d9a9b3117..9f4acd57ae 100644 --- a/app/src/main/java/me/devsaki/hentoid/widget/PrefetchLinearLayoutManager.java +++ b/app/src/main/java/me/devsaki/hentoid/widget/PrefetchLinearLayoutManager.java @@ -1,9 +1,9 @@ package me.devsaki.hentoid.widget; import android.content.Context; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.OrientationHelper; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.OrientationHelper; +import androidx.recyclerview.widget.RecyclerView; import android.util.AttributeSet; import android.view.View; diff --git a/app/src/main/java/me/devsaki/hentoid/widget/ScrollPositionListener.java b/app/src/main/java/me/devsaki/hentoid/widget/ScrollPositionListener.java index 4a81e04460..b35e41fe0e 100644 --- a/app/src/main/java/me/devsaki/hentoid/widget/ScrollPositionListener.java +++ b/app/src/main/java/me/devsaki/hentoid/widget/ScrollPositionListener.java @@ -1,7 +1,7 @@ package me.devsaki.hentoid.widget; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.annimon.stream.function.IntConsumer; diff --git a/app/src/main/java/me/devsaki/hentoid/widget/ViewZoomGestureListener.java b/app/src/main/java/me/devsaki/hentoid/widget/ViewZoomGestureListener.java new file mode 100644 index 0000000000..59eef6a44a --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/widget/ViewZoomGestureListener.java @@ -0,0 +1,112 @@ +package me.devsaki.hentoid.widget; + +import android.content.Context; +import android.os.Handler; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +/** + * Credits go to the Tachiyomi team + */ +public class ViewZoomGestureListener extends GestureDetector { + + public ViewZoomGestureListener(Context context, Listener listener) { + super(context, listener); + this.listener = listener; + scaledTouchSlopslop = ViewConfiguration.get(context).getScaledTouchSlop(); + } + + private Handler handler = new Handler(); + private final int scaledTouchSlopslop; + private final int longTapTime = ViewConfiguration.getLongPressTimeout(); + private final int doubleTapTime = ViewConfiguration.getDoubleTapTimeout(); + + private float downX = 0f; + private float downY = 0f; + private long lastUp = 0L; + private MotionEvent lastDownEvent; + protected Listener listener; + + /** + * Runnable to execute when a long tap is confirmed. + */ + private Runnable longTapFn = new Runnable() { + @Override + public void run() { + listener.onLongTapConfirmed(lastDownEvent); + } + }; + + + @Override + public boolean onTouchEvent(MotionEvent ev) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + motionActionDown(ev); + } + break; + case MotionEvent.ACTION_MOVE: { + motionActionMove(ev); + } + break; + case MotionEvent.ACTION_UP: { + motionActionUp(ev); + } + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_POINTER_DOWN: { + motionActionCancelandPointerDown(); + } + break; + default: + // Nothing specific to do for other events + } + + return super.onTouchEvent(ev); + } + + private void motionActionDown(MotionEvent ev) { + if (lastDownEvent != null) lastDownEvent.recycle(); + lastDownEvent = MotionEvent.obtain(ev); + + // This is the key difference with the built-in detector. We have to ignore the + // event if the last up and current down are too close in time (double tap). + if (ev.getDownTime() - lastUp > doubleTapTime) { + downX = ev.getRawX(); + downY = ev.getRawY(); + handler.postDelayed(longTapFn, longTapTime); + } + } + + private void motionActionMove(MotionEvent ev) { + if (Math.abs(ev.getRawX() - downX) > scaledTouchSlopslop || Math.abs(ev.getRawY() - downY) > scaledTouchSlopslop) { + handler.removeCallbacks(longTapFn); + } + } + + private void motionActionUp(MotionEvent ev) { + lastUp = ev.getEventTime(); + handler.removeCallbacks(longTapFn); + } + + private void motionActionCancelandPointerDown() { + handler.removeCallbacks(longTapFn); + } + + /** + * Custom listener to also include a long tap confirmed + */ + public static class Listener extends SimpleOnGestureListener { + /** + * Notified when a long tap occurs with the initial on down [ev] that triggered it. + */ + public void onLongTapConfirmed(MotionEvent ev) { + // Nothing to see here + } + + public void onDoubleTapConfirmed(MotionEvent ev) { + // Nothing to see heer + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/widget/VolumeGestureListener.java b/app/src/main/java/me/devsaki/hentoid/widget/VolumeGestureListener.java new file mode 100644 index 0000000000..506de1b382 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/widget/VolumeGestureListener.java @@ -0,0 +1,76 @@ +package me.devsaki.hentoid.widget; + +import android.view.KeyEvent; +import android.view.View; + +public final class VolumeGestureListener implements View.OnKeyListener { + + private Runnable onVolumeDownListener; + + private Runnable onVolumeUpListener; + + private Runnable onBackListener; + + private int cooldown = 1000; + + private int turboCooldown = 500; + + private boolean isTurboEnabled = true; + + private long nextNotifyTime; + + public VolumeGestureListener setOnVolumeDownListener(Runnable onVolumeDownListener) { + this.onVolumeDownListener = onVolumeDownListener; + return this; + } + + public VolumeGestureListener setOnVolumeUpListener(Runnable onVolumeUpListener) { + this.onVolumeUpListener = onVolumeUpListener; + return this; + } + + public VolumeGestureListener setOnBackListener(Runnable onBackListener) { + this.onBackListener = onBackListener; + return this; + } + + public VolumeGestureListener setCooldown(int cooldown) { + this.cooldown = cooldown; + return this; + } + + public VolumeGestureListener setTurboCooldown(int turboCooldown) { + this.turboCooldown = turboCooldown; + return this; + } + + public VolumeGestureListener setTurboEnabled(boolean isTurboEnabled) { + this.isTurboEnabled = isTurboEnabled; + return this; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_DOWN) return false; + + Runnable listener; + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + listener = onVolumeDownListener; + } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + listener = onVolumeUpListener; + } else if (keyCode == KeyEvent.KEYCODE_BACK) { + listener = onBackListener; + } else { + return false; + } + + if (event.getRepeatCount() == 0) { + listener.run(); + nextNotifyTime = event.getEventTime() + cooldown; + } else if (event.getEventTime() >= nextNotifyTime) { + listener.run(); + nextNotifyTime = event.getEventTime() + (isTurboEnabled ? turboCooldown : cooldown); + } + return true; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/widget/VolumeKeyListener.java b/app/src/main/java/me/devsaki/hentoid/widget/VolumeKeyListener.java deleted file mode 100644 index 7ab1fabddc..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/widget/VolumeKeyListener.java +++ /dev/null @@ -1,43 +0,0 @@ -package me.devsaki.hentoid.widget; - -import android.view.KeyEvent; -import android.view.View; - -public final class VolumeKeyListener implements KeyInputDetector.OnKeyEventListener { - - private final KeyInputDetector detector; - - private Runnable onVolumeDownKeyListener; - private Runnable onVolumeUpKeyListener; - - public VolumeKeyListener() { - detector = new KeyInputDetector(this, 1000); - } - - public VolumeKeyListener setOnVolumeDownKeyListener(Runnable onVolumeDownKeyListener) { - this.onVolumeDownKeyListener = onVolumeDownKeyListener; - return this; - } - - public VolumeKeyListener setOnVolumeUpKeyListener(Runnable onVolumeUpKeyListener) { - this.onVolumeUpKeyListener = onVolumeUpKeyListener; - return this; - } - - public View.OnKeyListener getListener() - { - return detector; - } - - @Override - public boolean onEvent(int keyCode) { - if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { - onVolumeDownKeyListener.run(); - return true; - } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - onVolumeUpKeyListener.run(); - return true; - } - return false; - } -} diff --git a/app/src/main/res/color/primary_text_color_selector.xml b/app/src/main/res/color/primary_text_color_selector.xml index a52908e33e..bc310b3953 100644 --- a/app/src/main/res/color/primary_text_color_selector.xml +++ b/app/src/main/res/color/primary_text_color_selector.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/secondary_text_color_selector.xml b/app/src/main/res/color/secondary_text_color_selector.xml index a52908e33e..bc310b3953 100644 --- a/app/src/main/res/color/secondary_text_color_selector.xml +++ b/app/src/main/res/color/secondary_text_color_selector.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_button_circle.xml b/app/src/main/res/drawable/bg_button_circle.xml index bad89b1ae5..e4caaac9a5 100644 --- a/app/src/main/res/drawable/bg_button_circle.xml +++ b/app/src/main/res/drawable/bg_button_circle.xml @@ -2,7 +2,7 @@ - + diff --git a/app/src/main/res/drawable/bg_chip.xml b/app/src/main/res/drawable/bg_chip.xml index 658a02233b..abd37542af 100644 --- a/app/src/main/res/drawable/bg_chip.xml +++ b/app/src/main/res/drawable/bg_chip.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/bg_chip_disabled.xml b/app/src/main/res/drawable/bg_chip_disabled.xml index 78057c43b1..04ad1ab9a6 100644 --- a/app/src/main/res/drawable/bg_chip_disabled.xml +++ b/app/src/main/res/drawable/bg_chip_disabled.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_chip_pressed.xml b/app/src/main/res/drawable/bg_chip_pressed.xml index 149615f3bb..24e6a92adb 100644 --- a/app/src/main/res/drawable/bg_chip_pressed.xml +++ b/app/src/main/res/drawable/bg_chip_pressed.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_fab_extended.xml b/app/src/main/res/drawable/bg_fab_extended.xml index d816f10a12..fa71f9d683 100644 --- a/app/src/main/res/drawable/bg_fab_extended.xml +++ b/app/src/main/res/drawable/bg_fab_extended.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_pin_dialog.xml b/app/src/main/res/drawable/bg_pin_dialog.xml index 2403367b81..d3708e19f4 100644 --- a/app/src/main/res/drawable/bg_pin_dialog.xml +++ b/app/src/main/res/drawable/bg_pin_dialog.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/btn_content_selected_state.xml b/app/src/main/res/drawable/btn_content_selected_state.xml index 253674f79f..2b6b083e61 100644 --- a/app/src/main/res/drawable/btn_content_selected_state.xml +++ b/app/src/main/res/drawable/btn_content_selected_state.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_action_clear.xml b/app/src/main/res/drawable/ic_action_clear.xml index eb5142aa90..44a40f6aa1 100644 --- a/app/src/main/res/drawable/ic_action_clear.xml +++ b/app/src/main/res/drawable/ic_action_clear.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_action_download.xml b/app/src/main/res/drawable/ic_action_download.xml index 198fc40989..625a069ac5 100644 --- a/app/src/main/res/drawable/ic_action_download.xml +++ b/app/src/main/res/drawable/ic_action_download.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_action_gallery.xml b/app/src/main/res/drawable/ic_action_gallery.xml new file mode 100644 index 0000000000..ab36b07663 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_gallery.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_more_vertical.xml b/app/src/main/res/drawable/ic_action_more_vertical.xml new file mode 100644 index 0000000000..5176d8a4b1 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_more_vertical.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_next_item.xml b/app/src/main/res/drawable/ic_action_next_item.xml index 3f66992d6b..6d434d6bb0 100644 --- a/app/src/main/res/drawable/ic_action_next_item.xml +++ b/app/src/main/res/drawable/ic_action_next_item.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_action_pause.xml b/app/src/main/res/drawable/ic_action_pause.xml index 6a0cf1637b..77aa05761a 100644 --- a/app/src/main/res/drawable/ic_action_pause.xml +++ b/app/src/main/res/drawable/ic_action_pause.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_action_play.xml b/app/src/main/res/drawable/ic_action_play.xml index fed9532840..5afa627b26 100644 --- a/app/src/main/res/drawable/ic_action_play.xml +++ b/app/src/main/res/drawable/ic_action_play.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_action_previous_item.xml b/app/src/main/res/drawable/ic_action_previous_item.xml index 357ce8069d..e30df6802d 100644 --- a/app/src/main/res/drawable/ic_action_previous_item.xml +++ b/app/src/main/res/drawable/ic_action_previous_item.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_action_queue.xml b/app/src/main/res/drawable/ic_action_queue.xml new file mode 100644 index 0000000000..5d8ecb23f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_queue.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_refresh.xml b/app/src/main/res/drawable/ic_action_refresh.xml index 21eac4e432..517a6ddc6e 100644 --- a/app/src/main/res/drawable/ic_action_refresh.xml +++ b/app/src/main/res/drawable/ic_action_refresh.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_action_sd_storage.xml b/app/src/main/res/drawable/ic_action_sd_storage.xml index 0056aac608..33c7dd54cb 100644 --- a/app/src/main/res/drawable/ic_action_sd_storage.xml +++ b/app/src/main/res/drawable/ic_action_sd_storage.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_archive.xml b/app/src/main/res/drawable/ic_archive.xml index c6f2c944cf..cb8b4c0bd3 100644 --- a/app/src/main/res/drawable/ic_archive.xml +++ b/app/src/main/res/drawable/ic_archive.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml index 1a7e215024..1656e6a8b9 100644 --- a/app/src/main/res/drawable/ic_arrow_back.xml +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_attribute_tag.xml b/app/src/main/res/drawable/ic_attribute_tag.xml index d07a269c6e..e422f0900f 100644 --- a/app/src/main/res/drawable/ic_attribute_tag.xml +++ b/app/src/main/res/drawable/ic_attribute_tag.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_backspace.xml b/app/src/main/res/drawable/ic_backspace.xml index 2507caaad3..da536331e7 100644 --- a/app/src/main/res/drawable/ic_backspace.xml +++ b/app/src/main/res/drawable/ic_backspace.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml index c8c3a866f8..e1b5f6f61f 100644 --- a/app/src/main/res/drawable/ic_close.xml +++ b/app/src/main/res/drawable/ic_close.xml @@ -1,10 +1,9 @@ \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_create_new_folder.xml b/app/src/main/res/drawable/ic_create_new_folder.xml index e0c25a9b77..0695f29527 100644 --- a/app/src/main/res/drawable/ic_create_new_folder.xml +++ b/app/src/main/res/drawable/ic_create_new_folder.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_arrow_forward.xml b/app/src/main/res/drawable/ic_dark.xml similarity index 51% rename from app/src/main/res/drawable/ic_arrow_forward.xml rename to app/src/main/res/drawable/ic_dark.xml index 25fb386be5..dab027edf6 100644 --- a/app/src/main/res/drawable/ic_arrow_forward.xml +++ b/app/src/main/res/drawable/ic_dark.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_delete_forever.xml b/app/src/main/res/drawable/ic_delete_forever.xml index ea9636824b..9656e33013 100644 --- a/app/src/main/res/drawable/ic_delete_forever.xml +++ b/app/src/main/res/drawable/ic_delete_forever.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_delete_sweep.xml b/app/src/main/res/drawable/ic_delete_sweep.xml index 7c2e818725..bf7311ac55 100644 --- a/app/src/main/res/drawable/ic_delete_sweep.xml +++ b/app/src/main/res/drawable/ic_delete_sweep.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_dialog_warning.xml b/app/src/main/res/drawable/ic_dialog_warning.xml index 18c9033171..498da506a6 100644 --- a/app/src/main/res/drawable/ic_dialog_warning.xml +++ b/app/src/main/res/drawable/ic_dialog_warning.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_drawer.xml b/app/src/main/res/drawable/ic_drawer.xml new file mode 100644 index 0000000000..11acb8490f --- /dev/null +++ b/app/src/main/res/drawable/ic_drawer.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml index d191ba9265..7846e007d3 100644 --- a/app/src/main/res/drawable/ic_error.xml +++ b/app/src/main/res/drawable/ic_error.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_fav_empty.xml b/app/src/main/res/drawable/ic_fav_empty.xml index 6debc20ff7..7fdd4119a5 100644 --- a/app/src/main/res/drawable/ic_fav_empty.xml +++ b/app/src/main/res/drawable/ic_fav_empty.xml @@ -1,4 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_fav_full.xml b/app/src/main/res/drawable/ic_fav_full.xml index 8d2576afb3..9500430cea 100644 --- a/app/src/main/res/drawable/ic_fav_full.xml +++ b/app/src/main/res/drawable/ic_fav_full.xml @@ -1,4 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml index af0d4d067a..885988edf2 100644 --- a/app/src/main/res/drawable/ic_info.xml +++ b/app/src/main/res/drawable/ic_info.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_item_error_outline.xml b/app/src/main/res/drawable/ic_item_error_outline.xml index 06556c5cf1..6e5bec2c30 100644 --- a/app/src/main/res/drawable/ic_item_error_outline.xml +++ b/app/src/main/res/drawable/ic_item_error_outline.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_light.xml b/app/src/main/res/drawable/ic_light.xml new file mode 100644 index 0000000000..da4e0ca307 --- /dev/null +++ b/app/src/main/res/drawable/ic_light.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_8muses.xml b/app/src/main/res/drawable/ic_menu_8muses.xml new file mode 100644 index 0000000000..402dfe37ba --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_8muses.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_about.xml b/app/src/main/res/drawable/ic_menu_about.xml index a265cc5349..53137a40bb 100644 --- a/app/src/main/res/drawable/ic_menu_about.xml +++ b/app/src/main/res/drawable/ic_menu_about.xml @@ -7,6 +7,6 @@ android:viewportWidth="24.0" tools:ignore="UnusedResources"> diff --git a/app/src/main/res/drawable/ic_menu_asmcomics.xml b/app/src/main/res/drawable/ic_menu_asmcomics.xml index c421a53a97..b2a83aa207 100644 --- a/app/src/main/res/drawable/ic_menu_asmcomics.xml +++ b/app/src/main/res/drawable/ic_menu_asmcomics.xml @@ -1,43 +1,43 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:fillColor="#DEFFFFFF" + android:pathData="M1.033,7.661C1.194,6.813 1.827,6.128 2.272,5.406 2.949,4.793 3.531,3.97 4.386,3.607 5.244,2.594 6.38,3.732 5.682,4.734 5.188,5.409 5.069,4.24 5.55,4.007 5.195,3.342 4.059,4.607 3.606,4.998 2.954,5.486 2.681,6.31 2.239,6.93 1.84,7.655 1.305,8.489 1.38,9.384 1.406,10.556 2.684,9.046 2.921,8.619 3.544,7.82 4.152,7.071 4.711,6.246 4.862,5.802 5.587,4.457 5.918,5.504 5.756,6.409 5.404,7.161 5.105,7.987 4.808,8.556 4.718,9.87 5.509,8.897 5.867,8.634 6.891,7.659 6.418,8.745 5.967,9.279 4.849,10.265 4.299,9.363 3.985,8.808 4.521,7.648 4.368,7.467 4.01,7.986 3.749,8.407 3.383,8.836 3.051,9.482 2.515,10.315 1.664,10.508 0.725,10.469 0.459,9.294 0.701,8.684 0.844,8.71 0.643,7.865 1.033,7.661ZM10.976,8.976C11.924,8.244 11.029,7.444 10.139,7.403 9.354,6.985 8.074,7.235 7.67,6.304 7.704,5.386 8.598,4.802 9.285,4.333 10.002,3.988 10.706,3.656 11.512,3.476 12.356,3.329 13.282,3.375 13.99,3.764 14.554,4.102 14.654,5.875 13.764,5.236 14.552,4.374 12.702,4.058 12.065,4.244 11.185,4.43 10.313,4.509 9.667,5.025 8.917,5.273 7.528,6.396 8.975,6.705c0.545,-0.091 1.386,0.005 1.998,0.297 0.584,-0.162 0.578,0.37 1.059,0.254 0.655,0.361 0.289,1.378 0.011,1.564 0.39,-0.164 2.031,-1.095 1.27,-0.096 -0.857,0.434 -1.731,0.838 -2.597,1.258C9.876,10.72 8.756,10.986 7.674,11.14 6.88,10.73 8.465,10.22 8.88,10.059 9.561,9.639 10.439,9.595 10.976,8.976ZM9.126,10.301C8.687,10.313 7.636,10.927 8.646,10.61 9.026,10.639 10.078,9.94 9.126,10.301ZM14.969,5.859C15.712,5.596 17.442,4.464 17.478,5.918 16.963,6.712 16.471,7.517 16.052,8.371c0.724,-0.642 1.348,-1.411 2.07,-2.059 0.507,-0.52 1.078,-1.099 1.864,-1.275 1.192,0.15 -0.106,1.622 0.168,1.891 0.711,-0.822 1.501,-1.586 2.43,-2.162 1.056,-0.501 1.17,0.948 0.589,1.509 -0.423,0.921 -1.162,1.644 -1.401,2.6C21.618,9.317 22.141,10.494 21.21,9.922 20.539,9.421 20.796,8.364 21.286,7.792 21.628,7.387 21.944,6.404 22.507,5.886 22.677,5.511 22.769,5.134 22.261,5.56 21.688,6.093 20.987,6.524 20.528,7.109 19.843,7.6 19.413,9.014 18.519,8.824 18.165,8.006 18.99,7.244 19.348,6.551 20.098,5.477 18.676,6.367 18.323,6.736 17.624,7.197 17.041,7.874 16.504,8.457 16.057,8.925 15.693,9.668 15.184,10.229 14.011,11.073 14.297,9.172 14.97,8.792 15.141,8.166 15.557,7.252 16.081,6.903 15.758,6.866 17.198,5.202 16.038,5.736 15.232,6.075 14.324,6.5 13.79,7.178 12.821,7.522 13.488,6.221 14.146,6.293 14.417,6.165 14.784,6.116 14.969,5.859ZM19.944,7.142c0.157,-0.322 -0.091,0.081 0,0z" /> + android:strokeWidth="0.03754943" /> + android:fillColor="#DEFFFFFF" + android:pathData="m5.538,14.442q0.187,0 0.384,0.032 0.203,0.027 0.315,0.075 0.064,0.027 0.085,0.069 0.027,0.043 0.027,0.112v0.251q-0.155,-0.021 -0.347,-0.037 -0.187,-0.021 -0.389,-0.021 -0.24,0 -0.405,0.069 -0.165,0.064 -0.267,0.224 -0.101,0.16 -0.144,0.432 -0.043,0.267 -0.043,0.672 0,0.448 0.053,0.731 0.053,0.283 0.155,0.443 0.107,0.155 0.267,0.213 0.16,0.059 0.379,0.059 0.224,0 0.389,-0.027 0.165,-0.027 0.251,-0.027 0.133,0 0.133,0.128v0.261q-0.155,0.059 -0.373,0.101 -0.213,0.048 -0.496,0.048 -0.352,0 -0.603,-0.107 -0.251,-0.107 -0.416,-0.336 -0.16,-0.235 -0.235,-0.592 -0.075,-0.363 -0.075,-0.869 0,-0.512 0.075,-0.875 0.075,-0.363 0.235,-0.592 0.165,-0.229 0.421,-0.331 0.256,-0.107 0.624,-0.107z" + android:strokeColor="#00000000" /> + android:fillColor="#DEFFFFFF" + android:pathData="m7.296,16.346q0,0.411 0.032,0.688 0.037,0.272 0.123,0.443 0.091,0.165 0.235,0.24 0.149,0.069 0.373,0.069 0.224,0 0.368,-0.069 0.149,-0.075 0.235,-0.24 0.091,-0.171 0.128,-0.443 0.037,-0.277 0.037,-0.688 0,-0.411 -0.037,-0.683 -0.037,-0.277 -0.128,-0.443 -0.085,-0.171 -0.235,-0.24 -0.144,-0.075 -0.368,-0.075 -0.224,0 -0.373,0.075 -0.144,0.069 -0.235,0.24 -0.085,0.165 -0.123,0.443 -0.032,0.272 -0.032,0.683zM9.397,16.346q0,0.592 -0.096,0.965 -0.096,0.373 -0.272,0.581 -0.176,0.208 -0.427,0.288 -0.245,0.075 -0.544,0.075 -0.299,0 -0.544,-0.075 -0.245,-0.08 -0.421,-0.288 -0.176,-0.208 -0.272,-0.581 -0.096,-0.373 -0.096,-0.965 0,-0.592 0.096,-0.965 0.096,-0.373 0.272,-0.581 0.176,-0.208 0.421,-0.283 0.245,-0.08 0.544,-0.08 0.299,0 0.544,0.08 0.251,0.075 0.427,0.283 0.176,0.208 0.272,0.581 0.096,0.373 0.096,0.965z" + android:strokeColor="#00000000" /> + android:fillColor="#DEFFFFFF" + android:pathData="m12.605,18.069q-0.005,-0.315 -0.016,-0.677 -0.005,-0.368 -0.016,-0.741 -0.011,-0.379 -0.021,-0.747 -0.011,-0.368 -0.021,-0.693l-0.731,2.043h-0.379q-0.069,0 -0.096,-0.032 -0.021,-0.037 -0.043,-0.091l-0.688,-1.92q-0.011,0.336 -0.021,0.72 -0.011,0.384 -0.021,0.779 -0.011,0.389 -0.021,0.773 -0.005,0.379 -0.005,0.709h-0.405q-0.128,0 -0.128,-0.123 0.011,-0.869 0.048,-1.744 0.043,-0.88 0.096,-1.829h0.56q0.101,0 0.144,0.123l0.736,2.069 0.779,-2.192h0.528q0.064,0 0.091,0.032 0.027,0.032 0.032,0.091 0.059,0.88 0.096,1.765 0.037,0.88 0.048,1.808h-0.416q-0.128,0 -0.128,-0.123z" + android:strokeColor="#00000000" /> + android:fillColor="#DEFFFFFF" + android:pathData="m13.98,18.192q-0.128,0 -0.128,-0.123v-3.573h0.432q0.128,0 0.128,0.123v3.573z" + android:strokeColor="#00000000" /> + android:fillColor="#DEFFFFFF" + android:pathData="m16.434,14.442q0.187,0 0.384,0.032 0.203,0.027 0.315,0.075 0.064,0.027 0.085,0.069 0.027,0.043 0.027,0.112v0.251q-0.155,-0.021 -0.347,-0.037 -0.187,-0.021 -0.389,-0.021 -0.24,0 -0.405,0.069 -0.165,0.064 -0.267,0.224 -0.101,0.16 -0.144,0.432 -0.043,0.267 -0.043,0.672 0,0.448 0.053,0.731 0.053,0.283 0.155,0.443 0.107,0.155 0.267,0.213 0.16,0.059 0.379,0.059 0.224,0 0.389,-0.027 0.165,-0.027 0.251,-0.027 0.133,0 0.133,0.128v0.261q-0.155,0.059 -0.373,0.101 -0.213,0.048 -0.496,0.048 -0.352,0 -0.603,-0.107 -0.251,-0.107 -0.416,-0.336 -0.16,-0.235 -0.235,-0.592 -0.075,-0.363 -0.075,-0.869 0,-0.512 0.075,-0.875 0.075,-0.363 0.235,-0.592 0.165,-0.229 0.421,-0.331 0.256,-0.107 0.624,-0.107z" + android:strokeColor="#00000000" /> + android:fillColor="#DEFFFFFF" + android:pathData="m17.724,15.461q0,-0.507 0.288,-0.763 0.288,-0.256 0.891,-0.256 0.224,0 0.448,0.032 0.224,0.027 0.347,0.069 0.085,0.037 0.101,0.085 0.021,0.043 0.021,0.101v0.245q-0.197,-0.027 -0.405,-0.043 -0.208,-0.021 -0.427,-0.021 -0.208,0 -0.347,0.037 -0.139,0.032 -0.224,0.096 -0.08,0.064 -0.117,0.16 -0.032,0.096 -0.032,0.224 0,0.144 0.037,0.24 0.037,0.091 0.123,0.16 0.091,0.069 0.229,0.128 0.144,0.053 0.352,0.128 0.213,0.08 0.384,0.16 0.176,0.08 0.299,0.203 0.128,0.117 0.192,0.293 0.069,0.171 0.069,0.437 0,0.576 -0.309,0.827 -0.309,0.245 -0.896,0.245 -0.256,0 -0.496,-0.037 -0.24,-0.032 -0.373,-0.08 -0.069,-0.027 -0.096,-0.069 -0.027,-0.043 -0.027,-0.107v-0.267q0.187,0.027 0.411,0.053 0.229,0.027 0.485,0.027 0.224,0 0.368,-0.037 0.149,-0.043 0.229,-0.112 0.085,-0.075 0.117,-0.181 0.032,-0.107 0.032,-0.24 0,-0.149 -0.037,-0.245 -0.032,-0.096 -0.117,-0.16 -0.085,-0.069 -0.229,-0.128 -0.139,-0.059 -0.352,-0.133 -0.224,-0.08 -0.4,-0.165 -0.171,-0.085 -0.293,-0.203 -0.117,-0.123 -0.181,-0.288 -0.064,-0.171 -0.064,-0.416z" + android:strokeColor="#00000000" /> diff --git a/app/src/main/res/drawable/ic_menu_asmhentai.xml b/app/src/main/res/drawable/ic_menu_asmhentai.xml index b542fabfe4..31599ca599 100644 --- a/app/src/main/res/drawable/ic_menu_asmhentai.xml +++ b/app/src/main/res/drawable/ic_menu_asmhentai.xml @@ -2,10 +2,10 @@ + android:viewportWidth="24" + android:viewportHeight="24"> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_ehentai.xml b/app/src/main/res/drawable/ic_menu_ehentai.xml index 535a90e46c..d539f081bc 100644 --- a/app/src/main/res/drawable/ic_menu_ehentai.xml +++ b/app/src/main/res/drawable/ic_menu_ehentai.xml @@ -3,8 +3,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - + diff --git a/app/src/main/res/drawable/ic_menu_fakku.xml b/app/src/main/res/drawable/ic_menu_fakku.xml index c1be809755..4899954e6f 100644 --- a/app/src/main/res/drawable/ic_menu_fakku.xml +++ b/app/src/main/res/drawable/ic_menu_fakku.xml @@ -1,9 +1,21 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/ic_menu_hentaicafe.xml b/app/src/main/res/drawable/ic_menu_hentaicafe.xml index 8621a3c645..e8a3a9496c 100644 --- a/app/src/main/res/drawable/ic_menu_hentaicafe.xml +++ b/app/src/main/res/drawable/ic_menu_hentaicafe.xml @@ -2,11 +2,11 @@ + android:viewportWidth="512" + android:viewportHeight="512"> diff --git a/app/src/main/res/drawable/ic_menu_mikan.xml b/app/src/main/res/drawable/ic_menu_mikan.xml index c24937e509..cea369fe62 100644 --- a/app/src/main/res/drawable/ic_menu_mikan.xml +++ b/app/src/main/res/drawable/ic_menu_mikan.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_menu_nexus.xml b/app/src/main/res/drawable/ic_menu_nexus.xml new file mode 100644 index 0000000000..ad769df4b1 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_nexus.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_menu_nhentai.xml b/app/src/main/res/drawable/ic_menu_nhentai.xml index 5b08864e89..e17f029d67 100644 --- a/app/src/main/res/drawable/ic_menu_nhentai.xml +++ b/app/src/main/res/drawable/ic_menu_nhentai.xml @@ -2,11 +2,11 @@ + android:viewportWidth="1024" + android:viewportHeight="1024"> - + + diff --git a/app/src/main/res/drawable/ic_menu_prefs.xml b/app/src/main/res/drawable/ic_menu_prefs.xml index 09040f21eb..133cedce1b 100644 --- a/app/src/main/res/drawable/ic_menu_prefs.xml +++ b/app/src/main/res/drawable/ic_menu_prefs.xml @@ -3,10 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" android:width="24dp" android:height="24dp" - android:viewportHeight="24.0" android:viewportWidth="24.0" + android:viewportHeight="24.0" tools:ignore="UnusedResources"> diff --git a/app/src/main/res/drawable/ic_menu_pururin.xml b/app/src/main/res/drawable/ic_menu_pururin.xml index e28cb29d15..7e5e19fa2a 100644 --- a/app/src/main/res/drawable/ic_menu_pururin.xml +++ b/app/src/main/res/drawable/ic_menu_pururin.xml @@ -1,5 +1,11 @@ - - + + diff --git a/app/src/main/res/drawable/ic_menu_queue.xml b/app/src/main/res/drawable/ic_menu_queue.xml index eb25166ed9..894478751d 100644 --- a/app/src/main/res/drawable/ic_menu_queue.xml +++ b/app/src/main/res/drawable/ic_menu_queue.xml @@ -3,10 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" android:width="24dp" android:height="24dp" - android:viewportHeight="24.0" android:viewportWidth="24.0" + android:viewportHeight="24.0" tools:ignore="UnusedResources"> diff --git a/app/src/main/res/drawable/ic_menu_search.xml b/app/src/main/res/drawable/ic_menu_search.xml index 4b0262c61b..d3a9454b4a 100644 --- a/app/src/main/res/drawable/ic_menu_search.xml +++ b/app/src/main/res/drawable/ic_menu_search.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_menu_sort_by_date.xml b/app/src/main/res/drawable/ic_menu_sort_123.xml similarity index 92% rename from app/src/main/res/drawable/ic_menu_sort_by_date.xml rename to app/src/main/res/drawable/ic_menu_sort_123.xml index 231c5702e7..51b01f97a7 100644 --- a/app/src/main/res/drawable/ic_menu_sort_by_date.xml +++ b/app/src/main/res/drawable/ic_menu_sort_123.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_menu_sort_321.xml b/app/src/main/res/drawable/ic_menu_sort_321.xml index a39963dd7c..43ecb9ec50 100644 --- a/app/src/main/res/drawable/ic_menu_sort_321.xml +++ b/app/src/main/res/drawable/ic_menu_sort_321.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:fillColor="#DEFFFFFF" + android:pathData="m2,5.508h2v0.5L3,6.008v1h1v0.5L2,7.508v1h3v-4L2,4.508ZM2.988,19.271h1v-4h-2v1h1zM2,11L3.8,11L2,13.1 1.784,14.288h3L5,13L3.2,13L5,10.9L5,10L2,10ZM6.988,16.271v2L20.988,18.271v-2zM7,7.508h14v-2L7,5.508ZM7,13L21,13L21,11L7,11Z" /> diff --git a/app/src/main/res/drawable/ic_menu_sort_alpha.xml b/app/src/main/res/drawable/ic_menu_sort_az.xml similarity index 81% rename from app/src/main/res/drawable/ic_menu_sort_alpha.xml rename to app/src/main/res/drawable/ic_menu_sort_az.xml index 5cc8e5cc27..56e832ab13 100644 --- a/app/src/main/res/drawable/ic_menu_sort_alpha.xml +++ b/app/src/main/res/drawable/ic_menu_sort_az.xml @@ -2,9 +2,9 @@ + android:viewportWidth="24.0" + android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_menu_sort_last_read.xml b/app/src/main/res/drawable/ic_menu_sort_last_read.xml index de25eb445f..7b19268049 100644 --- a/app/src/main/res/drawable/ic_menu_sort_last_read.xml +++ b/app/src/main/res/drawable/ic_menu_sort_last_read.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_menu_sort_random.xml b/app/src/main/res/drawable/ic_menu_sort_random.xml index 04850bdcd8..c068d95452 100644 --- a/app/src/main/res/drawable/ic_menu_sort_random.xml +++ b/app/src/main/res/drawable/ic_menu_sort_random.xml @@ -1,9 +1,9 @@ - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + diff --git a/app/src/main/res/drawable/ic_menu_sort_read.xml b/app/src/main/res/drawable/ic_menu_sort_read.xml index 743220b56c..4fbb1b29c4 100644 --- a/app/src/main/res/drawable/ic_menu_sort_read.xml +++ b/app/src/main/res/drawable/ic_menu_sort_read.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_menu_sort_unread.xml b/app/src/main/res/drawable/ic_menu_sort_unread.xml index 44c4a85a93..4c1f23cd1b 100644 --- a/app/src/main/res/drawable/ic_menu_sort_unread.xml +++ b/app/src/main/res/drawable/ic_menu_sort_unread.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_menu_sort_za.xml b/app/src/main/res/drawable/ic_menu_sort_za.xml index 980ae76378..b53c08046e 100644 --- a/app/src/main/res/drawable/ic_menu_sort_za.xml +++ b/app/src/main/res/drawable/ic_menu_sort_za.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:fillColor="#DEFFFFFF" + android:pathData="M14.94,4.66L10.22,4.66L12.58,2.3ZM10.25,19.37h4.66l-2.33,2.33zM16.88,6.27 L12.38,17.73h1.84l0.92,-2.45h5.11l0.92,2.45h1.84l-4.49,-11.46zM15.75,13.64 L17.69,8.46 19.63,13.64zM4.544,16.14h6.12v1.59L2.134,17.73v-1.29l5.92,-8.56h-5.88v-1.6h8.3v1.26z" /> diff --git a/app/src/main/res/drawable/ic_menu_tsumino.xml b/app/src/main/res/drawable/ic_menu_tsumino.xml index 84ce024411..67f25d5fa5 100644 --- a/app/src/main/res/drawable/ic_menu_tsumino.xml +++ b/app/src/main/res/drawable/ic_menu_tsumino.xml @@ -3,12 +3,12 @@ xmlns:tools="http://schemas.android.com/tools" android:width="24dp" android:height="24dp" - android:viewportHeight="1024" android:viewportWidth="1024" + android:viewportHeight="1024" tools:ignore="UnusedResources"> - + + diff --git a/app/src/main/res/drawable/ic_queued.xml b/app/src/main/res/drawable/ic_queued.xml deleted file mode 100644 index aa28f2568a..0000000000 --- a/app/src/main/res/drawable/ic_queued.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml index 12273df421..28378f1136 100644 --- a/app/src/main/res/drawable/ic_search.xml +++ b/app/src/main/res/drawable/ic_search.xml @@ -1,5 +1,9 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml index a20affa175..9d8c1e32ea 100644 --- a/app/src/main/res/drawable/ic_settings.xml +++ b/app/src/main/res/drawable/ic_settings.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml index e9106594b6..0ff7ecebc9 100644 --- a/app/src/main/res/drawable/ic_share.xml +++ b/app/src/main/res/drawable/ic_share.xml @@ -5,6 +5,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable/ic_stat_hentoid.xml b/app/src/main/res/drawable/ic_stat_hentoid.xml index 5beb4a0b5d..cbd49e9c19 100644 --- a/app/src/main/res/drawable/ic_stat_hentoid.xml +++ b/app/src/main/res/drawable/ic_stat_hentoid.xml @@ -1,9 +1,18 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/ic_stat_hentoid_warning.xml b/app/src/main/res/drawable/ic_stat_hentoid_warning.xml index 5dcbf91624..ce816af7f7 100644 --- a/app/src/main/res/drawable/ic_stat_hentoid_warning.xml +++ b/app/src/main/res/drawable/ic_stat_hentoid_warning.xml @@ -1,12 +1,24 @@ - - - - - + + + + + android:strokeWidth="21.57841873" + android:strokeColor="#00000000" /> diff --git a/app/src/main/res/drawable/line_divider.xml b/app/src/main/res/drawable/line_divider.xml new file mode 100644 index 0000000000..a6f985945f --- /dev/null +++ b/app/src/main/res/drawable/line_divider.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/row_item_cover_frame.xml b/app/src/main/res/drawable/row_item_cover_frame.xml index b517883ca9..df065e475c 100644 --- a/app/src/main/res/drawable/row_item_cover_frame.xml +++ b/app/src/main/res/drawable/row_item_cover_frame.xml @@ -9,5 +9,5 @@ + android:color="@color/secondary" /> diff --git a/app/src/main/res/drawable/row_item_selector.xml b/app/src/main/res/drawable/row_item_selector.xml index faea117e52..2b3aabbd42 100644 --- a/app/src/main/res/drawable/row_item_selector.xml +++ b/app/src/main/res/drawable/row_item_selector.xml @@ -1,8 +1,8 @@ - - + + - + diff --git a/app/src/main/res/layout-v21/include_downloads_toolbar.xml b/app/src/main/res/layout-v21/include_downloads_toolbar.xml deleted file mode 100644 index e012ebda28..0000000000 --- a/app/src/main/res/layout-v21/include_downloads_toolbar.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout-v21/include_drawer_list.xml b/app/src/main/res/layout-v21/include_drawer_list.xml deleted file mode 100644 index d9cd7a1136..0000000000 --- a/app/src/main/res/layout-v21/include_drawer_list.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout-v21/include_toolbar.xml b/app/src/main/res/layout-v21/include_toolbar.xml deleted file mode 100644 index b389dcfdf0..0000000000 --- a/app/src/main/res/layout-v21/include_toolbar.xml +++ /dev/null @@ -1,9 +0,0 @@ - - diff --git a/app/src/main/res/layout-v21/nav_header.xml b/app/src/main/res/layout-v21/nav_header.xml deleted file mode 100644 index f6af742999..0000000000 --- a/app/src/main/res/layout-v21/nav_header.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index 5f5632b87e..487d3eb9a6 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -7,7 +7,7 @@ android:orientation="vertical" tools:context=".activities.AboutActivity"> - @@ -19,7 +19,8 @@ android:src="@drawable/ic_hentoid" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:ignore="ContentDescription" /> + tools:ignore="ContentDescription" + /> + + + tools:text="Hentoid X.X.X (bbb)" /> -