diff --git a/README-ru.md b/README-ru.md index cb4306621f..987a359a74 100644 --- a/README-ru.md +++ b/README-ru.md @@ -12,7 +12,7 @@ ___ ![](https://github.com/avluis/Hentoid/blob/master/wiki-img/welcome-ru.png) -###### [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/en-flag.svg) English](https://github.com/avluis/Hentoid/tree/master/README.md) | [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/ru-flag.svg) Русский](https://github.com/avluis/Hentoid/tree/master/README-ru.md) +###### [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/en-flag.svg) English](https://github.com/avluis/Hentoid/tree/master/README.md) | [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/ru-flag.svg) Русский](https://github.com/avluis/Hentoid/tree/master/README-ru.md) | [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/uk-flag.svg) Українська](https://github.com/avluis/Hentoid/tree/master/README-uk.md) ##### `Hentoid` — это приложение для скачивания додзинси и хентай-манги без рекламы. ###### `Hentoid` поддерживает следующие сайты: @@ -21,7 +21,7 @@ ___ ##### Посетите [вики Hentoid](https://github.com/AVnetWS/Hentoid/wiki), если вам нужна помощь с установкой `Hentoid` на ваше устройство. ##### Последнюю версию можно найти [здесь](https://github.com/AVnetWS/Hentoid/releases/latest). -##### Не забывайте о [ЧаВО](https://github.com/avluis/Hentoid/wiki/FAQ), если у вас есть какие-либо проблемы +##### Не забывайте о [ЧаВО](https://github.com/avluis/Hentoid/wiki/FAQ), если у вас есть какие-либо проблемы. ##### На нашем [сервере в Discord](https://discord.gg/QEZ3qk9) вы можете пообщаться с другими пользователями, узнать про последние объявления, найти бета-версии или просто пообщаться с командой разработчиков. ### Учтите: язык сервера — английский. Пожалуйста, не пишите на нём по-русски. ___ diff --git a/README-uk.md b/README-uk.md new file mode 100644 index 0000000000..bc2eb07cb8 --- /dev/null +++ b/README-uk.md @@ -0,0 +1,33 @@ + +___ + +[![Завантажити](https://img.shields.io/badge/Завантажити-APK-brightgreen.svg)](https://github.com/avluis/Hentoid/releases/latest) + +[![Контроль якості](https://sonarcloud.io/api/project_badges/measure?project=RobbWatershed_hentoid-sonar&metric=alert_status)](https://sonarcloud.io/dashboard?id=RobbWatershed_hentoid-sonar) [![Статус збірки](https://app.bitrise.io/app/70539fbfc39cb9d8/status.svg?token=_kOMCvtscTfWpw7mdsqvJA&branch=dev)](https://app.bitrise.io/app/70539fbfc39cb9d8) + +[![](https://discordapp.com/api/guilds/173995475098271746/embed.png?style=banner2)](https://discord.gg/QEZ3qk9) + +![](https://github.com/avluis/Hentoid/blob/master/wiki-img/welcome-uk.png) + +###### [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/en-flag.svg) English](https://github.com/avluis/Hentoid/tree/master/README.md) | [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/ru-flag.svg) Русский](https://github.com/avluis/Hentoid/tree/master/README-ru.md) | [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/uk-flag.svg) Українська](https://github.com/avluis/Hentoid/tree/master/README-uk.md) + +##### `Hentoid` — це додаток для завантаження додзінсі та хентай-манґи без реклами. +###### `Hentoid` підтримує наступні сайти: +###### [nhentai](https://nhentai.net/), [hitomi](https://hitomi.la/), [asmhentai](http://asmhentai.com/), [tsumino](http://www.tsumino.com/), [pururin](https://pururin.io/), [e-hentai](https://e-hentai.org/), [exHentai/sadpanda](https://exhentai.org), [8muses](https://www.8muses.com), [Doujins.com](https://doujins.com), [Luscious.net](https://www.luscious.net), [Porncomixonline](https://www.porncomixonline.net/), [HBrowse](https://www.hbrowse.com/), [Hentai2Read](https://hentai2read.com/), [HentaiFox](https://hentaifox.com), [Myreadingmanga](https://myreadingmanga.info/), [Manwha Hentai](https://manhwahentai.me/), [Imhentai](https://imhentai.com), [Toonily](https://toonily.com/), [Allporncomic](https://allporncomic.com/) і [Pixiv](https://www.pixiv.net/). +###### Увага: В `Hentoid` все ще можна імпортувати завантаження з [FAKKU](https://www.fakku.net/) й [Pururin](https://raw.githubusercontent.com/AVnetWS/Hentoid-Resources/master/repo/assets/img/pururin.jpg)'у. +##### Завітайте на [вікі Hentoid](https://github.com/AVnetWS/Hentoid/wiki), якщо вам потрібна домога з установкою `Hentoid` на ваш пристрій. +##### Останню версію можна знайти [тут](https://github.com/AVnetWS/Hentoid/releases/latest). + +##### Не забувайте про [ЧаВО](https://github.com/avluis/Hentoid/wiki/FAQ), якщо у вас є будь-які проблеми. +##### На нашому [сервері Discord](https://discord.gg/QEZ3qk9) ви можете поспілкуватися з іншими користувачами, дізнатися про останні оголошення, знайти бета-версії або просто поспілкуватися з командою розробників. +### Увага: мова сервера — англійська. Будь ласка, не пишіть на ньому російською чи українською. +___ +###### [Допожіть нам](https://github.com/AVnetWS/Hentoid/wiki/Contributing) зробити для вас `Hentoid` найкращим додатком для хентай-манґи. + +[![](https://github.com/avluis/Hentoid/blob/master/wiki-img/CherryBanner.png)](https://github.com/RobbWatershed/GalleryCherry) +___ + + diff --git a/README.md b/README.md index 122db178b8..fd5311b2a7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ___ ![](https://github.com/avluis/Hentoid/blob/master/wiki-img/welcome.png) -###### [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/en-flag.svg) English](https://github.com/avluis/Hentoid/tree/master/README.md) | [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/ru-flag.svg) Русский](https://github.com/avluis/Hentoid/tree/master/README-ru.md) +###### [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/en-flag.svg) English](https://github.com/avluis/Hentoid/tree/master/README.md) | [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/ru-flag.svg) Русский](https://github.com/avluis/Hentoid/tree/master/README-ru.md) | [![](https://github.com/avluis/Hentoid/blob/master/wiki-img/uk-flag.svg) Українська](https://github.com/avluis/Hentoid/tree/master/README-uk.md) ##### `Hentoid` is an ad-free Doujinshi & H-Manga archiving and viewing app. diff --git a/app/build.gradle b/app/build.gradle index a789ec1752..97f838ec41 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,7 +35,7 @@ android { //noinspection ExpiringTargetSdkVersion targetSdkVersion 31 versionCode 130 // is updated automatically by BitRise; only used when building locally - versionName '1.15.23' + versionName '1.16.1' def includeObjectBoxBrowser = System.getenv("INCLUDE_OBJECTBOX_BROWSER") ?: "false" def includeLeakCanary = System.getenv("INCLUDE_LEAK_CANARY") ?: "false" @@ -46,7 +46,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' // Only include complete translations - resConfigs 'en', 'ru', 'it' + resConfigs 'en', 'ru', 'it', 'uk' renderscriptTargetApi 21 renderscriptSupportModeEnabled false @@ -104,7 +104,7 @@ dependencies { * TESTING */ testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.3.1' + testImplementation 'org.mockito:mockito-core:4.4.0' testImplementation 'androidx.test:core:1.4.0' testImplementation 'io.kotlintest:kotlintest-assertions:3.4.2' testImplementation "androidx.arch.core:core-testing:2.1.0" @@ -160,7 +160,7 @@ dependencies { implementation "com.github.zjupure:webpdecoder:2.0.$webp_glide_version" // Animated PNG (apng) support -> https://github.com/penfeizhou/APNG4Android - implementation 'com.github.penfeizhou.android.animation:apng:2.17.2' + implementation 'com.github.penfeizhou.android.animation:apng:2.18.0' // Animated GIF creator -> https://github.com/waynejo/android-ndk-gif implementation 'com.waynejo:androidndkgif:0.3.3' @@ -186,7 +186,7 @@ dependencies { implementation 'com.github.AppIntro:AppIntro:6.1.0' // Tooltips - implementation 'com.github.skydoves:balloon:1.4.1' + implementation 'com.github.skydoves:balloon:1.4.3' // Dropdown lists implementation 'com.github.skydoves:powerspinner:1.1.9' @@ -279,7 +279,7 @@ dependencies { implementation 'org.apache.commons:commons-text:1.9' // Cleaner date manipulation - implementation 'com.jakewharton.threetenabp:threetenabp:1.3.1' + implementation 'com.jakewharton.threetenabp:threetenabp:1.4.0' // Archive management implementation 'com.github.omicronapps:7-Zip-JBinding-4Android:Release-16.02-2.02' diff --git a/app/objectbox-models/default.json b/app/objectbox-models/default.json index 578697c36e..ee8a7eade0 100644 --- a/app/objectbox-models/default.json +++ b/app/objectbox-models/default.json @@ -41,7 +41,7 @@ }, { "id": "2:5880334030341287801", - "lastPropertyId": "28:576356326453963220", + "lastPropertyId": "29:6098325687122950518", "name": "Content", "properties": [ { @@ -185,6 +185,11 @@ "id": "28:576356326453963220", "name": "manuallyMerged", "type": 1 + }, + { + "id": "29:6098325687122950518", + "name": "downloadCompletionDate", + "type": 6 } ], "relations": [ @@ -619,7 +624,7 @@ }, { "id": "14:4653544552059492176", - "lastPropertyId": "6:2875790170342417054", + "lastPropertyId": "7:8254213446255957923", "name": "Chapter", "properties": [ { @@ -655,6 +660,11 @@ "id": "6:2875790170342417054", "name": "uniqueId", "type": 9 + }, + { + "id": "7:8254213446255957923", + "name": "uploadDate", + "type": 6 } ], "relations": [] 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 1698d3ddd1..fc556156e6 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/ImageViewerActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/ImageViewerActivity.java @@ -50,7 +50,7 @@ protected void onCreate(Bundle savedInstanceState) { if (null == intent || null == intent.getExtras()) throw new IllegalArgumentException("Required init arguments not found"); - ImageViewerActivityBundle.Parser parser = new ImageViewerActivityBundle.Parser(intent.getExtras()); + ImageViewerActivityBundle parser = new ImageViewerActivityBundle(intent.getExtras()); long contentId = parser.getContentId(); if (0 == contentId) throw new IllegalArgumentException("Incorrect ContentId"); int pageNumber = parser.getPageNumber(); diff --git a/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.java index 2729023d1c..d290088c68 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.java @@ -6,7 +6,7 @@ import static me.devsaki.hentoid.events.CommunicationEvent.EV_DISABLE; import static me.devsaki.hentoid.events.CommunicationEvent.EV_ENABLE; import static me.devsaki.hentoid.events.CommunicationEvent.EV_SEARCH; -import static me.devsaki.hentoid.events.CommunicationEvent.EV_UPDATE_SORT; +import static me.devsaki.hentoid.events.CommunicationEvent.EV_UPDATE_TOOLBAR; import static me.devsaki.hentoid.events.CommunicationEvent.RC_CONTENTS; import static me.devsaki.hentoid.events.CommunicationEvent.RC_DRAWER; import static me.devsaki.hentoid.events.CommunicationEvent.RC_GROUPS; @@ -21,14 +21,12 @@ import android.view.MenuItem; import android.view.View; import android.view.WindowManager; -import android.widget.ImageView; import android.widget.TextView; -import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBarDrawerToggle; -import androidx.appcompat.widget.PopupMenu; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import androidx.core.view.GravityCompat; @@ -52,6 +50,8 @@ import org.greenrobot.eventbus.ThreadMode; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -63,10 +63,13 @@ import me.devsaki.hentoid.database.ObjectBoxDAO; import me.devsaki.hentoid.database.domains.Attribute; import me.devsaki.hentoid.database.domains.Content; +import me.devsaki.hentoid.database.domains.Group; import me.devsaki.hentoid.enums.Grouping; import me.devsaki.hentoid.events.AppUpdatedEvent; import me.devsaki.hentoid.events.CommunicationEvent; import me.devsaki.hentoid.events.ProcessEvent; +import me.devsaki.hentoid.fragments.library.LibraryBottomGroupsFragment; +import me.devsaki.hentoid.fragments.library.LibraryBottomSortFilterFragment; import me.devsaki.hentoid.fragments.library.LibraryContentFragment; import me.devsaki.hentoid.fragments.library.LibraryGroupsFragment; import me.devsaki.hentoid.fragments.library.UpdateSuccessDialogFragment; @@ -75,7 +78,6 @@ import me.devsaki.hentoid.notification.archive.ArchiveProgressNotification; import me.devsaki.hentoid.notification.archive.ArchiveStartNotification; import me.devsaki.hentoid.util.ContentHelper; -import me.devsaki.hentoid.util.Debouncer; import me.devsaki.hentoid.util.FileHelper; import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.PermissionHelper; @@ -84,6 +86,8 @@ import me.devsaki.hentoid.util.notification.NotificationManager; import me.devsaki.hentoid.viewmodels.LibraryViewModel; import me.devsaki.hentoid.viewmodels.ViewModelFactory; +import me.devsaki.hentoid.widget.ContentSearchManager; +import me.devsaki.hentoid.widget.GroupSearchManager; import timber.log.Timber; @SuppressLint("NonConstantResourceId") @@ -104,19 +108,11 @@ public class LibraryActivity extends BaseActivity { // ==== Advanced search / sort bar // Grey background of the advanced search / sort bar - private View searchSortBar; + private View searchSubBar; // Advanced search text button private View advancedSearchButton; - // Show artists / groups button - private TextView showArtistsGroupsButton; // CLEAR button private View searchClearButton; - // Sort direction button - private ImageView sortDirectionButton; - // Sort reshuffle button - private ImageView sortReshuffleButton; - // Sort field button - private TextView sortFieldButton; // === Alert bar // Background and text of the alert bar @@ -130,20 +126,18 @@ public class LibraryActivity extends BaseActivity { private Toolbar toolbar; // "Search" button on top menu private MenuItem searchMenu; - // "Edit mode" / "Validate edit" button on top menu + // "Display type" button on top menu + private MenuItem displayTypeMenu; + // "Edit mode" button on top menu private MenuItem reorderMenu; + // "Confirm edit" button on top menu + private MenuItem reorderConfirmMenu; // "Cancel edit" button on top menu private MenuItem reorderCancelMenu; // "Create new group" button on top menu private MenuItem newGroupMenu; - // "Toggle completed" button on top menu - private MenuItem completedFilterMenu; - // "Toggle favourites" button on top menu - private MenuItem favsMenu; // "Sort" button on top menu private MenuItem sortMenu; - // Alert bars - private PopupMenu autoHidePopup; // === Selection toolbar private Toolbar selectionToolbar; @@ -175,22 +169,22 @@ public class LibraryActivity extends BaseActivity { // ======== VARIABLES // Used to ignore native calls to onQueryTextChange private boolean invalidateNextQueryTextChange = false; - // Current text search query - private String query = ""; - // Current metadata search query - private List metadata = Collections.emptyList(); + // Current text search query; one per tab + private final List query = Arrays.asList("", ""); + // Current metadata search query; one per tab + private final List> metadata = Arrays.asList(new ArrayList<>(), new ArrayList<>()); // True if item positioning edit mode is on (only available for specific groupings) private boolean editMode = false; - // True if there's at least one existing custom group; false instead - private boolean isCustomGroupingAvailable; // Titles of each of the Viewpager2's tabs private final Map titles = new HashMap<>(); // TODO doc - private boolean isGroupFavsChecked = false; - - - // Used to auto-hide the sort controls bar when no activity is detected - private Debouncer sortCommandsAutoHide; + private Group group = null; + // TODO doc + private Grouping grouping = Preferences.getGroupingDisplay(); + // TODO doc + private Bundle contentSearchBundle = null; + // TODO doc + private Bundle groupSearchBundle = null; // === PUBLIC ACCESSORS (to be used by fragments) @@ -199,32 +193,20 @@ public Toolbar getSelectionToolbar() { return selectionToolbar; } - public ImageView getSortDirectionButton() { - return sortDirectionButton; - } - - public View getSortReshuffleButton() { - return sortReshuffleButton; - } - - public TextView getSortFieldButton() { - return sortFieldButton; - } - public String getQuery() { - return query; + return query.get(getCurrentFragmentIndex()); } public void setQuery(String query) { - this.query = query; + this.query.set(getCurrentFragmentIndex(), query); } public List getMetadata() { - return metadata; + return metadata.get(getCurrentFragmentIndex()); } public void setMetadata(List metadata) { - this.metadata = metadata; + this.metadata.set(getCurrentFragmentIndex(), metadata); } public boolean isEditMode() { @@ -236,14 +218,6 @@ public void setEditMode(boolean editMode) { updateToolbar(); } - public void toggleEditMode() { - setEditMode(!editMode); - } - - public boolean isGroupFavsChecked() { - return isGroupFavsChecked; - } - @Override protected void onCreate(Bundle savedInstanceState) { @@ -299,7 +273,16 @@ public void onDrawerClosed(View view) { ViewModelFactory vmFactory = new ViewModelFactory(getApplication()); viewModel = new ViewModelProvider(this, vmFactory).get(LibraryViewModel.class); - viewModel.isCustomGroupingAvailable().observe(this, b -> this.isCustomGroupingAvailable = b); + viewModel.getContentSearchManagerBundle().observe(this, b -> contentSearchBundle = b); + viewModel.getGroup().observe(this, g -> { + group = g; + updateToolbar(); + }); + viewModel.getGroupSearchManagerBundle().observe(this, b -> { + groupSearchBundle = b; + GroupSearchManager.GroupSearchBundle searchBundle = new GroupSearchManager.GroupSearchBundle(b); + onGroupingChanged(searchBundle.getGroupingId()); + }); if (!Preferences.getRecentVisibility()) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); @@ -315,7 +298,6 @@ public void onDrawerClosed(View view) { updateSelectionToolbar(0, 0, 0, 0); onCreated(); - sortCommandsAutoHide = new Debouncer<>(this, 3000, this::hideSearchSortBar); if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this); } @@ -375,7 +357,7 @@ protected void onStart() { try { Content c = dao.selectContent(previouslyViewedContent); if (c != null) - ContentHelper.openHentoidViewer(this, c, previouslyViewedPage, viewModel.getSearchManagerBundle(), false); + ContentHelper.openHentoidViewer(this, c, previouslyViewedPage, contentSearchBundle, false); } finally { dao.cleanup(); } @@ -397,36 +379,22 @@ private void initUI() { alertFixBtn = findViewById(R.id.library_alert_fix_btn); // Search bar - searchSortBar = findViewById(R.id.advanced_search_background); - - // "Group by" menu - View groupByButton = findViewById(R.id.group_by_btn); - groupByButton.setOnClickListener(this::onGroupByButtonClick); + searchSubBar = findViewById(R.id.advanced_search_background); // Link to advanced search advancedSearchButton = findViewById(R.id.advanced_search_btn); advancedSearchButton.setOnClickListener(v -> onAdvancedSearchButtonClick()); - // "Show artists/groups" menu (group tab only when Grouping.ARTIST is on) - showArtistsGroupsButton = findViewById(R.id.groups_visibility_btn); - showArtistsGroupsButton.setText(getArtistsGroupsTextFromPrefs()); - showArtistsGroupsButton.setOnClickListener(this::onGroupsVisibilityButtonClick); - // Clear search searchClearButton = findViewById(R.id.search_clear_btn); searchClearButton.setOnClickListener(v -> { - query = ""; - metadata.clear(); + setQuery(""); + getMetadata().clear(); actionSearchView.setQuery("", false); - hideSearchSortBar(false); + hideSearchSubBar(); signalCurrentFragment(EV_SEARCH, ""); }); - // Sort controls - sortDirectionButton = findViewById(R.id.sort_direction_btn); - sortReshuffleButton = findViewById(R.id.sort_reshuffle_btn); - sortFieldButton = findViewById(R.id.sort_field_btn); - // Main tabs viewPager = findViewById(R.id.library_pager); viewPager.setUserInputEnabled(false); // Disable swipe to change tabs @@ -434,42 +402,42 @@ private void initUI() { @Override public void onPageSelected(int position) { enableCurrentFragment(); - hideSearchSortBar(false); + hideSearchSubBar(); updateToolbar(); updateSelectionToolbar(0, 0, 0, 0); } }); viewPager.setAdapter(pagerAdapter); - updateDisplay(); + updateDisplay(Preferences.getGroupingDisplay().getId()); } - private void updateDisplay() { + private void updateDisplay(int targetGroupingId) { pagerAdapter.notifyDataSetChanged(); - if (Preferences.getGroupingDisplay().equals(Grouping.FLAT)) { // Display books right away + if (targetGroupingId == Grouping.FLAT.getId()) { // Display books right away viewPager.setCurrentItem(1); +// viewModel.searchContent(); } enableCurrentFragment(); } private void initToolbar() { toolbar = findViewById(R.id.library_toolbar); - toolbar.setNavigationOnClickListener(v -> openNavigationDrawer()); searchMenu = toolbar.getMenu().findItem(R.id.action_search); searchMenu.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { @Override public boolean onMenuItemActionExpand(MenuItem item) { - showSearchSortBar(true, false, null); + showSearchSubBar(true, false); invalidateNextQueryTextChange = true; // Re-sets the query on screen, since default behaviour removes it right after collapse _and_ expand - if (!query.isEmpty()) + if (!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(Looper.getMainLooper()).postDelayed(() -> { invalidateNextQueryTextChange = true; - actionSearchView.setQuery(query, false); + actionSearchView.setQuery(getQuery(), false); }, 100); return true; @@ -478,20 +446,23 @@ public boolean onMenuItemActionExpand(MenuItem item) { @Override public boolean onMenuItemActionCollapse(MenuItem item) { if (!isSearchQueryActive()) { - hideSearchSortBar(false); + hideSearchSubBar(); } invalidateNextQueryTextChange = true; return true; } }); - completedFilterMenu = toolbar.getMenu().findItem(R.id.action_completed_filter); - favsMenu = toolbar.getMenu().findItem(R.id.action_favourites); - updateFavouriteFilter(); + displayTypeMenu = toolbar.getMenu().findItem(R.id.action_display_type); + if (Preferences.Constant.LIBRARY_DISPLAY_LIST == Preferences.getLibraryDisplay()) + displayTypeMenu.setIcon(R.drawable.ic_view_gallery); + else + displayTypeMenu.setIcon(R.drawable.ic_view_list); reorderMenu = toolbar.getMenu().findItem(R.id.action_edit); reorderCancelMenu = toolbar.getMenu().findItem(R.id.action_edit_cancel); + reorderConfirmMenu = toolbar.getMenu().findItem(R.id.action_edit_confirm); newGroupMenu = toolbar.getMenu().findItem(R.id.action_group_new); - sortMenu = toolbar.getMenu().findItem(R.id.action_order); + sortMenu = toolbar.getMenu().findItem(R.id.action_sort_filter); actionSearchView = (SearchView) searchMenu.getActionView(); actionSearchView.setIconifiedByDefault(true); @@ -500,8 +471,8 @@ public boolean onMenuItemActionCollapse(MenuItem item) { actionSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String s) { - query = s; - signalCurrentFragment(EV_SEARCH, query); + setQuery(s); + signalCurrentFragment(EV_SEARCH, getQuery()); actionSearchView.clearFocus(); return true; @@ -512,8 +483,8 @@ public boolean onQueryTextChange(String s) { if (invalidateNextQueryTextChange) { // Should not happen when search panel is closing or opening invalidateNextQueryTextChange = false; } else if (s.isEmpty()) { - query = ""; - signalCurrentFragment(EV_SEARCH, query); + setQuery(""); + signalCurrentFragment(EV_SEARCH, getQuery()); searchClearButton.setVisibility(View.GONE); } @@ -537,16 +508,19 @@ public void initFragmentToolbars( } } - public void sortCommandsAutoHide(boolean hideSortOnly, PopupMenu popup) { - this.autoHidePopup = popup; - sortCommandsAutoHide.submit(hideSortOnly); - } - public void updateSearchBarOnResults(boolean nonEmptyResults) { if (isSearchQueryActive()) { - showSearchSortBar(true, true, false); - if (nonEmptyResults) collapseSearchMenu(); + if (!getQuery().isEmpty()) { + actionSearchView.setQuery(getQuery(), false); + expandSearchMenu(); + } else if (nonEmptyResults) { + collapseSearchMenu(); + } + showSearchSubBar(!isGroupDisplayed(), true); } else { + collapseSearchMenu(); + if (actionSearchView.getQuery().length() > 0) + actionSearchView.setQuery("", false); searchClearButton.setVisibility(View.GONE); } } @@ -559,27 +533,26 @@ public void updateSearchBarOnResults(boolean nonEmptyResults) { */ public boolean toolbarOnItemClicked(@NonNull MenuItem menuItem) { switch (menuItem.getItemId()) { - case R.id.action_completed_filter: - if (!menuItem.isChecked()) - askFilterCompleted(); - else { - completedFilterMenu.setChecked(!completedFilterMenu.isChecked()); - updateCompletedFilter(); - viewModel.resetCompletedFilter(); - } + case R.id.action_display_type: + int displayType = Preferences.getLibraryDisplay(); + if (Preferences.Constant.LIBRARY_DISPLAY_LIST == displayType) + displayType = Preferences.Constant.LIBRARY_DISPLAY_GRID; + else displayType = Preferences.Constant.LIBRARY_DISPLAY_LIST; + Preferences.setLibraryDisplay(displayType); break; - case R.id.action_favourites: - menuItem.setChecked(!menuItem.isChecked()); - updateFavouriteFilter(); - if (isGroupDisplayed()) { - isGroupFavsChecked = menuItem.isChecked(); - viewModel.searchGroup(Preferences.getGroupingDisplay(), query, Preferences.getGroupSortField(), Preferences.isGroupSortDesc(), Preferences.getArtistGroupVisibility(), isGroupFavsChecked); - } else - viewModel.setContentFavouriteFilter(menuItem.isChecked()); + case R.id.action_browse_groups: + LibraryBottomGroupsFragment.invoke( + this, + this.getSupportFragmentManager() + ); break; - case R.id.action_order: - showSearchSortBar(null, null, true); - sortCommandsAutoHide.submit(true); + case R.id.action_sort_filter: + LibraryBottomSortFilterFragment.invoke( + this, + this.getSupportFragmentManager(), + isGroupDisplayed(), + group != null && group.grouping.equals(Grouping.CUSTOM) && 1 == group.getSubtype() + ); break; default: return false; @@ -587,59 +560,19 @@ public boolean toolbarOnItemClicked(@NonNull MenuItem menuItem) { return true; } - private void showSearchSortBar(Boolean showAdvancedSearch, Boolean showClear, Boolean showSort) { - if (showSort != null && showSort && View.VISIBLE == sortFieldButton.getVisibility()) { - hideSearchSortBar(View.GONE != advancedSearchButton.getVisibility()); - return; - } - - searchSortBar.setVisibility(View.VISIBLE); + private void showSearchSubBar(Boolean showAdvancedSearch, Boolean showClear) { + searchSubBar.setVisibility(View.VISIBLE); if (showAdvancedSearch != null) advancedSearchButton.setVisibility(showAdvancedSearch && !isGroupDisplayed() ? View.VISIBLE : View.GONE); if (showClear != null) searchClearButton.setVisibility(showClear ? View.VISIBLE : View.GONE); - - if (showSort != null) { - sortFieldButton.setVisibility(showSort ? View.VISIBLE : View.GONE); - if (showSort) { - boolean isRandom = (!isGroupDisplayed() && Preferences.Constant.ORDER_FIELD_RANDOM == Preferences.getContentSortField()); - sortDirectionButton.setVisibility(isRandom ? View.GONE : View.VISIBLE); - sortReshuffleButton.setVisibility(isRandom ? View.VISIBLE : View.GONE); - searchClearButton.setVisibility(View.GONE); - } else { - sortDirectionButton.setVisibility(View.GONE); - sortReshuffleButton.setVisibility(View.GONE); - } - } - - if (isGroupDisplayed() && Preferences.getGroupingDisplay().equals(Grouping.ARTIST)) { - showArtistsGroupsButton.setVisibility(View.VISIBLE); - } else { - showArtistsGroupsButton.setVisibility(View.GONE); - } } - public void hideSearchSortBar(boolean hideSortOnly) { - boolean isSearchVisible = (View.VISIBLE == advancedSearchButton.getVisibility() || View.VISIBLE == searchClearButton.getVisibility()); - - if (!hideSortOnly || !isSearchVisible) - searchSortBar.setVisibility(View.GONE); - - if (!hideSortOnly) { - advancedSearchButton.setVisibility(View.GONE); - searchClearButton.setVisibility(View.GONE); - } - - sortDirectionButton.setVisibility(View.GONE); - sortReshuffleButton.setVisibility(View.GONE); - sortFieldButton.setVisibility(View.GONE); - showArtistsGroupsButton.setVisibility(View.GONE); - - if (autoHidePopup != null) autoHidePopup.dismiss(); - - // Restore CLEAR button if it's needed - if (hideSortOnly && isSearchQueryActive()) searchClearButton.setVisibility(View.VISIBLE); + public void hideSearchSubBar() { + searchSubBar.setVisibility(View.GONE); + advancedSearchButton.setVisibility(View.GONE); + searchClearButton.setVisibility(View.GONE); } public boolean closeLeftDrawer() { @@ -658,6 +591,12 @@ public boolean collapseSearchMenu() { return false; } + public void expandSearchMenu() { + if (searchMenu != null && !searchMenu.isActionViewExpanded()) { + searchMenu.expandActionView(); + } + } + private void initSelectionToolbar() { selectionToolbar = findViewById(R.id.library_selection_toolbar); selectionToolbar.getMenu().clear(); @@ -679,63 +618,6 @@ private void initSelectionToolbar() { splitMenu = selectionToolbar.getMenu().findItem(R.id.action_split); } - private Grouping getGroupingFromMenuId(@IdRes int menuId) { - switch (menuId) { - case (R.id.groups_flat): - return Grouping.FLAT; - case (R.id.groups_by_artist): - return Grouping.ARTIST; - case (R.id.groups_by_dl_date): - return Grouping.DL_DATE; - case (R.id.groups_custom): - return Grouping.CUSTOM; - default: - return Grouping.NONE; - } - } - - private @IdRes - int getMenuIdFromGrouping(Grouping grouping) { - switch (grouping) { - case ARTIST: - return R.id.groups_by_artist; - case DL_DATE: - return R.id.groups_by_dl_date; - case CUSTOM: - return R.id.groups_custom; - case FLAT: - case NONE: - default: - return R.id.groups_flat; - } - } - - private int getVisibilityCodeFromMenuId(@IdRes int menuId) { - switch (menuId) { - case (R.id.show_artists): - return Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS; - case (R.id.show_groups): - return Preferences.Constant.ARTIST_GROUP_VISIBILITY_GROUPS; - case (R.id.show_artists_and_groups): - default: - return Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS_GROUPS; - } - } - - /** - * Update completed filter button appearance on the action bar - */ - private void updateCompletedFilter() { - completedFilterMenu.setIcon(completedFilterMenu.isChecked() ? R.drawable.ic_completed_filter_on : R.drawable.ic_completed_filter_off); - } - - /** - * Update favourite filter button appearance on the action bar - */ - private void updateFavouriteFilter() { - favsMenu.setIcon(favsMenu.isChecked() ? R.drawable.ic_filter_favs_on : R.drawable.ic_filter_favs_off); - } - /** * Callback for any change in Preferences */ @@ -753,13 +635,8 @@ private void onSharedPreferenceChanged(String key) { break; case Preferences.Key.SD_STORAGE_URI: case Preferences.Key.EXTERNAL_LIBRARY_URI: - Preferences.setGroupingDisplay(Grouping.FLAT.getId()); - viewModel.setGroup(null, true); - updateDisplay(); - break; - case Preferences.Key.GROUPING_DISPLAY: - case Preferences.Key.ARTIST_GROUP_VISIBILITY: - viewModel.setGrouping(Preferences.getGroupingDisplay(), Preferences.getGroupSortField(), Preferences.isGroupSortDesc(), Preferences.getArtistGroupVisibility(), isGroupFavsChecked); + updateDisplay(Grouping.FLAT.getId()); + viewModel.setGrouping(Grouping.FLAT.getId()); break; default: // Nothing to handle there @@ -773,94 +650,33 @@ private void onAdvancedSearchButtonClick() { signalCurrentFragment(EV_ADVANCED_SEARCH, null); } - /** - * Handler for the "Group by" button - */ - private void onGroupByButtonClick(View groupByButton) { - // Load and display the field popup menu - PopupMenu popup = new PopupMenu(this, groupByButton); - popup.getMenuInflater() - .inflate(R.menu.library_groups_popup, popup.getMenu()); - - popup.getMenu().findItem(R.id.groups_custom).setVisible(isCustomGroupingAvailable); - - // Mark current grouping - MenuItem currentItem = popup.getMenu().findItem(getMenuIdFromGrouping(Preferences.getGroupingDisplay())); - currentItem.setTitle(currentItem.getTitle() + " <"); - - popup.setOnMenuItemClickListener(item -> { - Grouping currentGrouping = Preferences.getGroupingDisplay(); - Grouping selectedGrouping = getGroupingFromMenuId(item.getItemId()); - // Don't do anything if the current group is selected - if (currentGrouping.equals(selectedGrouping)) return false; - - Preferences.setGroupingDisplay(selectedGrouping.getId()); - isGroupFavsChecked = false; - favsMenu.setChecked(false); - updateFavouriteFilter(); - - if (isGroupDisplayed() && selectedGrouping.equals(Grouping.ARTIST)) { - showArtistsGroupsButton.setVisibility(View.VISIBLE); - } else { - showArtistsGroupsButton.setVisibility(View.GONE); - } - + private void onGroupingChanged(int targetGroupingId) { + Grouping targetGrouping = Grouping.searchById(targetGroupingId); + if (grouping.getId() != targetGroupingId) { // Reset custom book ordering if reverting to a grouping where that doesn't apply - if (!selectedGrouping.canReorderBooks() + if (!targetGrouping.canReorderBooks() && Preferences.Constant.ORDER_FIELD_CUSTOM == Preferences.getContentSortField()) { Preferences.setContentSortField(Preferences.Default.ORDER_CONTENT_FIELD); } // Reset custom group ordering if reverting to a grouping where that doesn't apply - if (!selectedGrouping.canReorderGroups() + if (!targetGrouping.canReorderGroups() && Preferences.Constant.ORDER_FIELD_CUSTOM == Preferences.getGroupSortField()) { Preferences.setGroupSortField(Preferences.Default.ORDER_GROUP_FIELD); } // Go back to groups tab if we're not - goBackToGroups(); + if (targetGroupingId != Grouping.FLAT.getId()) + goBackToGroups(); // Update screen display if needed (flat <-> the rest) - if (currentGrouping.equals(Grouping.FLAT) || selectedGrouping.equals(Grouping.FLAT)) - updateDisplay(); - sortCommandsAutoHide(true, popup); - return true; - }); - popup.show(); //showing popup menu - sortCommandsAutoHide(true, popup); - } + if (grouping.equals(Grouping.FLAT) || targetGroupingId == Grouping.FLAT.getId()) + updateDisplay(targetGroupingId); - private String getArtistsGroupsTextFromPrefs() { - switch (Preferences.getArtistGroupVisibility()) { - case Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS: - return getResources().getString(R.string.show_artists); - case Preferences.Constant.ARTIST_GROUP_VISIBILITY_GROUPS: - return getResources().getString(R.string.show_groups); - case Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS_GROUPS: - return getResources().getString(R.string.show_artists_and_groups); - default: - return ""; + grouping = targetGrouping; + updateToolbar(); } } - /** - * Handler for the "Show artists/groups" button - */ - private void onGroupsVisibilityButtonClick(View groupsVisibilityButton) { - // Load and display the visibility popup menu - PopupMenu popup = new PopupMenu(this, groupsVisibilityButton); - popup.getMenuInflater().inflate(R.menu.library_groups_visibility_popup, popup.getMenu()); - popup.setOnMenuItemClickListener(item -> { - item.setChecked(true); - int code = getVisibilityCodeFromMenuId(item.getItemId()); - Preferences.setArtistGroupVisibility(code); - showArtistsGroupsButton.setText(getArtistsGroupsTextFromPrefs()); - sortCommandsAutoHide(true, popup); - return true; - }); - popup.show(); - sortCommandsAutoHide(true, popup); - } - /** * Update the screen title according to current search filter (#TOTAL BOOKS) if no filter is * enabled (#FILTERED / #TOTAL BOOKS) if a filter is enabled @@ -882,7 +698,7 @@ public void updateTitle(long totalSelectedCount, long totalCount) { * @return True if a search query is active (using universal search or advanced search); false if not (=whole unfiltered library selected) */ public boolean isSearchQueryActive() { - return (!query.isEmpty() || !metadata.isEmpty()); + return (!getQuery().isEmpty() || !getMetadata().isEmpty()); } private void fixPermissions() { @@ -927,32 +743,62 @@ public void goBackToGroups() { if (isGroupDisplayed()) return; enableFragment(0); - viewModel.searchGroup(Preferences.getGroupingDisplay(), query, Preferences.getGroupSortField(), Preferences.isGroupSortDesc(), Preferences.getArtistGroupVisibility(), isGroupFavsChecked); + viewModel.searchGroup(); viewPager.setCurrentItem(0); if (titles.containsKey(0)) toolbar.setTitle(titles.get(0)); + //toolbar.setNavigationIcon(R.drawable.ic_drawer); } - public void showBooksInGroup(me.devsaki.hentoid.database.domains.Group group) { + public void showBooksInGroup(Group group) { enableFragment(1); viewModel.setGroup(group, true); viewPager.setCurrentItem(1); + //toolbar.setNavigationIcon(R.drawable.ic_arrow_back); + } + + public boolean isFilterActive() { + if (isSearchQueryActive()) { + setQuery(""); + setMetadata(Collections.emptyList()); + collapseSearchMenu(); + hideSearchSubBar(); + } + if (isGroupDisplayed()) { + GroupSearchManager.GroupSearchBundle bundle = new GroupSearchManager.GroupSearchBundle(groupSearchBundle); + return bundle.isFilterActive(); + } else { + ContentSearchManager.ContentSearchBundle bundle = new ContentSearchManager.ContentSearchBundle(contentSearchBundle); + return bundle.isFilterActive(); + } } private void updateToolbar() { Grouping currentGrouping = Preferences.getGroupingDisplay(); + displayTypeMenu.setVisible(!editMode); searchMenu.setVisible(!editMode); newGroupMenu.setVisible(!editMode && isGroupDisplayed() && currentGrouping.canReorderGroups()); // Custom groups only - favsMenu.setVisible(!editMode); - reorderMenu.setIcon(editMode ? R.drawable.ic_check : R.drawable.ic_reorder_lines); + reorderConfirmMenu.setVisible(editMode); reorderCancelMenu.setVisible(editMode); sortMenu.setVisible(!editMode); - completedFilterMenu.setVisible(!editMode && !isGroupDisplayed()); - if (isGroupDisplayed()) reorderMenu.setVisible(currentGrouping.canReorderGroups()); - else reorderMenu.setVisible(currentGrouping.canReorderBooks()); + boolean isToolbarNavigationDrawer = true; + if (isGroupDisplayed()) { + reorderMenu.setVisible(currentGrouping.canReorderGroups()); + } else { + reorderMenu.setVisible(currentGrouping.canReorderBooks() && group != null && group.getSubtype() != 1); + isToolbarNavigationDrawer = currentGrouping.equals(Grouping.FLAT); + } + + if (isToolbarNavigationDrawer) { // Open the left drawer + toolbar.setNavigationIcon(R.drawable.ic_drawer); + toolbar.setNavigationOnClickListener(v -> openNavigationDrawer()); + } else { // Go back to groups + toolbar.setNavigationIcon(R.drawable.ic_arrow_back); + toolbar.setNavigationOnClickListener(v -> goBackToGroups()); + } - signalCurrentFragment(EV_UPDATE_SORT, null); + signalCurrentFragment(EV_UPDATE_TOOLBAR, null); } public void updateSelectionToolbar( @@ -1001,26 +847,6 @@ public void updateSelectionToolbar( } } - public void askFilterCompleted() { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - String title = getString(R.string.ask_filter_completed); - builder.setMessage(title) - .setPositiveButton(R.string.filter_not_completed, - (dialog, which) -> { - completedFilterMenu.setChecked(!completedFilterMenu.isChecked()); - updateCompletedFilter(); - viewModel.toggleNotCompletedFilter(); - }) - .setNegativeButton(R.string.filter_completed, - (dialog, which) -> { - completedFilterMenu.setChecked(!completedFilterMenu.isChecked()); - updateCompletedFilter(); - viewModel.toggleCompletedFilter(); - }) - .create().show(); - } - - /** * Display the yes/no dialog to make sure the user really wants to delete selected items * @@ -1028,7 +854,7 @@ public void askFilterCompleted() { */ public void askDeleteItems( @NonNull final List contents, - @NonNull final List groups, + @NonNull final List groups, @Nullable final Runnable onSuccess, @NonNull final SelectExtension selectExtension) { // TODO display the number of books and groups that will be deleted @@ -1117,7 +943,7 @@ private void onContentArchiveError(Throwable e) { } private void signalCurrentFragment(int eventType, @Nullable String message) { - signalFragment(isGroupDisplayed() ? 0 : 1, eventType, message); + signalFragment(getCurrentFragmentIndex(), eventType, message); } private void signalFragment(int fragmentIndex, int eventType, @Nullable String message) { @@ -1125,8 +951,11 @@ private void signalFragment(int fragmentIndex, int eventType, @Nullable String m } private void enableCurrentFragment() { - if (isGroupDisplayed()) enableFragment(0); - else enableFragment(1); + enableFragment(getCurrentFragmentIndex()); + } + + private int getCurrentFragmentIndex() { + return isGroupDisplayed() ? 0 : 1; } private void enableFragment(int fragmentIndex) { @@ -1134,6 +963,40 @@ private void enableFragment(int fragmentIndex) { EventBus.getDefault().post(new CommunicationEvent(EV_DISABLE, (0 == fragmentIndex) ? RC_CONTENTS : RC_GROUPS, null)); } + public static @StringRes + int getNameFromFieldCode(int prefFieldCode) { + switch (prefFieldCode) { + case (Preferences.Constant.ORDER_FIELD_TITLE): + return R.string.sort_title; + case (Preferences.Constant.ORDER_FIELD_ARTIST): + return R.string.sort_artist; + case (Preferences.Constant.ORDER_FIELD_NB_PAGES): + return R.string.sort_pages; + case (Preferences.Constant.ORDER_FIELD_DOWNLOAD_PROCESSING_DATE): + return R.string.sort_dl_date; + case (Preferences.Constant.ORDER_FIELD_DOWNLOAD_COMPLETION_DATE): + return R.string.sort_dl_completion_date; + case (Preferences.Constant.ORDER_FIELD_UPLOAD_DATE): + return R.string.sort_uplodad_date; + case (Preferences.Constant.ORDER_FIELD_READ_DATE): + return R.string.sort_read_date; + case (Preferences.Constant.ORDER_FIELD_READS): + return R.string.sort_reads; + case (Preferences.Constant.ORDER_FIELD_SIZE): + return R.string.sort_size; + case (Preferences.Constant.ORDER_FIELD_READ_PROGRESS): + return R.string.sort_reading_progress; + case (Preferences.Constant.ORDER_FIELD_CUSTOM): + return R.string.sort_custom; + case (Preferences.Constant.ORDER_FIELD_RANDOM): + return R.string.sort_random; + case (Preferences.Constant.ORDER_FIELD_CHILDREN): + return R.string.sort_books; + default: + return R.string.sort_invalid; + } + } + /** * ============================== SUBCLASS */ 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 407e81b8e6..7d4257856b 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.java @@ -163,15 +163,16 @@ protected void onNewIntent(Intent intent) { } private void processIntent(@NonNull Bundle extras) { - QueueActivityBundle.Parser parser = new QueueActivityBundle.Parser(extras); - long contentHash = parser.contentHash(); + QueueActivityBundle parser = new QueueActivityBundle(extras); + long contentHash = parser.getContentHash(); if (contentHash != 0) { if (parser.isErrorsTab()) viewPager.setCurrentItem(1); viewModel.setContentToShowFirst(contentHash); } - Site revivedSite = parser.getRevivedSite(); - String oldCookie = parser.getOldCookie(); - if (revivedSite != null && !oldCookie.isEmpty()) reviveDownload(revivedSite, oldCookie); + Site revivedSite = Site.searchByCode(parser.getReviveDownloadForSiteCode()); + String oldCookie = parser.getReviveOldCookie(); + if (!revivedSite.equals(Site.NONE) && !oldCookie.isEmpty()) + reviveDownload(revivedSite, oldCookie); } @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 a6409c4d9a..839b51e967 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/SearchActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/SearchActivity.java @@ -1,5 +1,7 @@ package me.devsaki.hentoid.activities; +import static java.lang.String.format; + import android.app.Activity; import android.content.Intent; import android.net.Uri; @@ -28,8 +30,6 @@ import me.devsaki.hentoid.viewmodels.ViewModelFactory; import timber.log.Timber; -import static java.lang.String.format; - /** * Activity for the advanced search screen */ @@ -60,8 +60,8 @@ public class SearchActivity extends BaseActivity { protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - SearchActivityBundle.Builder builder = new SearchActivityBundle.Builder(); - builder.setUri(SearchActivityBundle.Builder.buildSearchUri(viewModel.getSelectedAttributesData().getValue())); + SearchActivityBundle builder = new SearchActivityBundle(); + builder.setUri(SearchActivityBundle.Companion.buildSearchUri(viewModel.getSelectedAttributesData().getValue()).toString()); outState.putAll(builder.getBundle()); outState.putBoolean("exclude", excludeClicked); } @@ -70,9 +70,9 @@ protected void onSaveInstanceState(@NonNull Bundle outState) { protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); excludeClicked = savedInstanceState.getBoolean("exclude"); - Uri searchUri = new SearchActivityBundle.Parser(savedInstanceState).getUri(); + Uri searchUri = Uri.parse(new SearchActivityBundle(savedInstanceState).getUri()); if (searchUri != null) { - List preSelectedAttributes = SearchActivityBundle.Parser.parseSearchUri(searchUri); + List preSelectedAttributes = SearchActivityBundle.Companion.parseSearchUri(searchUri); viewModel.setSelectedAttributes(preSelectedAttributes); } } @@ -84,11 +84,11 @@ protected void onCreate(Bundle savedInstanceState) { Intent intent = getIntent(); List preSelectedAttributes = null; if (intent != null && intent.getExtras() != null) { - SearchActivityBundle.Parser parser = new SearchActivityBundle.Parser(intent.getExtras()); - Uri searchUri = parser.getUri(); + SearchActivityBundle parser = new SearchActivityBundle(intent.getExtras()); + Uri searchUri = Uri.parse(parser.getUri()); excludeClicked = parser.getExcludeMode(); if (searchUri != null) - preSelectedAttributes = SearchActivityBundle.Parser.parseSearchUri(searchUri); + preSelectedAttributes = SearchActivityBundle.Companion.parseSearchUri(searchUri); } setContentView(R.layout.activity_search); @@ -105,7 +105,7 @@ protected void onCreate(Bundle savedInstanceState) { anyTypeButton.setEnabled(true); tagTypeButton = findViewById(R.id.textCategoryTag); - tagTypeButton.setOnClickListener(v -> onAttrButtonClick(excludeClicked,AttributeType.TAG)); + tagTypeButton.setOnClickListener(v -> onAttrButtonClick(excludeClicked, AttributeType.TAG)); artistTypeButton = findViewById(R.id.textCategoryArtist); @@ -168,8 +168,8 @@ private void onQueryUpdated(@NonNull final SparseIntArray attrCount) { updateAttributeTypeButton(sourceTypeButton, attrCount, AttributeType.SOURCE); } - public void onExcludeClick(View view){ - excludeClicked = ((CheckBox) view).isChecked(); + public void onExcludeClick(View view) { + excludeClicked = ((CheckBox) view).isChecked(); } /** @@ -246,10 +246,11 @@ private void onBooksCounted(int count) { * Transmit the search query to the library screen and close the advanced search screen */ private void searchBooks() { - Uri searchUri = SearchActivityBundle.Builder.buildSearchUri(viewModel.getSelectedAttributesData().getValue()); + Uri searchUri = SearchActivityBundle.Companion.buildSearchUri(viewModel.getSelectedAttributesData().getValue()); Timber.d("URI :%s", searchUri); - SearchActivityBundle.Builder builder = new SearchActivityBundle.Builder().setUri(searchUri); + SearchActivityBundle builder = new SearchActivityBundle(); + builder.setUri(searchUri.toString()); builder.setExcludeMode(excludeClicked); Intent returnIntent = new Intent(); diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/BaseWebActivityBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/BaseWebActivityBundle.kt index c804735a0e..cee5b723e5 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/BaseWebActivityBundle.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/BaseWebActivityBundle.kt @@ -7,11 +7,7 @@ import me.devsaki.hentoid.util.string * Helper class to transfer data from any Activity to {@link me.devsaki.hentoid.activities.PrefsActivity} * through a Bundle */ -class BaseWebActivityBundle(private val bundle: Bundle) { - - constructor() : this(Bundle()) +class BaseWebActivityBundle(val bundle: Bundle = Bundle()) { var url by bundle.string(default = "") - - fun toBundle() = bundle } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ContentItemBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ContentItemBundle.java deleted file mode 100644 index 3d060d5c01..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ContentItemBundle.java +++ /dev/null @@ -1,120 +0,0 @@ -package me.devsaki.hentoid.activities.bundles; - -import android.os.Bundle; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Helper class to transfer payload data to {@link me.devsaki.hentoid.viewholders.ContentItem} - * through a Bundle - *

- * Use Builder class to set data; use Parser class to get data - */ -public class ContentItemBundle { - private static final String KEY_DELETE_PROCESSING = "is_being_deleted"; - private static final String KEY_FAV_STATE = "favourite"; - private static final String KEY_READS = "reads"; - private static final String KEY_READ_COUNT = "read_count"; - private static final String KEY_COVER_URI = "cover_uri"; - private static final String KEY_COMPL_STATE = "completed"; - private static final String KEY_TITLE = "title"; - - private ContentItemBundle() { - throw new UnsupportedOperationException(); - } - - public static final class Builder { - - private final Bundle bundle = new Bundle(); - - public void setIsBeingDeleted(boolean isBeingDeleted) { - bundle.putBoolean(KEY_DELETE_PROCESSING, isBeingDeleted); - } - - public void setIsFavourite(boolean isFavourite) { - bundle.putBoolean(KEY_FAV_STATE, isFavourite); - } - - public void setIsCompleted(boolean isCompleted) { - bundle.putBoolean(KEY_COMPL_STATE, isCompleted); - } - - public void setReads(long reads) { - bundle.putLong(KEY_READS, reads); - } - - public void setReadPagesCount(long readPagesCount) { - bundle.putLong(KEY_READ_COUNT, readPagesCount); - } - - public void setCoverUri(String uri) { - bundle.putString(KEY_COVER_URI, uri); - } - - public void setTitle(String value) { - bundle.putString(KEY_TITLE, value); - } - - public boolean isEmpty() { - return bundle.isEmpty(); - } - - public Bundle getBundle() { - return bundle; - } - - - } - - public static final class Parser { - - private final Bundle bundle; - - public Parser(@Nonnull Bundle bundle) { - this.bundle = bundle; - } - - @Nullable - public Boolean isBeingDeleted() { - if (bundle.containsKey(KEY_DELETE_PROCESSING)) - return bundle.getBoolean(KEY_DELETE_PROCESSING); - else return null; - } - - @Nullable - public Boolean isFavourite() { - if (bundle.containsKey(KEY_FAV_STATE)) return bundle.getBoolean(KEY_FAV_STATE); - else return null; - } - - public Boolean isCompleted() { - if (bundle.containsKey(KEY_COMPL_STATE)) return bundle.getBoolean(KEY_COMPL_STATE); - else return null; - } - - @Nullable - public Long getReads() { - if (bundle.containsKey(KEY_READS)) return bundle.getLong(KEY_READS); - else return null; - } - - @Nullable - public Long getReadPagesCount() { - if (bundle.containsKey(KEY_READ_COUNT)) return bundle.getLong(KEY_READ_COUNT); - else return null; - } - - @Nullable - public String getCoverUri() { - if (bundle.containsKey(KEY_COVER_URI)) return bundle.getString(KEY_COVER_URI); - else return null; - } - - @Nullable - public String getTitle() { - if (bundle.containsKey(KEY_TITLE)) return bundle.getString(KEY_TITLE); - else return null; - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ContentItemBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ContentItemBundle.kt new file mode 100644 index 0000000000..0786a8bb50 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ContentItemBundle.kt @@ -0,0 +1,30 @@ +package me.devsaki.hentoid.activities.bundles + +import android.os.Bundle +import me.devsaki.hentoid.util.boolean +import me.devsaki.hentoid.util.int +import me.devsaki.hentoid.util.long +import me.devsaki.hentoid.util.string + +/** + * Helper class to transfer payload data to [me.devsaki.hentoid.viewholders.ContentItem] + * through a Bundle + */ +class ContentItemBundle(val bundle: Bundle = Bundle()) { + + var isBeingDeleted by bundle.boolean() + + var isFavourite by bundle.boolean() + + var isCompleted by bundle.boolean() + + var reads by bundle.long() + + var readPagesCount by bundle.int() + + var coverUri by bundle.string() + + var title by bundle.string() + + val isEmpty get() = bundle.isEmpty +} \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/DuplicateItemBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/DuplicateItemBundle.java deleted file mode 100644 index 3719e7383f..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/DuplicateItemBundle.java +++ /dev/null @@ -1,66 +0,0 @@ -package me.devsaki.hentoid.activities.bundles; - -import android.os.Bundle; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Helper class to transfer payload data to {@link me.devsaki.hentoid.viewholders.DuplicateItem} - * through a Bundle - *

- * Use Builder class to set data; use Parser class to get data - */ -public class DuplicateItemBundle { - private static final String KEY_KEEP = "keep"; - private static final String KEY_IS_BEING_DELETED = "isBeingDeleted"; - - private DuplicateItemBundle() { - throw new UnsupportedOperationException(); - } - - public static final class Builder { - - private final Bundle bundle = new Bundle(); - - public void setKeep(Boolean value) { - if (value != null) - bundle.putBoolean(KEY_KEEP, value); - } - - public void setIsBeingDeleted(Boolean value) { - if (value != null) - bundle.putBoolean(KEY_IS_BEING_DELETED, value); - } - - public boolean isEmpty() { - return bundle.isEmpty(); - } - - public Bundle getBundle() { - return bundle; - } - } - - public static final class Parser { - - private final Bundle bundle; - - public Parser(@Nonnull Bundle bundle) { - this.bundle = bundle; - } - - @Nullable - public Boolean getKeep() { - if (bundle.containsKey(KEY_KEEP)) return bundle.getBoolean(KEY_KEEP); - else return null; - } - - @Nullable - public Boolean isBeingDeleted() { - if (bundle.containsKey(KEY_IS_BEING_DELETED)) - return bundle.getBoolean(KEY_IS_BEING_DELETED); - else return null; - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/DuplicateItemBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/DuplicateItemBundle.kt new file mode 100644 index 0000000000..2c5b84f1f0 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/DuplicateItemBundle.kt @@ -0,0 +1,17 @@ +package me.devsaki.hentoid.activities.bundles + +import android.os.Bundle +import me.devsaki.hentoid.util.boolean + +/** + * Helper class to transfer payload data to [me.devsaki.hentoid.viewholders.DuplicateItem] + * through a Bundle + */ +class DuplicateItemBundle(val bundle: Bundle = Bundle()) { + + var isKeep by bundle.boolean() + + var isBeingDeleted by bundle.boolean() + + val isEmpty get() = bundle.isEmpty +} \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/GroupItemBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/GroupItemBundle.java deleted file mode 100644 index 20c0cba3ef..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/GroupItemBundle.java +++ /dev/null @@ -1,63 +0,0 @@ -package me.devsaki.hentoid.activities.bundles; - -import android.os.Bundle; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Helper class to transfer payload data to {@link me.devsaki.hentoid.viewholders.GroupDisplayItem} - * through a Bundle - *

- * Use Builder class to set data; use Parser class to get data - */ -public class GroupItemBundle { - private static final String KEY_PICTURE = "picture"; - private static final String KEY_FAV = "favourite"; - - private GroupItemBundle() { - throw new UnsupportedOperationException(); - } - - public static final class Builder { - - private final Bundle bundle = new Bundle(); - - public void setCoverUri(String uri) { - bundle.putString(KEY_PICTURE, uri); - } - - public void setFavourite(boolean isFavourite) { - bundle.putBoolean(KEY_FAV, isFavourite); - } - - public boolean isEmpty() { - return bundle.isEmpty(); - } - - public Bundle getBundle() { - return bundle; - } - } - - public static final class Parser { - - private final Bundle bundle; - - public Parser(@Nonnull Bundle bundle) { - this.bundle = bundle; - } - - @Nullable - public String getCoverUri() { - if (bundle.containsKey(KEY_PICTURE)) return bundle.getString(KEY_PICTURE); - else return null; - } - - @Nullable - public Boolean isFavourite() { - if (bundle.containsKey(KEY_FAV)) return bundle.getBoolean(KEY_FAV); - else return null; - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/GroupItemBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/GroupItemBundle.kt new file mode 100644 index 0000000000..458882343a --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/GroupItemBundle.kt @@ -0,0 +1,18 @@ +package me.devsaki.hentoid.activities.bundles + +import android.os.Bundle +import me.devsaki.hentoid.util.boolean +import me.devsaki.hentoid.util.string + +/** + * Helper class to transfer payload data to [me.devsaki.hentoid.viewholders.GroupDisplayItem] + * through a Bundle + */ +class GroupItemBundle(val bundle: Bundle = Bundle()) { + + var coverUri by bundle.string() + + var isFavourite by bundle.boolean() + + val isEmpty get() = bundle.isEmpty +} \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageItemBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageItemBundle.java deleted file mode 100644 index bd0aa9e3be..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageItemBundle.java +++ /dev/null @@ -1,63 +0,0 @@ -package me.devsaki.hentoid.activities.bundles; - -import android.os.Bundle; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Helper class to transfer payload data to {@link me.devsaki.hentoid.viewholders.ImageFileItem} - * through a Bundle - *

- * Use Builder class to set data; use Parser class to get data - */ -public class ImageItemBundle { - private static final String KEY_FAV_STATE = "favourite"; - private static final String KEY_CHP_ORDER = "chapterOrder"; - - private ImageItemBundle() { - throw new UnsupportedOperationException(); - } - - public static final class Builder { - - private final Bundle bundle = new Bundle(); - - public void setIsFavourite(boolean value) { - bundle.putBoolean(KEY_FAV_STATE, value); - } - - public void setChapterOrder(int value) { - bundle.putInt(KEY_CHP_ORDER, value); - } - - public boolean isEmpty() { - return bundle.isEmpty(); - } - - public Bundle getBundle() { - return bundle; - } - } - - public static final class Parser { - - private final Bundle bundle; - - public Parser(@Nonnull Bundle bundle) { - this.bundle = bundle; - } - - @Nullable - public Boolean isFavourite() { - if (bundle.containsKey(KEY_FAV_STATE)) return bundle.getBoolean(KEY_FAV_STATE); - else return null; - } - - @Nullable - public Integer getChapterOrder() { - if (bundle.containsKey(KEY_CHP_ORDER)) return bundle.getInt(KEY_CHP_ORDER); - else return null; - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageItemBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageItemBundle.kt new file mode 100644 index 0000000000..016c967b90 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageItemBundle.kt @@ -0,0 +1,18 @@ +package me.devsaki.hentoid.activities.bundles + +import android.os.Bundle +import me.devsaki.hentoid.util.boolean +import me.devsaki.hentoid.util.int + +/** + * Helper class to transfer payload data to [me.devsaki.hentoid.viewholders.ImageFileItem] + * through a Bundle + */ +class ImageItemBundle(val bundle: Bundle = Bundle()) { + + var isFavourite by bundle.boolean() + + var chapterOrder by bundle.int() + + val isEmpty get() = bundle.isEmpty +} \ No newline at end of file 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 deleted file mode 100644 index 7f83628237..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.java +++ /dev/null @@ -1,90 +0,0 @@ -package me.devsaki.hentoid.activities.bundles; - -import android.os.Bundle; - -import javax.annotation.Nonnull; - -/** - * Helper class to transfer data from any Activity to {@link me.devsaki.hentoid.activities.ImageViewerActivity} - * through a Bundle - *

- * Use Builder class to set data; use Parser class to get data - */ -public class ImageViewerActivityBundle { - private static final String KEY_CONTENT_ID = "contentId"; - private static final String KEY_SEARCH_PARAMS = "searchParams"; - private static final String KEY_IMAGE_INDEX = "imageIndex"; - private static final String KEY_IMAGE_NUMBER = "imageNumber"; - private static final String KEY_SCALE = "scale"; - private static final String KEY_FORCE_SHOW_GALLERY = "forceShowGallery"; - - private ImageViewerActivityBundle() { - throw new UnsupportedOperationException(); - } - - public static final class Builder { - - private final Bundle bundle = new Bundle(); - - public void setContentId(long contentId) { - bundle.putLong(KEY_CONTENT_ID, contentId); - } - - public void setSearchParams(Bundle params) { - bundle.putBundle(KEY_SEARCH_PARAMS, params); - } - - public void setImageIndex(int imageIndex) { - bundle.putInt(KEY_IMAGE_INDEX, imageIndex); - } - - public void setPageNumber(int imageNumber) { - bundle.putInt(KEY_IMAGE_NUMBER, imageNumber); - } - - public void setScale(float scale) { - bundle.putFloat(KEY_SCALE, scale); - } - - public void setForceShowGallery(boolean value) { - bundle.putBoolean(KEY_FORCE_SHOW_GALLERY, value); - } - - public Bundle getBundle() { - return bundle; - } - } - - public static final class Parser { - - private final Bundle bundle; - - public Parser(@Nonnull Bundle bundle) { - this.bundle = bundle; - } - - public long getContentId() { - return bundle.getLong(KEY_CONTENT_ID, 0); - } - - public Bundle getSearchParams() { - return bundle.getBundle(KEY_SEARCH_PARAMS); - } - - public int getImageIndex() { - return bundle.getInt(KEY_IMAGE_INDEX, -1); - } - - public int getPageNumber() { - return bundle.getInt(KEY_IMAGE_NUMBER, -1); - } - - public float getScale() { - return bundle.getFloat(KEY_SCALE, -1); - } - - public boolean isForceShowGallery() { - return bundle.getBoolean(KEY_FORCE_SHOW_GALLERY, false); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.kt new file mode 100644 index 0000000000..3a8ec38826 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/ImageViewerActivityBundle.kt @@ -0,0 +1,23 @@ +package me.devsaki.hentoid.activities.bundles + +import android.os.Bundle +import me.devsaki.hentoid.util.* + +/** + * Helper class to transfer data from any Activity to [me.devsaki.hentoid.activities.ImageViewerActivity] + * through a Bundle + */ +class ImageViewerActivityBundle(val bundle: Bundle = Bundle()) { + + var contentId by bundle.long(default = 0) + + var searchParams by bundle.bundle() + + var imageIndex by bundle.int(default = -1) + + var pageNumber by bundle.int(default = -1) + + var scale by bundle.float(default = -1f) + + var isForceShowGallery by bundle.boolean(default = false) +} \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/LibraryBottomSortFilterBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/LibraryBottomSortFilterBundle.kt new file mode 100644 index 0000000000..0de96a3234 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/LibraryBottomSortFilterBundle.kt @@ -0,0 +1,18 @@ +package me.devsaki.hentoid.activities.bundles + +import android.os.Bundle +import me.devsaki.hentoid.util.boolean +import me.devsaki.hentoid.util.int + +/** + * Helper class to transfer data from any Activity to [me.devsaki.hentoid.fragments.library.LibraryBottomSortFilterFragment] + * through a Bundle + */ +class LibraryBottomSortFilterBundle(val bundle: Bundle = Bundle()) { + + var isGroupsDisplayed by bundle.boolean(default = false) + + var isUngroupedGroupDisplayed by bundle.boolean(default = false) + + var showTabIndex by bundle.int(default = 0) +} \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/PrefsBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/PrefsBundle.kt index 88660f0cfd..c535f05cb2 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/PrefsBundle.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/PrefsBundle.kt @@ -4,12 +4,10 @@ import android.os.Bundle import me.devsaki.hentoid.util.boolean /** - * Helper class to transfer data from any Activity to {@link me.devsaki.hentoid.activities.PrefsActivity} + * Helper class to transfer data from any Activity to [me.devsaki.hentoid.activities.PrefsActivity] * through a Bundle */ -class PrefsBundle(private val bundle: Bundle) { - - constructor() : this(Bundle()) +class PrefsBundle(val bundle: Bundle = Bundle()) { var isViewerPrefs by bundle.boolean(default = false) @@ -18,6 +16,4 @@ class PrefsBundle(private val bundle: Bundle) { var isDownloaderPrefs by bundle.boolean(default = false) var isStoragePrefs by bundle.boolean(default = false) - - fun toBundle() = bundle } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/QueueActivityBundle.java b/app/src/main/java/me/devsaki/hentoid/activities/bundles/QueueActivityBundle.java deleted file mode 100644 index b382a1aa2a..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/QueueActivityBundle.java +++ /dev/null @@ -1,78 +0,0 @@ -package me.devsaki.hentoid.activities.bundles; - -import android.os.Bundle; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import me.devsaki.hentoid.enums.Site; - -/** - * Helper class to transfer data from any Activity to {@link me.devsaki.hentoid.activities.QueueActivity} - * through a Bundle - *

- * Use Builder class to set data; use Parser class to get data - */ -public class QueueActivityBundle { - private static final String KEY_IS_ERROR = "isError"; - private static final String KEY_CONTENT_HASH = "contentHash"; - private static final String KEY_REVIVE_DOWNLOAD = "reviveDownload"; - private static final String KEY_REVIVE_OLD_COOKIE = "reviveOldCookie"; - - private QueueActivityBundle() { - throw new UnsupportedOperationException(); - } - - public static final class Builder { - - private final Bundle bundle = new Bundle(); - - public void setIsErrorsTab(boolean isError) { - bundle.putBoolean(KEY_IS_ERROR, isError); - } - - public void setContentHash(long contentHash) { - bundle.putLong(KEY_CONTENT_HASH, contentHash); - } - - public void setReviveDownload(Site site) { - bundle.putInt(KEY_REVIVE_DOWNLOAD, site.getCode()); - } - - public void setReviveOldCookie(String cookie) { - bundle.putString(KEY_REVIVE_OLD_COOKIE, cookie); - } - - public Bundle getBundle() { - return bundle; - } - } - - public static final class Parser { - - private final Bundle bundle; - - public Parser(@Nonnull Bundle bundle) { - this.bundle = bundle; - } - - public boolean isErrorsTab() { - return bundle.getBoolean(KEY_IS_ERROR, false); - } - - public long contentHash() { - return bundle.getLong(KEY_CONTENT_HASH, 0); - } - - @Nullable - public Site getRevivedSite() { - int siteId = bundle.getInt(KEY_REVIVE_DOWNLOAD, -1); - if (siteId > -1) return Site.searchByCode(siteId); - else return null; - } - - public String getOldCookie() { - return bundle.getString(KEY_REVIVE_OLD_COOKIE, ""); - } - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/QueueActivityBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/QueueActivityBundle.kt new file mode 100644 index 0000000000..40a45c9d37 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/QueueActivityBundle.kt @@ -0,0 +1,23 @@ +package me.devsaki.hentoid.activities.bundles + +import android.os.Bundle +import me.devsaki.hentoid.enums.Site +import me.devsaki.hentoid.util.boolean +import me.devsaki.hentoid.util.int +import me.devsaki.hentoid.util.long +import me.devsaki.hentoid.util.string + +/** + * Helper class to transfer data from any Activity to [me.devsaki.hentoid.activities.QueueActivity] + * through a Bundle + */ +class QueueActivityBundle(val bundle: Bundle = Bundle()) { + + var isErrorsTab by bundle.boolean(default = false) + + var contentHash by bundle.long(default = 0) + + var reviveDownloadForSiteCode by bundle.int(default = Site.NONE.code) + + var reviveOldCookie by bundle.string(default = "") +} \ No newline at end of file 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 deleted file mode 100644 index c0b5c569d3..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.java +++ /dev/null @@ -1,142 +0,0 @@ -package me.devsaki.hentoid.activities.bundles; - -import android.net.Uri; -import android.os.Bundle; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -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.database.domains.AttributeMap; - -/** - * Helper class to transfer data from any Activity to {@link me.devsaki.hentoid.activities.SearchActivity} - * through a Bundle. - * - * Use Builder class to set data; use Parser class to get data - */ -public class SearchActivityBundle { - private static final String KEY_ATTRIBUTE_TYPES = "attributeTypes"; - private static final String KEY_MODE = "mode"; - private static final String KEY_URI = "uri"; - private static final String KEY_GROUP = "group"; - private static final String EXCLUDE_MODE = "exclude"; - - private SearchActivityBundle() { - throw new UnsupportedOperationException(); - } - - public static final class Builder { - - private final Bundle bundle = new Bundle(); - - public void setAttributeTypes(AttributeType... attributeTypes) { - ArrayList attrTypes = new ArrayList<>(); - for (AttributeType type : attributeTypes) attrTypes.add(type.getCode()); - - bundle.putIntegerArrayList(KEY_ATTRIBUTE_TYPES, attrTypes); - } - public void setExcludeMode(boolean excludeMode){ - bundle.putBoolean(EXCLUDE_MODE, excludeMode); - } - - public void setMode(int mode) { - bundle.putInt(KEY_MODE, mode); - } - - public void setGroup(long group) { - bundle.putLong(KEY_GROUP, group); - } - - public Builder setUri(Uri uri) { - bundle.putString(KEY_URI, uri.toString()); - 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 (Map.Entry> entry : metadataMap.entrySet()) { - AttributeType attrType = entry.getKey(); - List attrs = entry.getValue(); - if (attrs != null) - for (Attribute attr : attrs) - searchUri.appendQueryParameter(attrType.name(), attr.getId() + ";" + attr.getName() + ";" + attr.isExcluded()); - } - - return searchUri.build(); - } - - public Bundle getBundle() { - return bundle; - } - } - - public static final class Parser { - - private final Bundle bundle; - - public Parser(@Nonnull Bundle bundle) { - this.bundle = bundle; - } - - public List getAttributeTypes() { - List result = new ArrayList<>(); - - List attrTypesList = bundle.getIntegerArrayList(KEY_ATTRIBUTE_TYPES); - if (null != attrTypesList && !attrTypesList.isEmpty()) - for (Integer i : attrTypesList) result.add(AttributeType.searchByCode(i)); - - return result; - } - public boolean getExcludeMode(){return bundle.getBoolean(EXCLUDE_MODE, false);} - public int getMode() { - return bundle.getInt(KEY_MODE, -1); - } - - public long getGroupId() { - return bundle.getLong(KEY_GROUP, -1); - } - - @Nullable - public Uri getUri() { - Uri result = null; - - String uriStr = bundle.getString(KEY_URI, ""); - if (!uriStr.isEmpty()) result = Uri.parse(uriStr); - - 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 (3 == attrParams.length) { - result.add(new Attribute(type, attrParams[1]).setId(Long.parseLong(attrParams[0])).setExcluded(Boolean.parseBoolean(attrParams[2]))); - } - } - } - - return result; - } - } - - -} diff --git a/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.kt b/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.kt new file mode 100644 index 0000000000..fb1e73a795 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/activities/bundles/SearchActivityBundle.kt @@ -0,0 +1,61 @@ +package me.devsaki.hentoid.activities.bundles + +import android.net.Uri +import android.os.Bundle +import me.devsaki.hentoid.database.domains.Attribute +import me.devsaki.hentoid.database.domains.AttributeMap +import me.devsaki.hentoid.enums.AttributeType +import me.devsaki.hentoid.util.* + +/** + * Helper class to transfer data from any Activity to [me.devsaki.hentoid.activities.SearchActivity] + * through a Bundle + */ +class SearchActivityBundle(val bundle: Bundle = Bundle()) { + + var attributeTypes by bundle.intArrayList() + + var excludeMode by bundle.boolean(default = false) + + var mode by bundle.int(default = -1) + + var groupId by bundle.long(default = -1) + + var uri by bundle.string(default = "") + + // Helper methods + companion object { + fun buildSearchUri(attributes: List?): Uri { + val metadataMap = AttributeMap() + if (attributes != null) metadataMap.addAll(attributes) + val searchUri = Uri.Builder() + .scheme("search") + .authority("hentoid") + for ((attrType, attrs) in metadataMap) { + if (attrs != null) for (attr in attrs) searchUri.appendQueryParameter( + attrType.name, + attr.id.toString() + ";" + attr.name + ";" + attr.isExcluded + ) + } + return searchUri.build() + } + + fun parseSearchUri(uri: Uri?): List { + val result: MutableList = ArrayList() + if (uri != null) for (typeStr in uri.queryParameterNames) { + val type = AttributeType.searchByName(typeStr) + if (type != null) for (attrStr in uri.getQueryParameters(typeStr)) { + val attrParams = attrStr.split(";").toTypedArray() + if (3 == attrParams.size) { + result.add( + Attribute(type, attrParams[1]) + .setId(attrParams[0].toLong()) + .setExcluded(attrParams[2].toBoolean()) + ) + } + } + } + return result + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java index 5f4e630a73..fafe8b2a63 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.java @@ -401,7 +401,7 @@ protected void onSaveInstanceState(@NonNull Bundle outState) { // doesn't work that well (bugged when using back/forward commands). A valid solution still has to be found BaseWebActivityBundle bundle = new BaseWebActivityBundle(); bundle.setUrl(webView.getUrl()); - outState.putAll(bundle.toBundle()); + outState.putAll(bundle.getBundle()); } @Override @@ -934,6 +934,7 @@ void processDownload(boolean quickDownload, boolean isDownloadPlus) { List errors = new ArrayList<>(); errors.add(new ErrorRecord(ErrorType.BLOCKED, currentContent.getUrl(), "tags", "blocked tags : " + TextUtils.join(", ", blockedTagsLocal), Instant.now())); currentContent.setErrorLog(errors); + currentContent.setDownloadMode(Preferences.getBrowserDlAction()); currentContent.setStatus(StatusContent.ERROR); dao.insertContent(currentContent); ToastHelper.toast(R.string.blocked_tag_queued, blockedTagsLocal.get(0)); @@ -966,9 +967,9 @@ private void goToQueue() { Intent intent = new Intent(this, QueueActivity.class); if (currentContent != null) { - QueueActivityBundle.Builder builder = new QueueActivityBundle.Builder(); + QueueActivityBundle builder = new QueueActivityBundle(); builder.setContentHash(currentContent.uniqueHash()); - builder.setIsErrorsTab(currentContent.getStatus().equals(StatusContent.ERROR)); + builder.setErrorsTab(currentContent.getStatus().equals(StatusContent.ERROR)); intent.putExtras(builder.getBundle()); } @@ -1030,7 +1031,7 @@ int processContent(@NonNull Content onlineContent, boolean quickDownload) { downloadParams.put(HttpHelper.HEADER_COOKIE_KEY, HttpHelper.getCookies(onlineContent.getCoverImageUrl())); downloadParams.put(HttpHelper.HEADER_REFERER_KEY, onlineContent.getSite().getUrl()); - Response onlineCover = HttpHelper.getOnlineResource( + Response onlineCover = HttpHelper.getOnlineResourceFast( HttpHelper.fixUrl(onlineContent.getCoverImageUrl(), getStartUrl()), requestHeadersList, getStartSite().useMobileAgent(), @@ -1311,7 +1312,7 @@ private void onSettingsClick() { PrefsBundle prefsBundle = new PrefsBundle(); prefsBundle.setBrowserPrefs(true); - intent.putExtras(prefsBundle.toBundle()); + intent.putExtras(prefsBundle.getBundle()); startActivity(intent); } diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/CustomWebViewClient.java b/app/src/main/java/me/devsaki/hentoid/activities/sources/CustomWebViewClient.java index e04d6614cf..f9bfaf6329 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/sources/CustomWebViewClient.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/CustomWebViewClient.java @@ -142,7 +142,7 @@ class CustomWebViewClient extends WebViewClient { checkmark = ImageHelper.BitmapToWebp( ImageHelper.tintBitmap( - ImageHelper.getBitmapFromVectorDrawable(HentoidApp.getInstance(), R.drawable.ic_check), + ImageHelper.getBitmapFromVectorDrawable(HentoidApp.getInstance(), R.drawable.ic_checked), HentoidApp.getInstance().getResources().getColor(R.color.secondary_light) ) ); 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 index 1d23ddf047..c41943b0c5 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/sources/HitomiActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/HitomiActivity.java @@ -7,11 +7,9 @@ import androidx.annotation.NonNull; -import java.util.List; import java.util.Map; import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.database.domains.ImageFile; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.images.HitomiParser; @@ -92,19 +90,20 @@ public WebResourceResponse shouldInterceptRequest(@NonNull WebView view, @NonNul protected Content processContent(@NonNull Content content, @NonNull String url, boolean quickDownload) { // Wait until the page's resources are all loaded if (!quickDownload) { - Timber.i(">> not loading"); + Timber.v(">> not loading"); while (!isLoading()) Helper.pause(20); - Timber.i(">> loading"); + Timber.v(">> loading"); while (isLoading()) Helper.pause(100); - Timber.i(">> done"); + Timber.v(">> done"); } HitomiParser parser = new HitomiParser(); try { - List images = parser.parseImageListWithWebview(content, webView); - content.setImageFiles(images); + /*List images =*/ + parser.parseImageListWithWebview(content, webView); // Only fetch them when queue is processed + //content.setImageFiles(images); content.setStatus(StatusContent.SAVED); } catch (Exception e) { - Timber.w(e); + Timber.i(e); content.setStatus(StatusContent.IGNORED); } 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 index baa7c540c2..bd3148a2b2 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/sources/NhentaiActivity.java +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/NhentaiActivity.java @@ -15,7 +15,7 @@ public class NhentaiActivity extends BaseWebActivity { private static final String DOMAIN_FILTER = "nhentai.net"; - private static final String[] GALLERY_FILTER = {"nhentai.net/g/", "nhentai.net/search/\\?q=[%0-9]+$"}; + private static final String[] GALLERY_FILTER = {"nhentai.net/g/[%0-9]+[/]{0,1}$", "nhentai.net/search/\\?q=[%0-9]+$"}; private static final String[] RESULTS_FILTER = {"//nhentai.net[/]*$", "//nhentai.net/\\?", "//nhentai.net/search/\\?", "//nhentai.net/(character|artist|parody|tag|group)/"}; private static final String[] BLOCKED_CONTENT = {"popunder"}; private static final String[] AD_BLOCKS = {"section.advertisement"}; 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 395bb31282..9e7c0d24b7 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java +++ b/app/src/main/java/me/devsaki/hentoid/database/DatabaseMaintenance.java @@ -29,6 +29,7 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Grouping; import me.devsaki.hentoid.enums.StatusContent; +import me.devsaki.hentoid.util.ContentHelper; import me.devsaki.hentoid.util.Preferences; import timber.log.Timber; @@ -305,6 +306,36 @@ private static void setDefaultPropertiesOneShot(@NonNull final Context context, db.updateContentObject(c); emitter.onNext(pos++ / max); } + contents = db.selectContentWithNullDlCompletionDateField(); + Timber.i("Set default value for Content.downloadCompletionDate field : %s items detected", contents.size()); + max = contents.size(); + pos = 1; + for (Content c : contents) { + if (ContentHelper.isInLibrary(c.getStatus())) + c.setDownloadCompletionDate(c.getDownloadDate()); + else + c.setDownloadCompletionDate(0); + db.updateContentObject(c); + emitter.onNext(pos++ / max); + } + contents = db.selectContentWithInvalidUploadDate(); + Timber.i("Fixing invalid upload dates : %s items detected", contents.size()); + max = contents.size(); + pos = 1; + for (Content c : contents) { + c.setUploadDate(c.getUploadDate() * 1000); + db.updateContentObject(c); + emitter.onNext(pos++ / max); + } + List chapters = db.selectChapterWithNullUploadDate(); + Timber.i("Set default value for Chapter.uploadDate field : %s items detected", chapters.size()); + max = chapters.size(); + pos = 1; + for (Chapter c : chapters) { + c.setUploadDate(0); + emitter.onNext(pos++ / max); + } + db.insertChapters(chapters); Timber.i("Set default ObjectBox properties : done"); } finally { db.closeThreadResources(); @@ -441,7 +472,7 @@ private static void reattachGroupCovers(@NonNull final Context context, Observab try { // Compute missing downloaded Content size according to underlying ImageFile sizes Timber.i("Reattaching group covers : start"); - List groups = db.selecGroupsWithNoCoverContent(); + List groups = db.selectGroupsWithNoCoverContent(); Timber.i("Reattaching group covers : %s groups detected", groups.size()); int max = groups.size(); float pos = 1; diff --git a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.java b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.java index 129db6f769..854df5d815 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.java +++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.java @@ -456,7 +456,7 @@ public LiveData> selectGroupsLive( } // Order by latest download date of children (ObjectBox can't do that natively) - if (Preferences.Constant.ORDER_FIELD_DOWNLOAD_DATE == orderField) { + if (Preferences.Constant.ORDER_FIELD_DOWNLOAD_PROCESSING_DATE == orderField) { MediatorLiveData> result = new MediatorLiveData<>(); result.addSource(workingData, groups -> { int sortOrder = orderDesc ? -1 : 1; 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 e352bf8b08..630762da2c 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java +++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Random; @@ -510,8 +511,10 @@ private Property getPropertyFromField(int prefsFieldCode) { return Content_.author; // Might not be what users want when there are multiple authors case Preferences.Constant.ORDER_FIELD_NB_PAGES: return Content_.qtyPages; - case Preferences.Constant.ORDER_FIELD_DOWNLOAD_DATE: + case Preferences.Constant.ORDER_FIELD_DOWNLOAD_PROCESSING_DATE: return Content_.downloadDate; + case Preferences.Constant.ORDER_FIELD_DOWNLOAD_COMPLETION_DATE: + return Content_.downloadCompletionDate; case Preferences.Constant.ORDER_FIELD_UPLOAD_DATE: return Content_.uploadDate; case Preferences.Constant.ORDER_FIELD_READ_DATE: @@ -781,11 +784,11 @@ long[] selectContentUniversalByGroupItem( return selectContentUniversalContentByGroupItem(queryStr, groupId, filterBookFavourites, contentAttrSubQuery.findIds(), orderField, orderDesc, bookCompletedOnly, bookNotCompletedOnly); } - public List getShuffledIds() { + List getShuffledIds() { return Stream.of(store.boxFor(ShuffleRecord.class).getAll()).map(ShuffleRecord::getContentId).toList(); } - public void shuffleContentIds() { + void shuffleContentIds() { // Clear previous shuffled list Box shuffleStore = store.boxFor(ShuffleRecord.class); shuffleStore.removeAll(); @@ -796,19 +799,21 @@ public void shuffleContentIds() { } private long[] shuffleRandomSortId(Query query) { - List queryIds = Helper.getListFromPrimitiveArray(query.findIds()); + Set queryIds = Helper.getSetFromPrimitiveArray(query.findIds()); List shuffleIds = getShuffledIds(); + LinkedHashSet shuffledSet = new LinkedHashSet<>(shuffleIds.size()); + shuffledSet.addAll(shuffleIds); // Keep common IDs - shuffleIds.retainAll(queryIds); + shuffledSet.retainAll(queryIds); // Isolate new IDs that have never been shuffled and append them at the end - if (shuffleIds.size() < queryIds.size()) { - queryIds.removeAll(shuffleIds); - shuffleIds.addAll(queryIds); + if (shuffledSet.size() < queryIds.size()) { + queryIds.removeAll(shuffledSet); + shuffledSet.addAll(queryIds); } - return Helper.getPrimitiveArrayFromList(shuffleIds); + return Helper.getPrimitiveArrayFromList(Stream.of(shuffledSet).toList()); } long[] selectContentSearchId(String title, long groupId, List tags, boolean filterBookFavourites, boolean filterPageFavourites, int orderField, boolean orderDesc, boolean bookCompletedOnly, boolean bookNotCompletedOnly) { @@ -946,7 +951,7 @@ else if (bookNotCompletedOnly) if (attr.isExcluded()) results.removeAll(idsAsList); else - results.retainAll(idsAsList); + results.retainAll(idsAsList); // Careful with retainAll performance } } @@ -1515,8 +1520,8 @@ List selectDownloadedContentWithNoReadProgress() { return store.boxFor(Content.class).query().in(Content_.status, libraryStatus).isNull(Content_.readProgress).build().find(); } - List selecGroupsWithNoCoverContent() { - return store.boxFor(Group.class).query().isNull(Group_.coverContentId).build().find(); + List selectGroupsWithNoCoverContent() { + return store.boxFor(Group.class).query().isNull(Group_.coverContentId).or().equal(Group_.coverContentId, 0).build().find(); } List selectContentWithNullCompleteField() { @@ -1531,6 +1536,18 @@ List selectContentWithNullMergeField() { return store.boxFor(Content.class).query().isNull(Content_.manuallyMerged).build().find(); } + List selectContentWithNullDlCompletionDateField() { + return store.boxFor(Content.class).query().isNull(Content_.downloadCompletionDate).build().find(); + } + + List selectContentWithInvalidUploadDate() { + return store.boxFor(Content.class).query().greater(Content_.uploadDate, 0).less(Content_.uploadDate, 10000000000L).build().find(); + } + + List selectChapterWithNullUploadDate() { + return store.boxFor(Chapter.class).query().isNull(Chapter_.uploadDate).build().find(); + } + Query selectOldStoredContentQ() { QueryBuilder query = store.boxFor(Content.class).query(); query.in(Content_.status, new int[]{ @@ -1550,8 +1567,10 @@ QueryBuilder selectStoredContentQ(boolean nonFavouritesOnly, boolean in query.in(Content_.status, libraryQueueStatus); else query.in(Content_.status, libraryStatus); + /* TODO temp query.notNull(Content_.storageUri); query.notEqual(Content_.storageUri, "", QueryBuilder.StringOrder.CASE_INSENSITIVE); + */ if (nonFavouritesOnly) query.equal(Content_.favourite, false); if (orderField > -1) { Property field = getPropertyFromField(orderField); @@ -1584,7 +1603,9 @@ Query selectNonHashedContent() { long[] selectCustomGroupedContent() { QueryBuilder customContentQB = store.boxFor(Content.class).query(); - customContentQB.link(Content_.groupItems).link(GroupItem_.group).equal(Group_.grouping, Grouping.CUSTOM.getId()); + customContentQB.link(Content_.groupItems).link(GroupItem_.group) + .equal(Group_.grouping, Grouping.CUSTOM.getId()) // Custom group + .equal(Group_.subtype, 0); // Not the Ungrouped group (subtype 1) return customContentQB.build().findIds(); // See https://github.com/objectbox/objectbox-java/issues/1028 /* diff --git a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxRandomDataSource.java b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxRandomDataSource.java index 881ef3a8c1..c5ebf6da91 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxRandomDataSource.java +++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxRandomDataSource.java @@ -4,11 +4,15 @@ import androidx.paging.DataSource; import androidx.paging.PositionalDataSource; +import com.annimon.stream.Stream; + import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import io.objectbox.query.LazyList; import io.objectbox.query.Query; @@ -19,11 +23,29 @@ class ObjectBoxRandomDataSource extends PositionalDataSource { private final Query query; private final DataObserver> observer; - private final List shuffleIds; + private final List shuffledList; + private final Map idsToQueryListIndexes = new HashMap<>(); private ObjectBoxRandomDataSource(Query query, List shuffleIds) { this.query = query; - this.shuffleIds = shuffleIds; + + Set queryIds = Helper.getSetFromPrimitiveArray(query.findIds()); + int idx = 0; + for (Long id : queryIds) idsToQueryListIndexes.put(id, idx++); + + LinkedHashSet shuffledSet = new LinkedHashSet<>(shuffleIds.size()); + shuffledSet.addAll(shuffleIds); + + // Keep common IDs (intersect) + shuffledSet.retainAll(queryIds); + + // Isolate new IDs that have never been shuffled and append them at the end + if (shuffledSet.size() < queryIds.size()) { + queryIds.removeAll(shuffledSet); + shuffledSet.addAll(queryIds); + } + shuffledList = Stream.of(shuffledSet).toList(); + this.observer = data -> ObjectBoxRandomDataSource.this.invalidate(); query.subscribe().onlyChanges().weak().observer(this.observer); } @@ -49,31 +71,13 @@ public void loadRange(@NonNull PositionalDataSource.LoadRangeParams params, @Non } private List loadRange(int startPosition, int loadCount) { - return shuffleRandomSort(this.query, startPosition, loadCount); - } - - private List shuffleRandomSort(Query query, int startPosition, int loadCount) { LazyList lazyList = query.findLazy(); - List queryIds = Helper.getListFromPrimitiveArray(query.findIds()); - Map idsToIndexes = new HashMap<>(); - for (int i = 0; i < queryIds.size(); i++) { - idsToIndexes.put(queryIds.get(i), i); - } - - // Keep common IDs - shuffleIds.retainAll(queryIds); - - // Isolate new IDs that have never been shuffled and append them at the end - if (shuffleIds.size() < queryIds.size()) { - queryIds.removeAll(shuffleIds); - shuffleIds.addAll(queryIds); - } - int maxPage = Math.min(startPosition + loadCount, shuffleIds.size()); + int maxPage = Math.min(startPosition + loadCount, shuffledList.size()); List result = new ArrayList<>(); for (int i = startPosition; i < maxPage; i++) { - Integer index = idsToIndexes.get(shuffleIds.get(i)); + Integer index = idsToQueryListIndexes.get(shuffledList.get(i)); if (index != null) result.add(lazyList.get(index)); } diff --git a/app/src/main/java/me/devsaki/hentoid/database/domains/Chapter.java b/app/src/main/java/me/devsaki/hentoid/database/domains/Chapter.java index 1b85d56021..ec3214c134 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/domains/Chapter.java +++ b/app/src/main/java/me/devsaki/hentoid/database/domains/Chapter.java @@ -25,6 +25,7 @@ public class Chapter { @Backlink(to = "chapter") private ToMany imageFiles; private String uniqueId = ""; + private long uploadDate = 0; public Chapter() { // Required by ObjectBox when an alternate constructor exists @@ -132,6 +133,14 @@ public void addImageFile(ImageFile img) { if (imageFiles != null) imageFiles.add(img); } + public long getUploadDate() { + return uploadDate; + } + + public void setUploadDate(long uploadDate) { + this.uploadDate = uploadDate; + } + @Override public boolean equals(Object o) { if (this == o) return true; 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 d6bb59060c..49b41f57c2 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 @@ -93,6 +93,7 @@ public class Content implements Serializable { private Integer qtyPages; // Integer is actually unnecessary, but changing this to plain int requires a small DB model migration... private long uploadDate; private long downloadDate = 0; + private long downloadCompletionDate = 0; @Index @Convert(converter = StatusContent.StatusContentConverter.class, dbType = Integer.class) private StatusContent status; @@ -530,6 +531,15 @@ public Content setDownloadDate(long downloadDate) { return this; } + public long getDownloadCompletionDate() { + return downloadCompletionDate; + } + + public Content setDownloadCompletionDate(long value) { + downloadCompletionDate = value; + return this; + } + public StatusContent getStatus() { return status; } diff --git a/app/src/main/java/me/devsaki/hentoid/enums/Grouping.java b/app/src/main/java/me/devsaki/hentoid/enums/Grouping.java index 92976f01b9..37c5700fd3 100644 --- a/app/src/main/java/me/devsaki/hentoid/enums/Grouping.java +++ b/app/src/main/java/me/devsaki/hentoid/enums/Grouping.java @@ -1,25 +1,28 @@ package me.devsaki.hentoid.enums; -import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import me.devsaki.hentoid.R; /** * Groupings */ public enum Grouping { - FLAT(0, "Flat", false, false, false), - ARTIST(1, "By artist", false, true, true), - DL_DATE(2, "By download date", false, false, false), - CUSTOM(98, "Custom", true, true, true), - NONE(99, "None", false, false, false); + FLAT(0, R.string.groups_flat, false, false, false), + ARTIST(1, R.string.groups_by_artist, false, true, true), + DL_DATE(2, R.string.groups_by_dl_date, false, false, false), + CUSTOM(98, R.string.groups_custom, true, true, true), + NONE(99, R.string.none, false, false, false); private final int id; - private final String name; + private final @StringRes + int name; private final boolean canReorderGroups; private final boolean canDeleteGroups; private final boolean canReorderBooks; - Grouping(int id, @NonNull String name, boolean canReorderGroups, boolean canDeleteGroups, boolean canReorderBooks) { + Grouping(int id, @StringRes int name, boolean canReorderGroups, boolean canDeleteGroups, boolean canReorderBooks) { this.id = id; this.name = name; this.canReorderGroups = canReorderGroups; @@ -31,7 +34,8 @@ public int getId() { return id; } - public String getName() { + public @StringRes + int getName() { return name; } diff --git a/app/src/main/java/me/devsaki/hentoid/events/CommunicationEvent.java b/app/src/main/java/me/devsaki/hentoid/events/CommunicationEvent.java index c1bf6a0f43..35e1132b48 100644 --- a/app/src/main/java/me/devsaki/hentoid/events/CommunicationEvent.java +++ b/app/src/main/java/me/devsaki/hentoid/events/CommunicationEvent.java @@ -6,7 +6,7 @@ public class CommunicationEvent { public static final int EV_SEARCH = 1; public static final int EV_ADVANCED_SEARCH = 2; - public static final int EV_UPDATE_SORT = 4; + public static final int EV_UPDATE_TOOLBAR = 4; public static final int EV_CLOSED = 5; public static final int EV_ENABLE = 6; public static final int EV_DISABLE = 7; 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 b4d4a784b0..fc31917709 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/SearchBottomSheetFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/SearchBottomSheetFragment.java @@ -79,7 +79,7 @@ public class SearchBottomSheetFragment extends BottomSheetDialogFragment { // Current page of paged content (used to display the attributes list as an endless list) private int currentPage; - private long mTotalSelectedCount; // Total count of current available attributes + private int mTotalSelectedCount; // Total count of current available attributes // Selected attribute types (selection done in the activity view) private List selectedAttributeTypes = new ArrayList<>(); @@ -98,9 +98,10 @@ public class SearchBottomSheetFragment extends BottomSheetDialogFragment { private boolean excludeAttr = false; public static void invoke(@NonNull Context context, @NonNull FragmentManager fragmentManager, AttributeType[] types, boolean excludeClicked) { - SearchActivityBundle.Builder builder = new SearchActivityBundle.Builder(); + SearchActivityBundle builder = new SearchActivityBundle(); - builder.setAttributeTypes(types); + ArrayList attrTypes = new ArrayList<>(Stream.of(types).map(AttributeType::getCode).toList()); + builder.setAttributeTypes(attrTypes); builder.setExcludeMode(excludeClicked); SearchBottomSheetFragment searchBottomSheetFragment = new SearchBottomSheetFragment(); @@ -115,8 +116,10 @@ public void onAttach(@NonNull Context context) { Bundle bundle = getArguments(); if (bundle != null) { - SearchActivityBundle.Parser parser = new SearchActivityBundle.Parser(bundle); - selectedAttributeTypes = parser.getAttributeTypes(); + SearchActivityBundle parser = new SearchActivityBundle(bundle); + List attributeTypeCodes = parser.getAttributeTypes(); + if (null == attributeTypeCodes) attributeTypeCodes = Collections.emptyList(); + selectedAttributeTypes = Stream.of(attributeTypeCodes).map(AttributeType::searchByCode).toList(); excludeAttr = parser.getExcludeMode(); long groupId = parser.getGroupId(); currentPage = 1; @@ -135,7 +138,7 @@ public void onAttach(@NonNull Context context) { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.include_search_filter_category, container, false); + View rootView = inflater.inflate(R.layout.include_search_bottom_panel, container, false); AttributeType mainAttr = selectedAttributeTypes.get(0); // Image that displays current metadata type icon (e.g. face icon for character) @@ -257,7 +260,7 @@ private void onAttributesReady(CollectionDAO.AttributeQueryResult results) { a.setName(LanguageHelper.getLocalNameFromLanguage(requireContext(), a.getName())); } - mTotalSelectedCount = results.totalSelectedAttributes/* - selectedAttributes.size()*/; + mTotalSelectedCount = (int) results.totalSelectedAttributes/* - selectedAttributes.size()*/; if (clearOnSuccess) attributeAdapter.clear(); if (0 == mTotalSelectedCount) { String searchQuery = tagSearchView.getQuery().toString(); diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryBottomGroupsFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryBottomGroupsFragment.java new file mode 100644 index 0000000000..10dcdb7e19 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryBottomGroupsFragment.java @@ -0,0 +1,164 @@ +package me.devsaki.hentoid.fragments.library; + +import static me.devsaki.hentoid.util.Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS; +import static me.devsaki.hentoid.util.Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS_GROUPS; +import static me.devsaki.hentoid.util.Preferences.Constant.ARTIST_GROUP_VISIBILITY_GROUPS; + +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 androidx.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProvider; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.mikepenz.fastadapter.FastAdapter; +import com.mikepenz.fastadapter.adapters.ItemAdapter; +import com.mikepenz.fastadapter.select.SelectExtension; + +import java.util.ArrayList; +import java.util.List; + +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.activities.bundles.LibraryBottomSortFilterBundle; +import me.devsaki.hentoid.databinding.IncludeLibraryGroupsBottomPanelBinding; +import me.devsaki.hentoid.enums.Grouping; +import me.devsaki.hentoid.util.Preferences; +import me.devsaki.hentoid.util.ThemeHelper; +import me.devsaki.hentoid.viewholders.TextItem; +import me.devsaki.hentoid.viewmodels.LibraryViewModel; +import me.devsaki.hentoid.viewmodels.ViewModelFactory; + +public class LibraryBottomGroupsFragment extends BottomSheetDialogFragment { + + private LibraryViewModel viewModel; + + // UI + private IncludeLibraryGroupsBottomPanelBinding binding = null; + + // RecyclerView controls + private final ItemAdapter> itemAdapter = new ItemAdapter<>(); + private final FastAdapter> fastAdapter = FastAdapter.with(itemAdapter); + private SelectExtension> selectExtension; + + // Variables + private boolean isCustomGroupingAvailable; + + public static synchronized void invoke( + Context context, + FragmentManager fragmentManager) { + // Don't re-create it if already shown + for (Fragment fragment : fragmentManager.getFragments()) + if (fragment instanceof LibraryBottomGroupsFragment) return; + + LibraryBottomSortFilterBundle builder = new LibraryBottomSortFilterBundle(); + LibraryBottomGroupsFragment libraryBottomSheetFragment = new LibraryBottomGroupsFragment(); + libraryBottomSheetFragment.setArguments(builder.getBundle()); + ThemeHelper.setStyle(context, libraryBottomSheetFragment, STYLE_NORMAL, R.style.Theme_Light_BottomSheetDialog); + libraryBottomSheetFragment.show(fragmentManager, "libraryBottomSheetFragment"); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + Bundle bundle = getArguments(); + if (bundle != null) { + LibraryBottomSortFilterBundle parser = new LibraryBottomSortFilterBundle(bundle); + } + + ViewModelFactory vmFactory = new ViewModelFactory(requireActivity().getApplication()); + viewModel = new ViewModelProvider(requireActivity(), vmFactory).get(LibraryViewModel.class); + + viewModel.isCustomGroupingAvailable().observe(this, b -> { + isCustomGroupingAvailable = b; + itemAdapter.set(getGroupings()); + }); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = IncludeLibraryGroupsBottomPanelBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + // SORT TAB + // Gets (or creates and attaches if not yet existing) the extension from the given `FastAdapter` + selectExtension = fastAdapter.getOrCreateExtension(SelectExtension.class); + if (selectExtension != null) { + selectExtension.setSelectable(true); + selectExtension.setMultiSelect(false); + selectExtension.setSelectOnLongClick(false); + selectExtension.setSelectWithItemUpdate(true); + selectExtension.setAllowDeselection(false); + selectExtension.setSelectionListener((item, selected) -> { + if (selected) onSelectionChanged(item); + }); + } + binding.list.setAdapter(fastAdapter); + + updateArtistVisibility(); + + binding.artistDisplayGrp.addOnButtonCheckedListener((g, i, b) -> { + int code; + if (binding.artistDisplayArtists.isChecked() && binding.artistDisplayGroups.isChecked()) + code = ARTIST_GROUP_VISIBILITY_ARTISTS_GROUPS; + else if (binding.artistDisplayArtists.isChecked()) + code = ARTIST_GROUP_VISIBILITY_ARTISTS; + else code = ARTIST_GROUP_VISIBILITY_GROUPS; + Preferences.setArtistGroupVisibility(code); + updateArtistVisibility(); + viewModel.searchGroup(); + }); + } + + private List> getGroupings() { + List> result = new ArrayList<>(); + result.add(createFromGrouping(Grouping.FLAT)); + result.add(createFromGrouping(Grouping.ARTIST)); + result.add(createFromGrouping(Grouping.DL_DATE)); + if (isCustomGroupingAvailable) result.add(createFromGrouping(Grouping.CUSTOM)); + return result; + } + + private TextItem createFromGrouping(@NonNull Grouping grouping) { + return new TextItem<>( + getResources().getString(grouping.getName()), + grouping.getId(), + true, + Preferences.getGroupingDisplay().getId() == grouping.getId()); + } + + private void updateArtistVisibility() { + int visibility = (Preferences.getGroupingDisplay() == Grouping.ARTIST) ? View.VISIBLE : View.INVISIBLE; + binding.artistDisplayTxt.setVisibility(visibility); + binding.artistDisplayGrp.setVisibility(visibility); + int code = Preferences.getArtistGroupVisibility(); + binding.artistDisplayArtists.setChecked(ARTIST_GROUP_VISIBILITY_ARTISTS_GROUPS == code || ARTIST_GROUP_VISIBILITY_ARTISTS == code); + binding.artistDisplayGroups.setChecked(ARTIST_GROUP_VISIBILITY_ARTISTS_GROUPS == code || ARTIST_GROUP_VISIBILITY_GROUPS == code); + } + + /** + * Callback for any selected item + */ + private void onSelectionChanged(TextItem item) { + Integer code = item.getTag(); + if (code != null) { + viewModel.setGrouping(code); + updateArtistVisibility(); + } + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryBottomSortFilterFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryBottomSortFilterFragment.java new file mode 100644 index 0000000000..50e7a7218e --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryBottomSortFilterFragment.java @@ -0,0 +1,267 @@ +package me.devsaki.hentoid.fragments.library; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProvider; + +import com.annimon.stream.Optional; +import com.annimon.stream.Stream; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.mikepenz.fastadapter.FastAdapter; +import com.mikepenz.fastadapter.adapters.ItemAdapter; +import com.mikepenz.fastadapter.select.SelectExtension; + +import java.util.ArrayList; +import java.util.List; + +import me.devsaki.hentoid.R; +import me.devsaki.hentoid.activities.LibraryActivity; +import me.devsaki.hentoid.activities.bundles.LibraryBottomSortFilterBundle; +import me.devsaki.hentoid.databinding.IncludeLibrarySortFilterBottomPanelBinding; +import me.devsaki.hentoid.util.Preferences; +import me.devsaki.hentoid.util.ThemeHelper; +import me.devsaki.hentoid.viewholders.TextItem; +import me.devsaki.hentoid.viewmodels.LibraryViewModel; +import me.devsaki.hentoid.viewmodels.ViewModelFactory; +import me.devsaki.hentoid.widget.ContentSearchManager; +import me.devsaki.hentoid.widget.GroupSearchManager; + +public class LibraryBottomSortFilterFragment extends BottomSheetDialogFragment { + + private LibraryViewModel viewModel; + + // UI + private IncludeLibrarySortFilterBottomPanelBinding binding = null; + + // RecyclerView controls + private final ItemAdapter> itemAdapter = new ItemAdapter<>(); + private final FastAdapter> fastAdapter = FastAdapter.with(itemAdapter); + private SelectExtension> selectExtension; + + // Variables + private boolean isUngroupedGroupDisplayed; + private boolean isGroupsDisplayed; + private boolean favouriteFilter; + private boolean completedFilter; + private boolean notCompletedFilter; + private @ColorInt + int greyColor; + private @ColorInt + int selectedColor; + + + public static synchronized void invoke( + Context context, + FragmentManager fragmentManager, + boolean isGroupsDisplayed, + boolean isUngroupedGroupDisplayed) { + // Don't re-create it if already shown + for (Fragment fragment : fragmentManager.getFragments()) + if (fragment instanceof LibraryBottomSortFilterFragment) return; + + LibraryBottomSortFilterBundle builder = new LibraryBottomSortFilterBundle(); + builder.setGroupsDisplayed(isGroupsDisplayed); + builder.setUngroupedGroupDisplayed(isUngroupedGroupDisplayed); + + LibraryBottomSortFilterFragment libraryBottomSheetFragment = new LibraryBottomSortFilterFragment(); + libraryBottomSheetFragment.setArguments(builder.getBundle()); + ThemeHelper.setStyle(context, libraryBottomSheetFragment, STYLE_NORMAL, R.style.Theme_Light_BottomSheetDialog); + libraryBottomSheetFragment.show(fragmentManager, "libraryBottomSheetFragment"); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + Bundle bundle = getArguments(); + if (bundle != null) { + LibraryBottomSortFilterBundle parser = new LibraryBottomSortFilterBundle(bundle); + isGroupsDisplayed = parser.isGroupsDisplayed(); + isUngroupedGroupDisplayed = parser.isUngroupedGroupDisplayed(); + } + + ViewModelFactory vmFactory = new ViewModelFactory(requireActivity().getApplication()); + viewModel = new ViewModelProvider(requireActivity(), vmFactory).get(LibraryViewModel.class); + + viewModel.getContentSearchManagerBundle().observe(this, b -> { + if (isGroupsDisplayed) return; + ContentSearchManager.ContentSearchBundle searchBundle = new ContentSearchManager.ContentSearchBundle(b); + favouriteFilter = searchBundle.getFilterBookFavourites(); + completedFilter = searchBundle.getFilterBookCompleted(); + notCompletedFilter = searchBundle.getFilterBookNotCompleted(); + updateFilterTab(); + }); + viewModel.getGroupSearchManagerBundle().observe(this, b -> { + if (!isGroupsDisplayed) return; + GroupSearchManager.GroupSearchBundle searchBundle = new GroupSearchManager.GroupSearchBundle(b); + favouriteFilter = searchBundle.getFilterFavourites(); + updateFilterTab(); + }); + + greyColor = ContextCompat.getColor(context, R.color.medium_gray); + selectedColor = ThemeHelper.getColor(context, R.color.secondary_light); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = IncludeLibrarySortFilterBottomPanelBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + // SORT TAB + // Gets (or creates and attaches if not yet existing) the extension from the given `FastAdapter` + selectExtension = fastAdapter.getOrCreateExtension(SelectExtension.class); + if (selectExtension != null) { + selectExtension.setSelectable(true); + selectExtension.setMultiSelect(false); + selectExtension.setSelectOnLongClick(false); + selectExtension.setSelectWithItemUpdate(true); + selectExtension.setAllowDeselection(false); + selectExtension.setSelectionListener((item, selected) -> { + if (selected) this.onSelectionChanged(); + }); + } + binding.list.setAdapter(fastAdapter); + itemAdapter.set(getSortFields()); + + updateSortDirection(); + + binding.sortRandom.setOnClickListener(v -> { + viewModel.shuffleContent(); + viewModel.searchContent(); + }); + binding.sortAscDesc.addOnButtonCheckedListener((g, i, b) -> { + if (!b) return; + if (isGroupsDisplayed) { + Preferences.setGroupSortDesc(i == R.id.sort_descending); + viewModel.searchGroup(); + } else { + Preferences.setContentSortDesc(i == R.id.sort_descending); + viewModel.searchContent(); + } + }); + + binding.filterFavsBtn.setOnClickListener( + v -> { + favouriteFilter = !favouriteFilter; + updateFilterTab(); + if (isGroupsDisplayed) + viewModel.setGroupFavouriteFilter(favouriteFilter); + else + viewModel.setContentFavouriteFilter(favouriteFilter); + } + ); + binding.filterCompletedBtn.setOnClickListener( + v -> { + completedFilter = !completedFilter; + updateFilterTab(); + viewModel.toggleCompletedFilter(); + } + ); + binding.filterNotCompletedBtn.setOnClickListener( + v -> { + notCompletedFilter = !notCompletedFilter; + updateFilterTab(); + viewModel.toggleNotCompletedFilter(); + } + ); + } + + private void updateSortDirection() { + boolean isRandom = ((isGroupsDisplayed ? Preferences.getGroupSortField() : Preferences.getContentSortField()) == Preferences.Constant.ORDER_FIELD_RANDOM); + if (isRandom) { + binding.sortAscending.setVisibility(View.GONE); + binding.sortDescending.setVisibility(View.GONE); + binding.sortRandom.setVisibility(View.VISIBLE); + binding.sortRandom.setChecked(true); + } else { + binding.sortRandom.setVisibility(View.GONE); + binding.sortAscending.setVisibility(View.VISIBLE); + binding.sortDescending.setVisibility(View.VISIBLE); + boolean currentPrefSortDesc = isGroupsDisplayed ? Preferences.isGroupSortDesc() : Preferences.isContentSortDesc(); + binding.sortAscDesc.check(currentPrefSortDesc ? R.id.sort_descending : R.id.sort_ascending); + } + } + + private void updateFilterTab() { + binding.filterFavsBtn.setColorFilter(favouriteFilter ? selectedColor : greyColor); + + int completeFiltersVisibility = isGroupsDisplayed ? View.GONE : View.VISIBLE; + binding.filterCompletedBtn.setVisibility(completeFiltersVisibility); + binding.filterNotCompletedBtn.setVisibility(completeFiltersVisibility); + + binding.filterCompletedBtn.setColorFilter(completedFilter ? selectedColor : greyColor); + binding.filterNotCompletedBtn.setColorFilter(notCompletedFilter ? selectedColor : greyColor); + } + + private List> getSortFields() { + List> result = new ArrayList<>(); + if (isGroupsDisplayed) { + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_TITLE)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_CHILDREN)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_DOWNLOAD_PROCESSING_DATE)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_CUSTOM)); + } else { + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_TITLE)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_ARTIST)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_NB_PAGES)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_DOWNLOAD_PROCESSING_DATE)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_DOWNLOAD_COMPLETION_DATE)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_UPLOAD_DATE)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_READ_DATE)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_READS)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_SIZE)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_READ_PROGRESS)); + if (Preferences.getGroupingDisplay().canReorderBooks() && !isUngroupedGroupDisplayed) + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_CUSTOM)); + result.add(createFromFieldCode(Preferences.Constant.ORDER_FIELD_RANDOM)); + } + return result; + } + + private TextItem createFromFieldCode(int sortFieldCode) { + int currentPrefFieldCode = isGroupsDisplayed ? Preferences.getGroupSortField() : Preferences.getContentSortField(); + return new TextItem<>( + getResources().getString(LibraryActivity.getNameFromFieldCode(sortFieldCode)), + sortFieldCode, + true, + currentPrefFieldCode == sortFieldCode); + } + + /** + * Callback for any selection change (item added to or removed from selection) + */ + private void onSelectionChanged() { + Optional> item = Stream.of(selectExtension.getSelectedItems()).findFirst(); + if (item.isPresent()) { + Integer code = item.get().getTag(); + if (code != null) + if (isGroupsDisplayed) { + Preferences.setGroupSortField(code); + viewModel.searchGroup(); + } else { + Preferences.setContentSortField(code); + viewModel.searchContent(); + } + } + updateSortDirection(); + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.java index b4293e9445..146e9a1f9d 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.java @@ -6,7 +6,7 @@ import static me.devsaki.hentoid.events.CommunicationEvent.EV_DISABLE; import static me.devsaki.hentoid.events.CommunicationEvent.EV_ENABLE; import static me.devsaki.hentoid.events.CommunicationEvent.EV_SEARCH; -import static me.devsaki.hentoid.events.CommunicationEvent.EV_UPDATE_SORT; +import static me.devsaki.hentoid.events.CommunicationEvent.EV_UPDATE_TOOLBAR; import static me.devsaki.hentoid.events.CommunicationEvent.RC_CONTENTS; import static me.devsaki.hentoid.util.Preferences.Constant.QUEUE_NEW_DOWNLOADS_POSITION_ASK; import static me.devsaki.hentoid.util.Preferences.Constant.QUEUE_NEW_DOWNLOADS_POSITION_BOTTOM; @@ -22,12 +22,10 @@ import android.os.Handler; import android.os.Looper; import android.os.SystemClock; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.TextView; import androidx.activity.OnBackPressedCallback; @@ -35,11 +33,9 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.DimenRes; -import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; -import androidx.appcompat.widget.PopupMenu; import androidx.documentfile.provider.DocumentFile; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; @@ -54,6 +50,7 @@ import com.annimon.stream.Stream; import com.annimon.stream.function.Consumer; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; import com.google.firebase.crashlytics.FirebaseCrashlytics; @@ -122,6 +119,7 @@ import me.devsaki.hentoid.widget.AutofitGridLayoutManager; import me.devsaki.hentoid.widget.FastAdapterPreClickSelectHelper; import me.devsaki.hentoid.widget.LibraryPager; +import me.devsaki.hentoid.widget.ScrollPositionListener; import me.zhanghai.android.fastscroll.FastScrollerBuilder; import timber.log.Timber; @@ -152,14 +150,10 @@ public class LibraryContentFragment extends Fragment implements ChangeGroupDialo private RecyclerView recyclerView; // LayoutManager of the recyclerView private LinearLayoutManager llm; - - // === SORT TOOLBAR - // Sort direction button - private ImageView sortDirectionButton; - // Sort reshuffle button - private View sortReshuffleButton; - // Sort field button - private TextView sortFieldButton; + // "Go to top" FAB + private FloatingActionButton topFab; + // Scroll listener for the top FAB + private final ScrollPositionListener scrollListener = new ScrollPositionListener(this::onScrollPositionChange); // === FASTADAPTER COMPONENTS AND HELPERS private ItemAdapter itemAdapter; @@ -185,6 +179,8 @@ public class LibraryContentFragment extends Fragment implements ChangeGroupDialo private Group group = null; // TODO doc private boolean enabled = true; + // TODO doc + private Bundle contentSearchBundle = null; // Used to start processing when the recyclerView has finished updating private Debouncer listRefreshDebouncer; @@ -215,13 +211,13 @@ public boolean areContentsTheSame(@NonNull Content oldItem, @NonNull Content new @Nullable @Override public Object getChangePayload(@NonNull Content oldItem, @NonNull Content newItem) { - ContentItemBundle.Builder diffBundleBuilder = new ContentItemBundle.Builder(); + ContentItemBundle diffBundleBuilder = new ContentItemBundle(); if (oldItem.isFavourite() != newItem.isFavourite()) { - diffBundleBuilder.setIsFavourite(newItem.isFavourite()); + diffBundleBuilder.setFavourite(newItem.isFavourite()); } if (oldItem.isCompleted() != newItem.isCompleted()) { - diffBundleBuilder.setIsCompleted(newItem.isCompleted()); + diffBundleBuilder.setCompleted(newItem.isCompleted()); } if (oldItem.getReads() != newItem.getReads()) { diffBundleBuilder.setReads(newItem.getReads()); @@ -261,13 +257,13 @@ public boolean areContentsTheSame(ContentItem oldContentItem, ContentItem newCon if (null == oldItem || null == newItem) return false; - ContentItemBundle.Builder diffBundleBuilder = new ContentItemBundle.Builder(); + ContentItemBundle diffBundleBuilder = new ContentItemBundle(); if (oldItem.isFavourite() != newItem.isFavourite()) { - diffBundleBuilder.setIsFavourite(newItem.isFavourite()); + diffBundleBuilder.setFavourite(newItem.isFavourite()); } if (oldItem.isCompleted() != newItem.isCompleted()) { - diffBundleBuilder.setIsCompleted(newItem.isCompleted()); + diffBundleBuilder.setCompleted(newItem.isCompleted()); } if (oldItem.getReads() != newItem.getReads()) { diffBundleBuilder.setReads(newItem.getReads()); @@ -324,12 +320,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - viewModel.getNewSearch().observe(getViewLifecycleOwner(), this::onNewSearch); + viewModel.getNewContentSearch().observe(getViewLifecycleOwner(), this::onNewSearch); viewModel.getLibraryPaged().observe(getViewLifecycleOwner(), this::onLibraryChanged); viewModel.getTotalContent().observe(getViewLifecycleOwner(), this::onTotalContentChanged); viewModel.getGroup().observe(getViewLifecycleOwner(), this::onGroupChanged); - - viewModel.updateContentOrder(); // Trigger a blank search + viewModel.getContentSearchManagerBundle().observe(getViewLifecycleOwner(), b -> contentSearchBundle = b); // Display pager tooltip if (pager.isVisible()) pager.showTooltip(getViewLifecycleOwner()); @@ -353,10 +348,6 @@ public void onDisable() { private void initUI(@NonNull View rootView) { emptyText = requireViewById(rootView, R.id.library_empty_txt); - sortDirectionButton = activity.get().getSortDirectionButton(); - sortReshuffleButton = activity.get().getSortReshuffleButton(); - sortFieldButton = activity.get().getSortFieldButton(); - // RecyclerView recyclerView = requireViewById(rootView, R.id.library_list); if (Preferences.Constant.LIBRARY_DISPLAY_LIST == Preferences.getLibraryDisplay()) @@ -364,13 +355,22 @@ private void initUI(@NonNull View rootView) { else llm = new AutofitGridLayoutManager(requireContext(), (int) getResources().getDimension(R.dimen.card_grid_width)); recyclerView.setLayoutManager(llm); + recyclerView.addOnScrollListener(scrollListener); new FastScrollerBuilder(recyclerView).build(); + // Top FAB + topFab = requireViewById(rootView, R.id.top_fab); + topFab.setOnClickListener(v -> llm.scrollToPositionWithOffset(0, 0)); + topFab.setOnLongClickListener(v -> { + Preferences.setTopFabEnabled(false); + topFab.setVisibility(View.GONE); + return true; + }); + // Pager pager.initUI(rootView); setPagingMethod(Preferences.getEndlessScroll(), false); - updateSortControls(); addCustomBackControl(); } @@ -385,56 +385,6 @@ public void handleOnBackPressed() { activity.get().getOnBackPressedDispatcher().addCallback(activity.get(), callback); } - private void updateSortControls() { - // Sort controls - sortDirectionButton.setImageResource(Preferences.isContentSortDesc() ? R.drawable.ic_simple_arrow_down : R.drawable.ic_simple_arrow_up); - sortDirectionButton.setOnClickListener(v -> { - boolean sortDesc = !Preferences.isContentSortDesc(); - Preferences.setContentSortDesc(sortDesc); - // Update icon - sortDirectionButton.setImageResource(sortDesc ? R.drawable.ic_simple_arrow_down : R.drawable.ic_simple_arrow_up); - // Run a new search - viewModel.updateContentOrder(); - activity.get().sortCommandsAutoHide(true, null); - }); - sortReshuffleButton.setOnClickListener(v -> { - viewModel.shuffleContent(); - viewModel.updateContentOrder(); - activity.get().sortCommandsAutoHide(true, null); - }); - sortFieldButton.setText(getNameFromFieldCode(Preferences.getContentSortField())); - sortFieldButton.setOnClickListener(v -> { - // Load and display the field popup menu - PopupMenu popup = new PopupMenu(requireContext(), sortFieldButton, Gravity.END); - popup.getMenuInflater() - .inflate(R.menu.library_books_sort_popup, popup.getMenu()); - - popup.getMenu().findItem(R.id.sort_custom).setVisible(group != null && group.hasCustomBookOrder); - popup.setOnMenuItemClickListener(item -> { - // Update button text - sortFieldButton.setText(item.getTitle()); - item.setChecked(true); - int fieldCode = getFieldCodeFromMenuId(item.getItemId()); - if (fieldCode == Preferences.Constant.ORDER_FIELD_RANDOM) { - viewModel.shuffleContent(); - sortDirectionButton.setVisibility(View.GONE); - sortReshuffleButton.setVisibility(View.VISIBLE); - } else { - sortReshuffleButton.setVisibility(View.GONE); - sortDirectionButton.setVisibility(View.VISIBLE); - } - - Preferences.setContentSortField(fieldCode); - // Run a new search - viewModel.updateContentOrder(); - activity.get().sortCommandsAutoHide(true, popup); - return true; - }); - popup.show(); //showing popup menu - activity.get().sortCommandsAutoHide(true, popup); - }); //closing the setOnClickListener method - } - private String getQuery() { return activity.get().getQuery(); } @@ -451,73 +401,10 @@ private void setMetadata(List attrs) { activity.get().setMetadata(attrs); } - private int getFieldCodeFromMenuId(@IdRes int menuId) { - switch (menuId) { - case (R.id.sort_title): - return Preferences.Constant.ORDER_FIELD_TITLE; - case (R.id.sort_artist): - return Preferences.Constant.ORDER_FIELD_ARTIST; - case (R.id.sort_pages): - return Preferences.Constant.ORDER_FIELD_NB_PAGES; - case (R.id.sort_dl_date): - return Preferences.Constant.ORDER_FIELD_DOWNLOAD_DATE; - case (R.id.sort_read_date): - return Preferences.Constant.ORDER_FIELD_READ_DATE; - case (R.id.sort_reads): - return Preferences.Constant.ORDER_FIELD_READS; - case (R.id.sort_size): - return Preferences.Constant.ORDER_FIELD_SIZE; - case (R.id.sort_reading_progress): - return Preferences.Constant.ORDER_FIELD_READ_PROGRESS; - case (R.id.sort_custom): - return Preferences.Constant.ORDER_FIELD_CUSTOM; - case (R.id.sort_random): - return Preferences.Constant.ORDER_FIELD_RANDOM; - default: - return Preferences.Constant.ORDER_FIELD_NONE; - } - } + private void enterEditMode() { + activity.get().setEditMode(true); - private int getNameFromFieldCode(int prefFieldCode) { - switch (prefFieldCode) { - case (Preferences.Constant.ORDER_FIELD_TITLE): - return R.string.sort_title; - case (Preferences.Constant.ORDER_FIELD_ARTIST): - return R.string.sort_artist; - case (Preferences.Constant.ORDER_FIELD_NB_PAGES): - return R.string.sort_pages; - case (Preferences.Constant.ORDER_FIELD_DOWNLOAD_DATE): - return R.string.sort_dl_date; - case (Preferences.Constant.ORDER_FIELD_READ_DATE): - return R.string.sort_read_date; - case (Preferences.Constant.ORDER_FIELD_READS): - return R.string.sort_reads; - case (Preferences.Constant.ORDER_FIELD_SIZE): - return R.string.sort_size; - case (Preferences.Constant.ORDER_FIELD_READ_PROGRESS): - return R.string.sort_reading_progress; - case (Preferences.Constant.ORDER_FIELD_CUSTOM): - return R.string.sort_custom; - case (Preferences.Constant.ORDER_FIELD_RANDOM): - return R.string.sort_random; - default: - return R.string.sort_invalid; - } - } - - private void toggleEditMode() { - activity.get().toggleEditMode(); - - // Leave edit mode by validating => Save new item position - if (!activity.get().isEditMode()) { - // Set ordering field to custom - Preferences.setContentSortField(Preferences.Constant.ORDER_FIELD_CUSTOM); - sortFieldButton.setText(getNameFromFieldCode(Preferences.Constant.ORDER_FIELD_CUSTOM)); - // Set ordering direction to ASC (we just manually ordered stuff; it has to be displayed as is) - Preferences.setContentSortDesc(false); - viewModel.saveContentPositions(Stream.of(itemAdapter.getAdapterItems()).map(ContentItem::getContent).withoutNulls().toList(), this::refreshIfNeeded); - group.hasCustomBookOrder = true; - } else if (group.hasCustomBookOrder) { // Enter edit mode -> warn if a custom order already exists + if (group.hasCustomBookOrder) { // Warn if a custom order already exists new MaterialAlertDialogBuilder(requireContext(), ThemeHelper.getIdForCurrentTheme(requireContext(), R.style.Theme_Light_Dialog)) .setIcon(R.drawable.ic_warning) .setTitle(R.string.app_name) @@ -527,7 +414,7 @@ private void toggleEditMode() { .setNegativeButton(R.string.no, (dialog2, which) -> { dialog2.dismiss(); - cancelEditMode(); + cancelEdit(); }) .create() .show(); @@ -536,18 +423,35 @@ private void toggleEditMode() { setPagingMethod(Preferences.getEndlessScroll(), activity.get().isEditMode()); } - private void cancelEditMode() { + private void cancelEdit() { activity.get().setEditMode(false); setPagingMethod(Preferences.getEndlessScroll(), false); } + private void confirmEdit() { + activity.get().setEditMode(false); + + // == Save new item position + // Set ordering field to custom + Preferences.setContentSortField(Preferences.Constant.ORDER_FIELD_CUSTOM); + // Set ordering direction to ASC (we just manually ordered stuff; it has to be displayed as is) + Preferences.setContentSortDesc(false); + viewModel.saveContentPositions(Stream.of(itemAdapter.getAdapterItems()).map(ContentItem::getContent).withoutNulls().toList(), this::refreshIfNeeded); + group.hasCustomBookOrder = true; + + setPagingMethod(Preferences.getEndlessScroll(), activity.get().isEditMode()); + } + private boolean onToolbarItemClicked(@NonNull MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.action_edit: - toggleEditMode(); + enterEditMode(); + break; + case R.id.action_edit_confirm: + confirmEdit(); break; case R.id.action_edit_cancel: - cancelEditMode(); + cancelEdit(); break; default: return activity.get().toolbarOnItemClicked(menuItem); @@ -929,19 +833,18 @@ public void onAppUpdated(AppUpdatedEvent event) { @Subscribe(threadMode = ThreadMode.MAIN) public void onActivityEvent(CommunicationEvent event) { - if (event.getRecipient() != RC_CONTENTS || null == sortDirectionButton) return; + if (event.getRecipient() != RC_CONTENTS) return; switch (event.getType()) { + case EV_UPDATE_TOOLBAR: + addCustomBackControl(); + activity.get().initFragmentToolbars(selectExtension, this::onToolbarItemClicked, this::onSelectionToolbarItemClicked); + break; case EV_SEARCH: if (event.getMessage() != null) onSubmitSearch(event.getMessage()); break; case EV_ADVANCED_SEARCH: onAdvancedSearchButtonClick(); break; - case EV_UPDATE_SORT: - updateSortControls(); - addCustomBackControl(); - activity.get().initFragmentToolbars(selectExtension, this::onToolbarItemClicked, this::onSelectionToolbarItemClicked); - break; case EV_ENABLE: onEnable(); break; @@ -981,11 +884,8 @@ private void customBackPress() { new Handler(Looper.getMainLooper()).postDelayed(() -> activity.get().goBackToGroups(), 100); } // If none of the above and a search filter is on => clear search filter - else if (isSearchQueryActive()) { - setQuery(""); - setMetadata(Collections.emptyList()); - activity.get().hideSearchSortBar(false); - viewModel.searchContent(getQuery(), getMetadata()); + else if (activity.get().isFilterActive()) { + viewModel.clearContentFilters(); } // If none of the above, user is asking to leave => use double-tap else if (backButtonPressed + 2000 > SystemClock.elapsedRealtime()) { @@ -1006,10 +906,13 @@ else if (backButtonPressed + 2000 > SystemClock.elapsedRealtime()) { private void onSharedPreferenceChanged(String key) { Timber.i("Prefs change detected : %s", key); switch (key) { + case Preferences.Key.TOP_FAB: + topFab.setVisibility(Preferences.isTopFabEnabled() ? View.VISIBLE : View.GONE); + break; case Preferences.Key.ENDLESS_SCROLL: setPagingMethod(Preferences.getEndlessScroll(), activity.get().isEditMode()); FirebaseCrashlytics.getInstance().setCustomKey("Library display mode", Preferences.getEndlessScroll() ? "endless" : "paged"); - viewModel.updateContentOrder(); // Trigger a blank search + viewModel.searchContent(); // Trigger a blank search break; default: // Nothing to handle there @@ -1036,13 +939,13 @@ else if (s.equals(Site.NONE)) private void onAdvancedSearchButtonClick() { Intent search = new Intent(this.getContext(), SearchActivity.class); - SearchActivityBundle.Builder builder = new SearchActivityBundle.Builder(); + SearchActivityBundle builder = new SearchActivityBundle(); if (!getMetadata().isEmpty()) - builder.setUri(SearchActivityBundle.Builder.buildSearchUri(getMetadata())); + builder.setUri(SearchActivityBundle.Companion.buildSearchUri(getMetadata()).toString()); if (group != null) - builder.setGroup(group.id); + builder.setGroupId(group.id); builder.setExcludeMode(excludeClicked); search.putExtras(builder.getBundle()); @@ -1057,13 +960,13 @@ private void onAdvancedSearchButtonClick() { private void advancedSearchReturnResult(final ActivityResult result) { if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null && result.getData().getExtras() != null) { - SearchActivityBundle.Parser parser = new SearchActivityBundle.Parser(result.getData().getExtras()); - Uri searchUri = parser.getUri(); + SearchActivityBundle parser = new SearchActivityBundle(result.getData().getExtras()); + Uri searchUri = Uri.parse(parser.getUri()); if (searchUri != null) { excludeClicked = parser.getExcludeMode(); setQuery(searchUri.getPath()); - setMetadata(SearchActivityBundle.Parser.parseSearchUri(searchUri)); + setMetadata(SearchActivityBundle.Companion.parseSearchUri(searchUri)); viewModel.searchContent(getQuery(), getMetadata()); } } @@ -1076,7 +979,7 @@ private void advancedSearchReturnResult(final ActivityResult result) { */ private void setPagingMethod(boolean isEndless, boolean isEditMode) { // Editing will always be done in Endless mode - viewModel.setPagingMethod(isEndless || isEditMode); + viewModel.setContentPagingMethod(isEndless || isEditMode); // RecyclerView horizontal centering ViewGroup.LayoutParams layoutParams = recyclerView.getLayoutParams(); @@ -1324,7 +1227,7 @@ private void onNewSearch(Boolean b) { */ private void onLibraryChanged(PagedList result) { Timber.i(">> Library changed ! Size=%s", result.size()); - if (!enabled) return; + if (!enabled && !Preferences.getGroupingDisplay().equals(Grouping.FLAT)) return; activity.get().updateTitle(result.size(), totalContentCount); @@ -1423,7 +1326,7 @@ private boolean onItemClick(int position, @NonNull ContentItem item) { // TODO doc public void readBook(@NonNull Content content, boolean forceShowGallery) { topItemPosition = getTopItemPosition(); - ContentHelper.openHentoidViewer(requireContext(), content, -1, viewModel.getSearchManagerBundle(), forceShowGallery); + ContentHelper.openHentoidViewer(requireContext(), content, -1, contentSearchBundle, forceShowGallery); } /** @@ -1561,7 +1464,7 @@ public void onChangeGroupSuccess() { */ private void refreshIfNeeded() { if (Grouping.CUSTOM.equals(Preferences.getGroupingDisplay()) || Preferences.getContentSortField() == Preferences.Constant.ORDER_FIELD_CUSTOM) - viewModel.updateContentOrder(); + viewModel.searchContent(); } /** @@ -1569,6 +1472,7 @@ private void refreshIfNeeded() { * Activated when all _adapter_ items are placed on their definitive position */ private void differEndCallback() { + Timber.v(">> differEndCallback"); if (topItemPosition > -1) { int targetPos = topItemPosition; listRefreshDebouncer.submit(targetPos); @@ -1654,4 +1558,18 @@ private void onDeleteSwipedBook(@NonNull final ContentItem item) { if (content != null) viewModel.deleteItems(Stream.of(content).toList(), Collections.emptyList(), false, null); } + + /** + * Scroll / page change listener + * + * @param scrollPosition New 0-based scroll position + */ + private void onScrollPositionChange(int scrollPosition) { + if (Preferences.isTopFabEnabled()) { + if (scrollPosition > 2) + topFab.setVisibility(View.VISIBLE); + else + topFab.setVisibility(View.GONE); + } + } } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.java index da9bcdb022..29c7a5be1f 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.java @@ -4,7 +4,7 @@ import static me.devsaki.hentoid.events.CommunicationEvent.EV_DISABLE; import static me.devsaki.hentoid.events.CommunicationEvent.EV_ENABLE; import static me.devsaki.hentoid.events.CommunicationEvent.EV_SEARCH; -import static me.devsaki.hentoid.events.CommunicationEvent.EV_UPDATE_SORT; +import static me.devsaki.hentoid.events.CommunicationEvent.EV_UPDATE_TOOLBAR; import static me.devsaki.hentoid.events.CommunicationEvent.RC_GROUPS; import android.annotation.SuppressLint; @@ -17,14 +17,11 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.TextView; import androidx.activity.OnBackPressedCallback; -import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.widget.PopupMenu; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; @@ -111,12 +108,6 @@ public class LibraryGroupsFragment extends Fragment implements ItemTouchCallback // LayoutManager of the recyclerView private LinearLayoutManager llm; - // === SORT TOOLBAR - // Sort direction button - private ImageView sortDirectionButton; - // Sort field button - private TextView sortFieldButton; - // === FASTADAPTER COMPONENTS AND HELPERS private ItemAdapter itemAdapter; private FastAdapter fastAdapter; @@ -132,7 +123,7 @@ public class LibraryGroupsFragment extends Fragment implements ItemTouchCallback // TODO doc private boolean firstLibraryLoad = true; // TODO doc - private boolean enabled = true; + private boolean enabled = false; public static final DiffCallback GROUPITEM_DIFF_CALLBACK = new DiffCallback() { @@ -150,7 +141,7 @@ public boolean areContentsTheSame(GroupDisplayItem oldItem, GroupDisplayItem new @Override public @org.jetbrains.annotations.Nullable Object getChangePayload(GroupDisplayItem oldItem, int oldPos, GroupDisplayItem newItem, int newPos) { - GroupItemBundle.Builder diffBundleBuilder = new GroupItemBundle.Builder(); + GroupItemBundle diffBundleBuilder = new GroupItemBundle(); if (!newItem.getGroup().coverContent.isNull() && oldItem.getGroup().coverContent.getTargetId() != newItem.getGroup().coverContent.getTargetId()) { diffBundleBuilder.setCoverUri(newItem.getGroup().coverContent.getTarget().getCover().getUsableUri()); @@ -205,12 +196,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat // Trigger a blank search // TODO when group is reached from FLAT through the "group by" menu, this triggers a double-load and a screen blink - viewModel.setGrouping( - Preferences.getGroupingDisplay(), - Preferences.getGroupSortField(), - Preferences.isGroupSortDesc(), - Preferences.getArtistGroupVisibility(), - activity.get().isGroupFavsChecked()); + viewModel.searchGroup(); } public void onEnable() { @@ -230,8 +216,6 @@ public void onDisable() { */ private void initUI(@NonNull View rootView) { emptyText = requireViewById(rootView, R.id.library_empty_txt); - sortDirectionButton = activity.get().getSortDirectionButton(); - sortFieldButton = activity.get().getSortFieldButton(); // RecyclerView recyclerView = requireViewById(rootView, R.id.library_list); @@ -245,7 +229,6 @@ private void initUI(@NonNull View rootView) { // Pager setPagingMethod(); - updateSortControls(); addCustomBackControl(); } @@ -260,78 +243,16 @@ public void handleOnBackPressed() { activity.get().getOnBackPressedDispatcher().addCallback(activity.get(), callback); } - private void updateSortControls() { - // Sort controls - sortDirectionButton.setImageResource(Preferences.isGroupSortDesc() ? R.drawable.ic_simple_arrow_down : R.drawable.ic_simple_arrow_up); - sortDirectionButton.setOnClickListener(v -> { - boolean sortDesc = !Preferences.isGroupSortDesc(); - Preferences.setGroupSortDesc(sortDesc); - // Update icon - sortDirectionButton.setImageResource(sortDesc ? R.drawable.ic_simple_arrow_down : R.drawable.ic_simple_arrow_up); - // Run a new search - viewModel.searchGroup(Preferences.getGroupingDisplay(), activity.get().getQuery(), Preferences.getGroupSortField(), sortDesc, Preferences.getArtistGroupVisibility(), activity.get().isGroupFavsChecked()); - activity.get().sortCommandsAutoHide(true, null); - }); - sortFieldButton.setText(getNameFromFieldCode(Preferences.getGroupSortField())); - sortFieldButton.setOnClickListener(v -> { - // Load and display the field popup menu - PopupMenu popup = new PopupMenu(requireContext(), sortDirectionButton); - popup.getMenuInflater() - .inflate(R.menu.library_groups_sort_popup, popup.getMenu()); - popup.getMenu().findItem(R.id.sort_custom).setVisible(Preferences.getGroupingDisplay().canReorderGroups()); - popup.setOnMenuItemClickListener(item -> { - // Update button text - sortFieldButton.setText(item.getTitle()); - item.setChecked(true); - int fieldCode = getFieldCodeFromMenuId(item.getItemId()); - Preferences.setGroupSortField(fieldCode); - // Run a new search - viewModel.searchGroup(Preferences.getGroupingDisplay(), activity.get().getQuery(), fieldCode, Preferences.isGroupSortDesc(), Preferences.getArtistGroupVisibility(), activity.get().isGroupFavsChecked()); - activity.get().sortCommandsAutoHide(true, popup); - return true; - }); - popup.show(); //showing popup menu - activity.get().sortCommandsAutoHide(true, popup); - }); //closing the setOnClickListener method - } - - private int getFieldCodeFromMenuId(@IdRes int menuId) { - switch (menuId) { - case (R.id.sort_title): - return Preferences.Constant.ORDER_FIELD_TITLE; - case (R.id.sort_books): - return Preferences.Constant.ORDER_FIELD_CHILDREN; - case (R.id.sort_dl_date): - return Preferences.Constant.ORDER_FIELD_DOWNLOAD_DATE; - case (R.id.sort_custom): - return Preferences.Constant.ORDER_FIELD_CUSTOM; - default: - return Preferences.Constant.ORDER_FIELD_NONE; - } - } - - private int getNameFromFieldCode(int prefFieldCode) { - switch (prefFieldCode) { - case (Preferences.Constant.ORDER_FIELD_TITLE): - return R.string.sort_title; - case (Preferences.Constant.ORDER_FIELD_CHILDREN): - return R.string.sort_books; - case (Preferences.Constant.ORDER_FIELD_DOWNLOAD_DATE): - return R.string.sort_dl_date; - case (Preferences.Constant.ORDER_FIELD_CUSTOM): - return R.string.sort_custom; - default: - return R.string.sort_invalid; - } - } - private boolean onToolbarItemClicked(@NonNull MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.action_edit: - toggleEditMode(); + enterEditMode(); + break; + case R.id.action_edit_confirm: + confirmEdit(); break; case R.id.action_edit_cancel: - cancelEditMode(); + cancelEdit(); break; case R.id.action_group_new: newGroupPrompt(); @@ -369,25 +290,29 @@ private boolean onSelectionToolbarItemClicked(@NonNull MenuItem menuItem) { return true; } - private void toggleEditMode() { - activity.get().toggleEditMode(); - - // Leave edit mode by validating => Save new item position - if (!activity.get().isEditMode()) { - // Set ordering field to custom - Preferences.setGroupSortField(Preferences.Constant.ORDER_FIELD_CUSTOM); - sortFieldButton.setText(getNameFromFieldCode(Preferences.Constant.ORDER_FIELD_CUSTOM)); - // Set ordering direction to ASC (we just manually ordered stuff; it has to be displayed as is) - Preferences.setGroupSortDesc(false); - viewModel.saveGroupPositions(Stream.of(itemAdapter.getAdapterItems()).map(GroupDisplayItem::getGroup).withoutNulls().toList()); - } + private void enterEditMode() { + activity.get().setEditMode(true); + setPagingMethod(); + viewModel.searchGroup(); + } + private void cancelEdit() { + activity.get().setEditMode(false); setPagingMethod(); } - private void cancelEditMode() { + private void confirmEdit() { activity.get().setEditMode(false); + + // == Save new item position + // Set ordering field to custom + Preferences.setGroupSortField(Preferences.Constant.ORDER_FIELD_CUSTOM); + // Set ordering direction to ASC (we just manually ordered stuff; it has to be displayed as is) + Preferences.setGroupSortDesc(false); + viewModel.saveGroupPositions(Stream.of(itemAdapter.getAdapterItems()).map(GroupDisplayItem::getGroup).withoutNulls().toList()); + setPagingMethod(); + viewModel.searchGroup(); } private void newGroupPrompt() { @@ -488,7 +413,7 @@ private void deleteSelectedItems() { PrefsBundle prefsBundle = new PrefsBundle(); prefsBundle.setStoragePrefs(true); - intent.putExtras(prefsBundle.toBundle()); + intent.putExtras(prefsBundle.getBundle()); requireContext().startActivity(intent); }); @@ -571,14 +496,13 @@ public void onAppUpdated(AppUpdatedEvent event) { public void onActivityEvent(CommunicationEvent event) { if (event.getRecipient() != RC_GROUPS) return; switch (event.getType()) { - case EV_SEARCH: - if (event.getMessage() != null) onSubmitSearch(event.getMessage()); - break; - case EV_UPDATE_SORT: - updateSortControls(); + case EV_UPDATE_TOOLBAR: addCustomBackControl(); activity.get().initFragmentToolbars(selectExtension, this::onToolbarItemClicked, this::onSelectionToolbarItemClicked); break; + case EV_SEARCH: + if (event.getMessage() != null) onSubmitSearch(event.getMessage()); + break; case EV_ENABLE: onEnable(); break; @@ -608,14 +532,11 @@ private void customBackPress() { if (!activity.get().collapseSearchMenu() && !activity.get().closeLeftDrawer()) { // If none of the above and a search filter is on => clear search filter - if (activity.get().isSearchQueryActive()) { - activity.get().setQuery(""); - activity.get().setMetadata(Collections.emptyList()); - activity.get().hideSearchSortBar(false); - viewModel.searchContent(activity.get().getQuery(), activity.get().getMetadata()); + if (activity.get().isFilterActive()) { + viewModel.clearGroupFilters(); } // If none of the above, user is asking to leave => use double-tap - if (backButtonPressed + 2000 > SystemClock.elapsedRealtime()) { + else if (backButtonPressed + 2000 > SystemClock.elapsedRealtime()) { callback.remove(); requireActivity().onBackPressed(); } else { @@ -630,9 +551,7 @@ private void customBackPress() { /** * Initialize the paging method of the screen */ - private void setPagingMethod(/*boolean isEditMode*/) { - viewModel.setPagingMethod(true); - + private void setPagingMethod() { itemAdapter = new ItemAdapter<>(); fastAdapter = FastAdapter.with(itemAdapter); if (!fastAdapter.hasObservers()) fastAdapter.setHasStableIds(true); @@ -708,6 +627,9 @@ private void onGroupsChanged(List result) { List groups = Stream.of(result).map(g -> new GroupDisplayItem(g, touchHelper, viewType)).withoutNulls().distinct().toList(); FastAdapterDiffUtil.INSTANCE.set(itemAdapter, groups, GROUPITEM_DIFF_CALLBACK); + // Update visibility of search bar + activity.get().updateSearchBarOnResults(!result.isEmpty()); + // Reset library load indicator firstLibraryLoad = true; } @@ -736,12 +658,7 @@ private void onLibraryChanged(PagedList result) { // Refresh groups (new content -> updated book count or new groups) // TODO do we really want to do that, especially when deleting content ? if (!firstLibraryLoad) - viewModel.setGrouping( - Preferences.getGroupingDisplay(), - Preferences.getGroupSortField(), - Preferences.isGroupSortDesc(), - Preferences.getArtistGroupVisibility(), - activity.get().isGroupFavsChecked()); + viewModel.searchGroup(); else { Timber.i(">>Library changed (groups) : ignored"); firstLibraryLoad = false; @@ -759,12 +676,7 @@ else if (s.equals(Site.NONE)) else ContentHelper.launchBrowserFor(requireContext(), query); } else { - viewModel.searchGroup( - Preferences.getGroupingDisplay(), - query, Preferences.getGroupSortField(), - Preferences.isGroupSortDesc(), - Preferences.getArtistGroupVisibility(), - activity.get().isGroupFavsChecked()); + viewModel.setGroupQuery(query); } } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/queue/QueueFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/queue/QueueFragment.java index 6fee465e16..2106f4f315 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/queue/QueueFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/queue/QueueFragment.java @@ -967,7 +967,7 @@ private void onSettingsClick() { PrefsBundle prefsBundle = new PrefsBundle(); prefsBundle.setDownloaderPrefs(true); - intent.putExtras(prefsBundle.toBundle()); + intent.putExtras(prefsBundle.getBundle()); requireContext().startActivity(intent); } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/tools/DuplicateDetailsFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/tools/DuplicateDetailsFragment.kt index 8a2dd53f5f..118b11bfb9 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/tools/DuplicateDetailsFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/tools/DuplicateDetailsFragment.kt @@ -82,12 +82,12 @@ class DuplicateDetailsFragment : Fragment(R.layout.fragment_duplicate_details), newItem: DuplicateItem, newItemPosition: Int ): Any? { - val diffBundleBuilder = DuplicateItemBundle.Builder() + val diffBundleBuilder = DuplicateItemBundle() if (oldItem.keep != newItem.keep) { - diffBundleBuilder.setKeep(newItem.keep) + diffBundleBuilder.isKeep = newItem.keep } if (oldItem.isBeingDeleted != newItem.isBeingDeleted) { - diffBundleBuilder.setIsBeingDeleted(newItem.isBeingDeleted) + diffBundleBuilder.isBeingDeleted = newItem.isBeingDeleted } return if (diffBundleBuilder.isEmpty) null else diffBundleBuilder.bundle } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerBottomContentFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerBottomContentFragment.java index 51394af45f..7a07fcb1ca 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerBottomContentFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerBottomContentFragment.java @@ -30,7 +30,7 @@ import me.devsaki.hentoid.core.HentoidApp; import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.database.domains.ImageFile; -import me.devsaki.hentoid.databinding.IncludeViewerContentInfoBinding; +import me.devsaki.hentoid.databinding.IncludeViewerContentBottomPanelBinding; import me.devsaki.hentoid.util.ContentHelper; import me.devsaki.hentoid.util.ThemeHelper; import me.devsaki.hentoid.viewmodels.ImageViewerViewModel; @@ -43,7 +43,7 @@ public class ViewerBottomContentFragment extends BottomSheetDialogFragment { private ImageViewerViewModel viewModel; // UI - private IncludeViewerContentInfoBinding binding = null; + private IncludeViewerContentBottomPanelBinding binding = null; static { @@ -76,7 +76,7 @@ public void onAttach(@NonNull Context context) { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = IncludeViewerContentInfoBinding.inflate(inflater, container, false); + binding = IncludeViewerContentBottomPanelBinding.inflate(inflater, container, false); return binding.getRoot(); } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerBottomImageFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerBottomImageFragment.java index e3fcca2195..20cc2cea8c 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerBottomImageFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerBottomImageFragment.java @@ -58,10 +58,6 @@ public class ViewerBottomImageFragment extends BottomSheetDialogFragment { private ImageViewerViewModel viewModel; - private int imageIndex = -1; - private float scale = -1; - private ImageFile image = null; - // UI private View rootView; private ImageView imgThumb; @@ -73,6 +69,11 @@ public class ViewerBottomImageFragment extends BottomSheetDialogFragment { private ImageView shareButton; private ImageView deleteButton; + // Variables + private int imageIndex = -1; + private float scale = -1; + private ImageFile image = null; + static { Context context = HentoidApp.getInstance(); @@ -89,7 +90,7 @@ public class ViewerBottomImageFragment extends BottomSheetDialogFragment { } public static void invoke(Context context, FragmentManager fragmentManager, int imageIndex, float currentScale) { - ImageViewerActivityBundle.Builder builder = new ImageViewerActivityBundle.Builder(); + ImageViewerActivityBundle builder = new ImageViewerActivityBundle(); builder.setImageIndex(imageIndex); builder.setScale(currentScale); @@ -106,7 +107,7 @@ public void onAttach(@NonNull Context context) { Bundle bundle = getArguments(); if (bundle != null) { - ImageViewerActivityBundle.Parser parser = new ImageViewerActivityBundle.Parser(bundle); + ImageViewerActivityBundle parser = new ImageViewerActivityBundle(bundle); imageIndex = parser.getImageIndex(); if (-1 == imageIndex) throw new IllegalArgumentException("Initialization failed"); scale = parser.getScale(); @@ -118,7 +119,7 @@ public void onAttach(@NonNull Context context) { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - rootView = inflater.inflate(R.layout.include_viewer_image_info, container, false); + rootView = inflater.inflate(R.layout.include_viewer_image_bottom_panel, container, false); imgThumb = requireViewById(rootView, R.id.ivThumb); imgPath = requireViewById(rootView, R.id.image_path); @@ -263,7 +264,7 @@ private void onCopyClick() { private void onShareClick() { Uri fileUri = Uri.parse(image.getFileUri()); if (FileHelper.fileExists(requireContext(), fileUri)) - FileHelper.shareFile(requireContext(), fileUri, "Share picture"); + FileHelper.shareFile(requireContext(), fileUri, ""); } /** diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerGalleryFragment.java b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerGalleryFragment.java index e8a7ec7d64..cae4cffea5 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerGalleryFragment.java +++ b/app/src/main/java/me/devsaki/hentoid/fragments/viewer/ViewerGalleryFragment.java @@ -144,10 +144,10 @@ public boolean areContentsTheSame(ImageFileItem oldItem, ImageFileItem newItem) if (null == oldImage || null == newImage) return false; - ImageItemBundle.Builder diffBundleBuilder = new ImageItemBundle.Builder(); + ImageItemBundle diffBundleBuilder = new ImageItemBundle(); if (oldImage.isFavourite() != newImage.isFavourite()) { - diffBundleBuilder.setIsFavourite(newImage.isFavourite()); + diffBundleBuilder.setFavourite(newImage.isFavourite()); } if (oldItem.getChapterOrder() != newItem.getChapterOrder()) { diffBundleBuilder.setChapterOrder(newItem.getChapterOrder()); 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 014ea143a0..d3646b43ef 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 @@ -122,7 +122,7 @@ public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstance PrefsBundle prefsBundle = new PrefsBundle(); prefsBundle.setViewerPrefs(true); - intent.putExtras(prefsBundle.toBundle()); + intent.putExtras(prefsBundle.getBundle()); requireContext().startActivity(intent); }); diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleriesMetadata.java b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleriesMetadata.java index 556d3c6414..b9adf99200 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleriesMetadata.java +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleriesMetadata.java @@ -27,6 +27,7 @@ public static class EHentaiGalleryMetadata { private String gid; private String token; + private String posted; private String title; private String thumb; private String filecount; @@ -41,6 +42,8 @@ public Content update(@NonNull Content content, @Nonnull String url, @NonNull Si .setTitle(title) .setStatus(StatusContent.SAVED); + if (!posted.isEmpty()) content.setUploadDate(Long.parseLong(posted) * 1000); + if (updatePages) { if (filecount != null) content.setQtyPages(Integer.parseInt(filecount)); else content.setQtyPages(0); diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.java b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.java index 26bb01ed1c..d779faf637 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.java +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.java @@ -1,6 +1,6 @@ package me.devsaki.hentoid.json.sources; -@SuppressWarnings({"unused, MismatchedQueryAndUpdateOfCollection", "squid:S1172", "squid:S1068"}) +@SuppressWarnings({"unused, MismatchedQueryAndUpdateOfCollection", "squid:S1172", "squid:S1068", "FieldCanBeLocal"}) public class EHentaiImageQuery { private final String method = "imagedispatch"; private final Integer gid; diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/HitomiGalleryInfo.java b/app/src/main/java/me/devsaki/hentoid/json/sources/HitomiGalleryInfo.java index 3764f12eac..525002886b 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/HitomiGalleryInfo.java +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/HitomiGalleryInfo.java @@ -9,6 +9,7 @@ import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; +import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.StringHelper; @SuppressWarnings({"unused, MismatchedQueryAndUpdateOfCollection", "squid:S1172", "squid:S1068"}) @@ -19,7 +20,7 @@ public class HitomiGalleryInfo { private String title; private List characters; private List groups; - // private Date date; TODO + private String date; // Format : "YYYY-MM-DD HH:MM:SS-05" (-05 being the timezone of the server ?) private String language; private String language_localname; private String language_url; @@ -67,7 +68,8 @@ private void addAttribute(@NonNull AttributeType attributeType, @NonNull String public void updateContent(@NonNull Content content) { content.setTitle(StringHelper.removeNonPrintableChars(title)); -// content.setUploadDate(date.getTime()); TODO + + content.setUploadDate(Helper.parseDatetimeToEpoch(date, "yyyy-MM-dd HH:mm:ssx")); AttributeMap attributes = new AttributeMap(); if (parodys != null) diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/LusciousBookMetadata.java b/app/src/main/java/me/devsaki/hentoid/json/sources/LusciousBookMetadata.java index f3744a6f5b..43a52668d4 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/LusciousBookMetadata.java +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/LusciousBookMetadata.java @@ -29,6 +29,7 @@ private static class AlbumInfo { private String id; private String title; private String url; + private String created; private Integer number_of_pictures; private CoverInfo cover; private LanguageInfo language; @@ -59,6 +60,7 @@ public Content update(@NonNull Content content, boolean updateImages) { return content.setStatus(StatusContent.IGNORED); content.setUrl(info.url); + if (!info.created.isEmpty()) content.setUploadDate(Long.parseLong(info.created) * 1000); content.setTitle(StringHelper.removeNonPrintableChars(info.title)); diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/PixivIllustMetadata.java b/app/src/main/java/me/devsaki/hentoid/json/sources/PixivIllustMetadata.java index 2a22b29e17..4c5e13201a 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/PixivIllustMetadata.java +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/PixivIllustMetadata.java @@ -303,7 +303,7 @@ public Content update(@NonNull final Content content, @Nonnull String url, boole content.setUrl(urlValue.replace(Site.PIXIV.getUrl(), "")); content.setCoverImageUrl(illustData.getThumbUrl()); - content.setUploadDate(illustData.getUploadTimestamp()); + content.setUploadDate(illustData.getUploadTimestamp() * 1000); content.putAttributes(getAttributes()); diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/YoastGalleryMetadata.java b/app/src/main/java/me/devsaki/hentoid/json/sources/YoastGalleryMetadata.java new file mode 100644 index 0000000000..9824eac854 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/YoastGalleryMetadata.java @@ -0,0 +1,28 @@ +package me.devsaki.hentoid.json.sources; + +import com.squareup.moshi.Json; + +import java.util.List; + +@SuppressWarnings({"unused, MismatchedQueryAndUpdateOfCollection", "squid:S1172", "squid:S1068"}) +public class YoastGalleryMetadata { + private @Json(name = "@graph") + List graph; + + private static class GraphData { + private @Json(name = "@type") + String type; + private String datePublished; + } + + public String getDatePublished() { + if (graph != null) { + for (GraphData data : graph) { + if (data.type != null && data.type.equalsIgnoreCase("webpage")) { + return data.datePublished; + } + } + } + return ""; + } +} diff --git a/app/src/main/java/me/devsaki/hentoid/notification/action/UserActionNotification.kt b/app/src/main/java/me/devsaki/hentoid/notification/action/UserActionNotification.kt index 330109ffce..28ad75a79d 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/action/UserActionNotification.kt +++ b/app/src/main/java/me/devsaki/hentoid/notification/action/UserActionNotification.kt @@ -29,9 +29,9 @@ class UserActionNotification(val site: Site, private val oldCookie: String) : No val resultIntent = Intent(context, QueueActivity::class.java) resultIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or FLAG_ACTIVITY_NEW_TASK// or Intent.FLAG_ACTIVITY_SINGLE_TOP - val bundleBuilder = QueueActivityBundle.Builder() - bundleBuilder.setReviveDownload(site) - bundleBuilder.setReviveOldCookie(oldCookie) + val bundleBuilder = QueueActivityBundle() + bundleBuilder.reviveDownloadForSiteCode = site.code + bundleBuilder.reviveOldCookie = oldCookie resultIntent.putExtras(bundleBuilder.bundle) val flags = 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 4b43248708..bb2a012c26 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/ParseHelper.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/ParseHelper.java @@ -9,6 +9,8 @@ import com.annimon.stream.Optional; import com.annimon.stream.Stream; +import org.apache.commons.lang3.tuple.ImmutableTriple; +import org.apache.commons.lang3.tuple.Triple; import org.greenrobot.eventbus.EventBus; import org.jsoup.nodes.Element; @@ -33,6 +35,7 @@ import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.events.DownloadPreparationEvent; import me.devsaki.hentoid.util.ContentHelper; +import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.StringHelper; import me.devsaki.hentoid.util.network.HttpHelper; @@ -373,28 +376,45 @@ public static String getExtensionFromFormat(@NonNull Map imgForm * @return Chapters detected from the given list of links, associated with the given Content ID */ public static List getChaptersFromLinks(@NonNull List chapterLinks, long contentId) { + return getChaptersFromLinks(chapterLinks, contentId, null, null); + } + + public static List getChaptersFromLinks( + @NonNull List chapterLinks, + long contentId, + String dateCssQuery, + String datePattern) { List result = new ArrayList<>(); Set urls = new HashSet<>(); // First extract data and filter URL duplicates - List> chapterData = new ArrayList<>(); + List> chapterData = new ArrayList<>(); for (Element e : chapterLinks) { String url = e.attr("href").trim(); String name = e.attr("title").trim(); if (name.isEmpty()) name = StringHelper.removeNonPrintableChars(e.ownText()).trim(); + long epoch = 0; + if (dateCssQuery != null && !dateCssQuery.isEmpty()) { + Element dateElement = e.selectFirst(dateCssQuery); + if (dateElement != null) { + String[] dateStr = dateElement.text().split("-"); + if (dateStr.length > 1) epoch = Helper.parseDateToEpoch(dateStr[1], datePattern); + } + } // Make sure we're not adding duplicates if (!urls.contains(url)) { urls.add(url); - chapterData.add(new Pair<>(url, name)); + chapterData.add(new ImmutableTriple<>(url, name, epoch)); } } Collections.reverse(chapterData); // Put unique results in their chronological order int order = 0; // Build the final list - for (Pair chapter : chapterData) { - Chapter chp = new Chapter(order++, chapter.first, chapter.second); + for (Triple chapter : chapterData) { + Chapter chp = new Chapter(order++, chapter.getLeft(), chapter.getMiddle()); + chp.setUploadDate(chapter.getRight()); chp.setContentId(contentId); result.add(chp); } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/AllPornComicContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/AllPornComicContent.java index afbe499acf..3c8797ba8f 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/AllPornComicContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/AllPornComicContent.java @@ -4,6 +4,7 @@ import org.jsoup.nodes.Element; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -14,15 +15,21 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; +import me.devsaki.hentoid.json.sources.YoastGalleryMetadata; import me.devsaki.hentoid.parsers.ParseHelper; +import me.devsaki.hentoid.util.Helper; +import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.StringHelper; import pl.droidsonroids.jspoon.annotation.Selector; +import timber.log.Timber; public class AllPornComicContent extends BaseContentParser { @Selector(value = "head [property=og:image]", attr = "content", defValue = "") private String coverUrl; @Selector(value = "head [property=og:title]", attr = "content", defValue = "") private String title; + @Selector(value = "head script.yoast-schema-graph") + private Element metadata; @Selector(value = ".post-content a[href*='characters']") private List characterTags; @@ -45,6 +52,17 @@ public Content update(@NonNull final Content content, @Nonnull String url, boole content.setTitle(StringHelper.removeNonPrintableChars(title)); } else content.setTitle(NO_TITLE); + if (metadata != null && metadata.childNodeSize() > 0) { + try { + YoastGalleryMetadata galleryMeta = JsonHelper.jsonToObject(metadata.childNode(0).toString(), YoastGalleryMetadata.class); + String publishDate = galleryMeta.getDatePublished(); // e.g. 2021-01-27T15:20:38+00:00 + if (!publishDate.isEmpty()) + content.setUploadDate(Helper.parseDatetimeToEpoch(publishDate, "yyyy-MM-dd'T'HH:mm:ssXXX")); + } catch (IOException e) { + Timber.i(e); + } + } + AttributeMap attributes = new AttributeMap(); ParseHelper.parseAttributes(attributes, AttributeType.CHARACTER, characterTags, false, Site.ALLPORNCOMIC); ParseHelper.parseAttributes(attributes, AttributeType.SERIE, seriesTags, false, Site.ALLPORNCOMIC); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/DoujinsContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/DoujinsContent.java index b82a645d33..8373f71952 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/DoujinsContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/DoujinsContent.java @@ -16,6 +16,7 @@ import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.ParseHelper; import me.devsaki.hentoid.parsers.images.DoujinsParser; +import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.StringHelper; import pl.droidsonroids.jspoon.annotation.Selector; @@ -28,6 +29,8 @@ public class DoujinsContent extends BaseContentParser { private List artists; @Selector(value = "a[href*='/searches?tag_id=']") // To deduplicate private List tags; + @Selector(value = "#content .folder-message") + private List contentInfo; public Content update(@NonNull final Content content, @Nonnull String url, boolean updateImages) { @@ -41,6 +44,16 @@ public Content update(@NonNull final Content content, @Nonnull String url, boole content.setTitle(StringHelper.removeNonPrintableChars(e.text())); } + if (contentInfo != null && !contentInfo.isEmpty()) { + for (Element e : contentInfo) { + if (e.text().toLowerCase().contains("•")) { // e.g. March 16th, 2022 • 25 images + String[] parts = e.text().split("•"); + content.setUploadDate(Helper.parseDateToEpoch(parts[0], "MMMM dd',' yyyy")); + break; + } + } + } + if (images != null && !images.isEmpty()) { // Cover = thumb from the 1st page String coverUrl = images.get(0).attr("data-thumb2"); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/ManhwaContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/ManhwaContent.java index 447bf1e1d9..a3bc646bb7 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/ManhwaContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/ManhwaContent.java @@ -4,6 +4,7 @@ import org.jsoup.nodes.Element; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -14,15 +15,21 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; +import me.devsaki.hentoid.json.sources.YoastGalleryMetadata; import me.devsaki.hentoid.parsers.ParseHelper; +import me.devsaki.hentoid.util.Helper; +import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.StringHelper; import pl.droidsonroids.jspoon.annotation.Selector; +import timber.log.Timber; public class ManhwaContent extends BaseContentParser { @Selector(value = "head [property=og:image]", attr = "content") private String coverUrl; @Selector(value = ".breadcrumb a") private List breadcrumbs; + @Selector(value = "head script.yoast-schema-graph") + private Element metadata; @Selector(value = ".author-content a") private List author; @Selector(value = ".artist-content a") @@ -42,6 +49,17 @@ public Content update(@NonNull final Content content, @Nonnull String url, boole content.setTitle(title); content.populateUniqueSiteId(); + if (metadata != null && metadata.childNodeSize() > 0) { + try { + YoastGalleryMetadata galleryMeta = JsonHelper.jsonToObject(metadata.childNode(0).toString(), YoastGalleryMetadata.class); + String publishDate = galleryMeta.getDatePublished(); // e.g. 2021-01-27T15:20:38+00:00 + if (!publishDate.isEmpty()) + content.setUploadDate(Helper.parseDatetimeToEpoch(publishDate, "yyyy-MM-dd'T'HH:mm:ssXXX")); + } catch (IOException e) { + Timber.i(e); + } + } + AttributeMap attributes = new AttributeMap(); ParseHelper.parseAttributes(attributes, AttributeType.ARTIST, artist, false, Site.MANHWA); ParseHelper.parseAttributes(attributes, AttributeType.ARTIST, author, false, Site.MANHWA); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/MrmContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/MrmContent.java index fed8ace185..6800abfc52 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/MrmContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/MrmContent.java @@ -16,12 +16,15 @@ import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.ParseHelper; +import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.StringHelper; import pl.droidsonroids.jspoon.annotation.Selector; public class MrmContent extends BaseContentParser { @Selector(value = "article h1", defValue = "") private String title; + @Selector(value = "time.entry-time", attr = "datetime", defValue = "") + private String uploadDate; @Selector(".entry-header .entry-meta .entry-categories a") private List categories; @Selector(value = ".entry-header .entry-terms a[href*='/lang/']") @@ -44,6 +47,8 @@ public Content update(@NonNull final Content content, @Nonnull String url, boole content.setTitle(title); } else content.setTitle(NO_TITLE); + content.setUploadDate(Helper.parseDatetimeToEpoch(uploadDate,"yyyy-MM-dd'T'HH:mm:ssXXX")); // e.g. 2022-03-20T00:09:43+07:00 + if (images != null && !images.isEmpty()) content.setCoverImageUrl(ParseHelper.getImgSrc(images.get(0))); 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 d0679b4bce..52a090f277 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 @@ -16,6 +16,7 @@ import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.ParseHelper; import me.devsaki.hentoid.parsers.images.NhentaiParser; +import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.StringHelper; import pl.droidsonroids.jspoon.annotation.Selector; @@ -31,6 +32,8 @@ public class NhentaiContent extends BaseContentParser { // Fallback value for title (see #449) @Selector(value = "#info h1", defValue = NO_TITLE) private String titleAlt; + @Selector(value = "#tags time", attr = "datetime", defValue = "") + private String uploadDate; @Selector(value = "#info a[href*='/artist']") private List artists; @@ -68,6 +71,8 @@ public Content update(@NonNull final Content content, @Nonnull String url, boole if (titleDef.isEmpty()) titleDef = titleAlt.trim(); content.setTitle(StringHelper.removeNonPrintableChars(titleDef)); + content.setUploadDate(Helper.parseDatetimeToEpoch(uploadDate,"yyyy-MM-dd'T'HH:mm:ss'.'nnnnnnXXX")); // e.g. 2022-03-20T00:09:43.309901+00:00 + AttributeMap attributes = new AttributeMap(); ParseHelper.parseAttributes(attributes, AttributeType.ARTIST, artists, false, "name", Site.NHENTAI); ParseHelper.parseAttributes(attributes, AttributeType.CIRCLE, circles, false, "name", Site.NHENTAI); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/PorncomixContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/PorncomixContent.java index 6d6474ee4c..60d87e5355 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/PorncomixContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/PorncomixContent.java @@ -4,6 +4,7 @@ import org.jsoup.nodes.Element; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -15,15 +16,21 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; +import me.devsaki.hentoid.json.sources.YoastGalleryMetadata; import me.devsaki.hentoid.parsers.ParseHelper; +import me.devsaki.hentoid.util.Helper; +import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.StringHelper; import pl.droidsonroids.jspoon.annotation.Selector; +import timber.log.Timber; public class PorncomixContent extends BaseContentParser { @Selector(value = "head [property=og:image]", attr = "content", defValue = "") private String coverUrl; @Selector(value = "head [property=og:title]", attr = "content", defValue = "") private String title; + @Selector(value = "head script.yoast-schema-graph") + private Element metadata; @Selector(value = ".wp-manga-tags-list a[href*='tag']") private List mangaTags; @@ -63,6 +70,17 @@ public Content update(@NonNull final Content content, @Nonnull String url, boole content.setUrl(url); content.setCoverImageUrl(coverUrl); + if (metadata != null && metadata.childNodeSize() > 0) { + try { + YoastGalleryMetadata galleryMeta = JsonHelper.jsonToObject(metadata.childNode(0).toString(), YoastGalleryMetadata.class); + String publishDate = galleryMeta.getDatePublished(); // e.g. 2021-01-27T15:20:38+00:00 + if (!publishDate.isEmpty()) + content.setUploadDate(Helper.parseDatetimeToEpoch(publishDate, "yyyy-MM-dd'T'HH:mm:ssXXX")); + } catch (IOException e) { + Timber.i(e); + } + } + String artist = ""; if (content.getUrl().contains("/manga")) { String[] titleParts = title.split("-"); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/content/ToonilyContent.java b/app/src/main/java/me/devsaki/hentoid/parsers/content/ToonilyContent.java index aabe3bf848..a29e9c98d6 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/content/ToonilyContent.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/content/ToonilyContent.java @@ -4,6 +4,7 @@ import org.jsoup.nodes.Element; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -14,15 +15,21 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.Site; import me.devsaki.hentoid.enums.StatusContent; +import me.devsaki.hentoid.json.sources.YoastGalleryMetadata; import me.devsaki.hentoid.parsers.ParseHelper; +import me.devsaki.hentoid.util.Helper; +import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.StringHelper; import pl.droidsonroids.jspoon.annotation.Selector; +import timber.log.Timber; public class ToonilyContent extends BaseContentParser { @Selector(value = "head [property=og:image]", attr = "content") private String coverUrl; @Selector(value = ".breadcrumb a") private List breadcrumbs; + @Selector(value = "head script.yoast-schema-graph") + private Element metadata; @Selector(value = ".author-content a") private List author; @Selector(value = ".artist-content a") @@ -42,6 +49,17 @@ public Content update(@NonNull final Content content, @Nonnull String url, boole content.setTitle(title); content.populateUniqueSiteId(); + if (metadata != null && metadata.childNodeSize() > 0) { + try { + YoastGalleryMetadata galleryMeta = JsonHelper.jsonToObject(metadata.childNode(0).toString(), YoastGalleryMetadata.class); + String publishDate = galleryMeta.getDatePublished(); // e.g. 2021-01-27T15:20:38+00:00 + if (!publishDate.isEmpty()) + content.setUploadDate(Helper.parseDatetimeToEpoch(publishDate, "yyyy-MM-dd'T'HH:mm:ssXXX")); + } catch (IOException e) { + Timber.i(e); + } + } + AttributeMap attributes = new AttributeMap(); ParseHelper.parseAttributes(attributes, AttributeType.ARTIST, artist, false, Site.TOONILY); ParseHelper.parseAttributes(attributes, AttributeType.ARTIST, author, false, Site.TOONILY); 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 5ab68dbcc5..12e818d8fb 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 @@ -16,6 +16,7 @@ import me.devsaki.hentoid.enums.AttributeType; import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.parsers.ParseHelper; +import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.StringHelper; import pl.droidsonroids.jspoon.annotation.Selector; @@ -26,6 +27,8 @@ public class TsuminoContent extends BaseContentParser { private Element cover; @Selector(value = "div#Title", defValue = "") private String title; + @Selector(value = "div#Uploaded", defValue = "") + private String uploadDate; @Selector(value = "div#Pages", defValue = "") private String pages; @Selector(value = "div#Artist a") @@ -53,6 +56,8 @@ public Content update(@NonNull final Content content, @Nonnull String url, boole content.setCoverImageUrl(coverUrl); content.setTitle(StringHelper.removeNonPrintableChars(title)); + content.setUploadDate(Helper.parseDateToEpoch(uploadDate, "yyyy MMMM dd")); // e.g. 2021 December 13 + AttributeMap attributes = new AttributeMap(); ParseHelper.parseAttributes(attributes, AttributeType.ARTIST, artists, false, TSUMINO); ParseHelper.parseAttributes(attributes, AttributeType.CIRCLE, circles, false, TSUMINO); diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.java index 9b78822e35..a87daddc3b 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.java @@ -36,7 +36,6 @@ import me.devsaki.hentoid.json.sources.EHentaiImageQuery; import me.devsaki.hentoid.json.sources.EHentaiImageResponse; import me.devsaki.hentoid.parsers.ParseHelper; -import me.devsaki.hentoid.util.Helper; import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.exception.EmptyResultException; import me.devsaki.hentoid.util.exception.LimitReachedException; @@ -51,14 +50,34 @@ public class EHentaiParser implements ImageListParser { private final ParseProgress progress = new ParseProgress(); - private boolean processHalted = false; - static class MpvInfo { Integer gid; String mpvkey; String api_url; List images; Integer pagecount; + + MpvImageInfo getImageInfo(int index) { + return new MpvImageInfo(this, images.get(index), index + 1); + } + } + + static class MpvImageInfo { + final int gid; + final int pageNum; + final String mpvkey; + final String api_url; + final EHentaiImageMetadata image; + final int pagecount; + + public MpvImageInfo(MpvInfo info, EHentaiImageMetadata img, int pageNum) { + gid = info.gid; + this.pageNum = pageNum; + mpvkey = info.mpvkey; + api_url = info.api_url; + image = img; + pagecount = info.pagecount; + } } @@ -98,38 +117,53 @@ public List parseImageList(@NonNull Content content) throws Exception Document galleryDoc = getOnlineDocument(content.getGalleryUrl(), headers, useHentoidAgent, useWebviewAgent); if (galleryDoc != null) { // Detect if multipage viewer is on -// result = loadMpv("https://e-hentai.org/mpv/530350/8b3c7e4a21/", headers, useHentoidAgent); + //result = loadMpv("https://e-hentai.org/mpv/530350/8b3c7e4a21/", headers, useHentoidAgent, useWebviewAgent); Elements elements = galleryDoc.select(MPV_LINK_CSS); if (!elements.isEmpty()) { String mpvUrl = elements.get(0).attr("href"); try { - result = loadMpv(content, mpvUrl, headers, useHentoidAgent, useWebviewAgent); + result = loadMpv(mpvUrl, headers, useHentoidAgent, useWebviewAgent, progress); } catch (EmptyResultException e) { - result = loadClassic(content, galleryDoc, headers, useHentoidAgent, useWebviewAgent); + result = loadClassic(content, galleryDoc, headers, useHentoidAgent, useWebviewAgent, progress); } } else { - result = loadClassic(content, galleryDoc, headers, useHentoidAgent, useWebviewAgent); + result = loadClassic(content, galleryDoc, headers, useHentoidAgent, useWebviewAgent, progress); } } - - progress.complete(); // If the process has been halted manually, the result is incomplete and should not be returned as is - if (processHalted) throw new PreparationInterruptedException(); + if (progress.isProcessHalted()) throw new PreparationInterruptedException(); } finally { EventBus.getDefault().unregister(this); } return result; } - @SuppressWarnings("BusyWait") - private List loadMpv( - @NonNull Content content, + private static EHentaiImageResponse getMpvImage( + @NonNull MpvImageInfo imageInfo, + @NonNull final List> headers, + boolean useHentoidAgent, + boolean useWebviewAgent) throws EmptyResultException, IOException { + EHentaiImageQuery query = new EHentaiImageQuery(imageInfo.gid, imageInfo.image.getKey(), imageInfo.mpvkey, imageInfo.pageNum); + String jsonRequest = JsonHelper.serializeToJson(query, EHentaiImageQuery.class); + Response response = HttpHelper.postOnlineResource(imageInfo.api_url, headers, true, useHentoidAgent, useWebviewAgent, jsonRequest, JsonHelper.JSON_MIME_TYPE); + ResponseBody body = response.body(); + if (null == body) + throw new EmptyResultException("API " + imageInfo.api_url + " returned an empty body"); + String bodyStr = body.string(); + if (!bodyStr.contains("{") || !bodyStr.contains("}")) + throw new EmptyResultException("API " + imageInfo.api_url + " returned non-JSON data"); + + return JsonHelper.jsonToObject(bodyStr, EHentaiImageResponse.class); + } + + static List loadMpv( @NonNull final String mpvUrl, @NonNull final List> headers, boolean useHentoidAgent, - boolean useWebviewAgent) throws IOException, EmptyResultException { + boolean useWebviewAgent, + @NonNull ParseProgress progress) throws IOException, EmptyResultException { List result = new ArrayList<>(); // B.1- Open the MPV and parse gallery metadata @@ -138,38 +172,30 @@ private List loadMpv( throw new EmptyResultException("No exploitable data has been found on the multiple page viewer"); int pageCount = Math.min(mpvInfo.pagecount, mpvInfo.images.size()); - progress.start(content.getId(), -1, pageCount); - - // B.2- Call the API to get the pictures URL - for (int pageNum = 1; pageNum <= pageCount && !processHalted; pageNum++) { - EHentaiImageQuery query = new EHentaiImageQuery(mpvInfo.gid, mpvInfo.images.get(pageNum - 1).getKey(), mpvInfo.mpvkey, pageNum); - String jsonRequest = JsonHelper.serializeToJson(query, EHentaiImageQuery.class); - Response response = HttpHelper.postOnlineResource(mpvInfo.api_url, headers, true, useHentoidAgent, useWebviewAgent, jsonRequest, JsonHelper.JSON_MIME_TYPE); - ResponseBody body = response.body(); - if (null == body) - throw new EmptyResultException("API " + mpvInfo.api_url + " returned an empty body"); - String bodyStr = body.string(); - if (!bodyStr.contains("{") || !bodyStr.contains("}")) - throw new EmptyResultException("API " + mpvInfo.api_url + " returned non-JSON data"); - - EHentaiImageResponse imageMetadata = JsonHelper.jsonToObject(bodyStr, EHentaiImageResponse.class); - if (1 == pageNum) + + for (int pageNum = 1; pageNum <= pageCount && !progress.isProcessHalted(); pageNum++) { + // Get the URL of he 1st page as the cover + if (1 == pageNum) { + EHentaiImageResponse imageMetadata = getMpvImage(mpvInfo.getImageInfo(0), headers, useHentoidAgent, useWebviewAgent); result.add(ImageFile.newCover(imageMetadata.getUrl(), StatusContent.SAVED)); - result.add(ParseHelper.urlToImageFile(imageMetadata.getUrl(), pageNum, pageCount, StatusContent.SAVED)); - progress.advance(); - // Emulate JS loader - if (0 == pageNum % 10) Helper.pause(750); + } + // Add page URLs to be read later by the downloader + result.add(ImageFile.fromPageUrl( + pageNum, + JsonHelper.serializeToJson(mpvInfo.getImageInfo(pageNum - 1), MpvImageInfo.class), + StatusContent.SAVED, pageCount)); } return result; } - private List loadClassic( + static List loadClassic( @NonNull Content content, @NonNull final Document galleryDoc, @NonNull final List> headers, boolean useHentoidAgent, - boolean useWebviewAgent) throws IOException { + boolean useWebviewAgent, + @NonNull ParseProgress progress) throws IOException { List result = new ArrayList<>(); // A.1- Detect the number of pages of the gallery @@ -187,7 +213,7 @@ private List loadClassic( fetchPageUrls(galleryDoc, pageUrls); if (nbGalleryPages > 1) { - for (int i = 1; i < nbGalleryPages && !processHalted; i++) { + for (int i = 1; i < nbGalleryPages && !progress.isProcessHalted(); i++) { Document pageDoc = getOnlineDocument(content.getGalleryUrl() + "/?p=" + i, headers, useHentoidAgent, useWebviewAgent); if (pageDoc != null) fetchPageUrls(pageDoc, pageUrls); progress.advance(); @@ -222,6 +248,11 @@ static String getDisplayedImageUrl(@Nonnull Document doc) { Element e = elements.first(); if (e != null) return ParseHelper.getImgSrc(e); } + elements = doc.select("#i3.img"); + if (!elements.isEmpty()) { + Element e = elements.first(); + if (e != null) return ParseHelper.getImgSrc(e); + } return ""; } @@ -286,23 +317,45 @@ static MpvInfo parseMpvPage(@NonNull final String url, return result; } - @Nullable - public Optional parseBackupUrl(@NonNull String url, @NonNull Map requestHeaders, int order, int maxPages, Chapter chapter) throws Exception { + static Optional parseBackupUrl( + @NonNull String url, + @NonNull Site site, + @NonNull Map requestHeaders, + int order, + int maxPages, + Chapter chapter) throws Exception { List> reqHeaders = HttpHelper.webkitRequestHeadersToOkHttpHeaders(requestHeaders, url); - Document doc = getOnlineDocument(url, reqHeaders, Site.EHENTAI.useHentoidAgent(), Site.EHENTAI.useWebviewAgent()); + Document doc = getOnlineDocument(url, reqHeaders, site.useHentoidAgent(), site.useWebviewAgent()); if (doc != null) { String imageUrl = getDisplayedImageUrl(doc).toLowerCase(); // If we have the 509.gif picture, it means the bandwidth limit for e-h has been reached if (imageUrl.contains("/509.gif")) - throw new LimitReachedException("Bandwidth limit reached"); + throw new LimitReachedException(site.getDescription() + " download points regenerate over time or can be bought if you're in a hurry"); if (!imageUrl.isEmpty()) return Optional.of(ParseHelper.urlToImageFile(imageUrl, order, maxPages, StatusContent.SAVED, chapter)); } return Optional.empty(); } - static ImmutablePair> parseImagePageEh(@NonNull String url, @NonNull List> requestHeaders) throws IOException, LimitReachedException, EmptyResultException { - Document doc = getOnlineDocument(url, requestHeaders, Site.EHENTAI.useHentoidAgent(), Site.EHENTAI.useWebviewAgent()); + @Nullable + public Optional parseBackupUrl(@NonNull String url, @NonNull Map requestHeaders, int order, int maxPages, Chapter chapter) throws Exception { + return parseBackupUrl(url, Site.EHENTAI, requestHeaders, order, maxPages, chapter); + } + + static ImmutablePair> parseImagePageMpv(@NonNull String json, @NonNull List> requestHeaders, @NonNull final Site site) throws IOException, LimitReachedException, EmptyResultException { + MpvImageInfo mpvInfo = JsonHelper.jsonToObject(json, MpvImageInfo.class); + EHentaiImageResponse imageMetadata = getMpvImage(mpvInfo, requestHeaders, site.useHentoidAgent(), site.useWebviewAgent()); + + String imageUrl = imageMetadata.getUrl(); + // If we have the 509.gif picture, it means the bandwidth limit for e-h has been reached + if (imageUrl.contains("/509.gif")) + throw new LimitReachedException("E(x)-hentai download points regenerate over time or can be bought on e(x)-hentai if you're in a hurry"); + + return new ImmutablePair<>(imageUrl, Optional.empty()); + } + + static ImmutablePair> parseImagePageClassic(@NonNull String url, @NonNull List> requestHeaders, @NonNull final Site site) throws IOException, LimitReachedException, EmptyResultException { + Document doc = getOnlineDocument(url, requestHeaders, site.useHentoidAgent(), site.useWebviewAgent()); if (doc != null) { String imageUrl = getDisplayedImageUrl(doc).toLowerCase(); // If we have the 509.gif picture, it means the bandwidth limit for e-h has been reached @@ -317,9 +370,14 @@ static ImmutablePair> parseImagePageEh(@NonNull String throw new EmptyResultException("Page contains no picture data : " + url); } + static ImmutablePair> parseImagePage(@NonNull String url, @NonNull List> requestHeaders, @NonNull final Site site) throws IOException, LimitReachedException, EmptyResultException { + if (url.startsWith("http")) return parseImagePageClassic(url, requestHeaders, site); + else return parseImagePageMpv(url, requestHeaders, site); + } + @Override public ImmutablePair> parseImagePage(@NonNull String url, @NonNull List> requestHeaders) throws IOException, LimitReachedException, EmptyResultException { - return parseImagePageEh(url, requestHeaders); + return parseImagePage(url, requestHeaders, Site.EHENTAI); } /** @@ -346,14 +404,16 @@ public void onDownloadEvent(DownloadEvent event) { case DownloadEvent.Type.EV_PAUSE: case DownloadEvent.Type.EV_CANCEL: case DownloadEvent.Type.EV_SKIP: - processHalted = true; + progress.haltProcess(); break; case DownloadEvent.Type.EV_COMPLETE: case DownloadEvent.Type.EV_PREPARATION: case DownloadEvent.Type.EV_PROGRESS: case DownloadEvent.Type.EV_UNPAUSE: + case DownloadEvent.Type.EV_INTERRUPT_CONTENT: default: // Other events aren't handled here + break; } } } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/ExHentaiParser.java b/app/src/main/java/me/devsaki/hentoid/parsers/images/ExHentaiParser.java index 2d848edc6b..fe9b0100bb 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/images/ExHentaiParser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/ExHentaiParser.java @@ -28,26 +28,16 @@ import me.devsaki.hentoid.database.domains.Content; import me.devsaki.hentoid.database.domains.ImageFile; import me.devsaki.hentoid.enums.Site; -import me.devsaki.hentoid.enums.StatusContent; import me.devsaki.hentoid.events.DownloadEvent; -import me.devsaki.hentoid.json.sources.EHentaiImageQuery; -import me.devsaki.hentoid.json.sources.EHentaiImageResponse; -import me.devsaki.hentoid.parsers.ParseHelper; -import me.devsaki.hentoid.util.Helper; -import me.devsaki.hentoid.util.JsonHelper; import me.devsaki.hentoid.util.exception.EmptyResultException; import me.devsaki.hentoid.util.exception.LimitReachedException; import me.devsaki.hentoid.util.exception.PreparationInterruptedException; import me.devsaki.hentoid.util.network.HttpHelper; -import okhttp3.Response; -import okhttp3.ResponseBody; public class ExHentaiParser implements ImageListParser { private final ParseProgress progress = new ParseProgress(); - private boolean processHalted = false; - @Override public List parseImageList(@NonNull Content onlineContent, @NonNull Content storedContent) throws Exception { @@ -89,125 +79,32 @@ public List parseImageList(@NonNull Content content) throws Exception if (!elements.isEmpty()) { String mpvUrl = elements.get(0).attr("href"); try { - result = loadMpv(content, mpvUrl, headers, useHentoidAgent, useWebviewAgent); + result = EHentaiParser.loadMpv(mpvUrl, headers, useHentoidAgent, useWebviewAgent, progress); } catch (EmptyResultException e) { - result = loadClassic(content, galleryDoc, headers, useHentoidAgent, useWebviewAgent); + result = EHentaiParser.loadClassic(content, galleryDoc, headers, useHentoidAgent, useWebviewAgent, progress); } } else { - result = loadClassic(content, galleryDoc, headers, useHentoidAgent, useWebviewAgent); + result = EHentaiParser.loadClassic(content, galleryDoc, headers, useHentoidAgent, useWebviewAgent, progress); } } progress.complete(); // If the process has been halted manually, the result is incomplete and should not be returned as is - if (processHalted) throw new PreparationInterruptedException(); + if (progress.isProcessHalted()) throw new PreparationInterruptedException(); } finally { EventBus.getDefault().unregister(this); } return result; } - @SuppressWarnings("BusyWait") - private List loadMpv( - @NonNull Content content, - @NonNull final String mpvUrl, - @NonNull final List> headers, - boolean useHentoidAgent, - boolean useWebviewAgent) throws IOException, EmptyResultException { - List result = new ArrayList<>(); - - // B.1- Open the MPV and parse gallery metadata - EHentaiParser.MpvInfo mpvInfo = EHentaiParser.parseMpvPage(mpvUrl, headers, useHentoidAgent, useWebviewAgent); - if (null == mpvInfo) - throw new EmptyResultException("No exploitable data has been found on the multiple page viewer"); - - int pageCount = Math.min(mpvInfo.pagecount, mpvInfo.images.size()); - progress.start(content.getId(), -1, pageCount); - - // B.2- Call the API to get the pictures URL - for (int pageNum = 1; pageNum <= pageCount && !processHalted; pageNum++) { - EHentaiImageQuery query = new EHentaiImageQuery(mpvInfo.gid, mpvInfo.images.get(pageNum - 1).getKey(), mpvInfo.mpvkey, pageNum); - String jsonRequest = JsonHelper.serializeToJson(query, EHentaiImageQuery.class); - Response response = HttpHelper.postOnlineResource(mpvInfo.api_url, headers, true, useHentoidAgent, useWebviewAgent, jsonRequest, JsonHelper.JSON_MIME_TYPE); - ResponseBody body = response.body(); - if (null == body) - throw new EmptyResultException("API " + mpvInfo.api_url + " returned an empty body"); - String bodyStr = body.string(); - if (!bodyStr.contains("{") || !bodyStr.contains("}")) - throw new EmptyResultException("API " + mpvInfo.api_url + " returned non-JSON data"); - - EHentaiImageResponse imageMetadata = JsonHelper.jsonToObject(bodyStr, EHentaiImageResponse.class); - - if (1 == pageNum) - result.add(ImageFile.newCover(imageMetadata.getUrl(), StatusContent.SAVED)); - result.add(ParseHelper.urlToImageFile(imageMetadata.getUrl(), pageNum, pageCount, StatusContent.SAVED)); - progress.advance(); - // Emulate JS loader - if (0 == pageNum % 10) Helper.pause(750); - } - - return result; - } - - private List loadClassic( - @NonNull Content content, - @NonNull final Document galleryDoc, - @NonNull final List> headers, - boolean useHentoidAgent, - boolean useWebviewAgent) throws IOException { - List result = new ArrayList<>(); - - // A.1- Detect the number of pages of the gallery - Elements elements = galleryDoc.select("table.ptt a"); - if (elements.isEmpty()) return result; - - int tabId = (1 == elements.size()) ? 0 : elements.size() - 2; - int nbGalleryPages = Integer.parseInt(elements.get(tabId).text()); - - progress.start(content.getId(), -1, nbGalleryPages); - - // 2- Browse the gallery and fetch the URL for every page (since all of them have a different temporary key...) - List pageUrls = new ArrayList<>(); - - EHentaiParser.fetchPageUrls(galleryDoc, pageUrls); - - if (nbGalleryPages > 1) { - for (int i = 1; i < nbGalleryPages && !processHalted; i++) { - Document pageDoc = getOnlineDocument(content.getGalleryUrl() + "/?p=" + i, headers, useHentoidAgent, useWebviewAgent); - if (pageDoc != null) EHentaiParser.fetchPageUrls(pageDoc, pageUrls); - progress.advance(); - } - } - - // 3- Add all pages for the downloader to parse - result.add(ImageFile.newCover(content.getCoverImageUrl(), StatusContent.SAVED)); - - int order = 1; - for (String pageUrl : pageUrls) { - result.add(ImageFile.fromPageUrl(order++, pageUrl, StatusContent.SAVED, pageUrls.size())); - } - - return result; - } - @Nullable public Optional parseBackupUrl(@NonNull String url, @NonNull Map requestHeaders, int order, int maxPages, Chapter chapter) throws Exception { - List> reqHeaders = HttpHelper.webkitRequestHeadersToOkHttpHeaders(requestHeaders, url); - Document doc = getOnlineDocument(url, reqHeaders, Site.EXHENTAI.useHentoidAgent(), Site.EXHENTAI.useWebviewAgent()); - if (doc != null) { - String imageUrl = EHentaiParser.getDisplayedImageUrl(doc).toLowerCase(); - // If we have the 509.gif picture, it means the bandwidth limit for e-h has been reached - if (imageUrl.contains("/509.gif")) - throw new LimitReachedException("Exhentai download points regenerate over time or can be bought on e-hentai if you're in a hurry"); - if (!imageUrl.isEmpty()) - return Optional.of(ParseHelper.urlToImageFile(imageUrl, order, maxPages, StatusContent.SAVED, chapter)); - } - return Optional.empty(); + return EHentaiParser.parseBackupUrl(url, Site.EXHENTAI, requestHeaders, order, maxPages, chapter); } @Override public ImmutablePair> parseImagePage(@NonNull String url, @NonNull List> requestHeaders) throws IOException, LimitReachedException, EmptyResultException { - return EHentaiParser.parseImagePageEh(url, requestHeaders); + return EHentaiParser.parseImagePage(url, requestHeaders, Site.EXHENTAI); } /** @@ -221,8 +118,13 @@ public void onDownloadEvent(DownloadEvent event) { case DownloadEvent.Type.EV_PAUSE: case DownloadEvent.Type.EV_CANCEL: case DownloadEvent.Type.EV_SKIP: - processHalted = true; + progress.haltProcess(); break; + case DownloadEvent.Type.EV_COMPLETE: + case DownloadEvent.Type.EV_PREPARATION: + case DownloadEvent.Type.EV_PROGRESS: + case DownloadEvent.Type.EV_UNPAUSE: + case DownloadEvent.Type.EV_INTERRUPT_CONTENT: default: // Other events aren't handled here } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/Manhwa18Parser.java b/app/src/main/java/me/devsaki/hentoid/parsers/images/Manhwa18Parser.java index 78b71b1391..d811227005 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/images/Manhwa18Parser.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/Manhwa18Parser.java @@ -45,7 +45,7 @@ public List parseImageListImpl(@NonNull Content onlineContent, @Nulla List chapterLinks = doc.select("div ul a[href*=chap]"); if (chapterLinks.isEmpty()) chapterLinks = doc.select("div ul a[href*=ch-]"); - chapters = ParseHelper.getChaptersFromLinks(chapterLinks, onlineContent.getId()); + chapters = ParseHelper.getChaptersFromLinks(chapterLinks, onlineContent.getId(), "div.chapter-time", "dd/MM/yyyy"); // If the stored content has chapters already, save them for comparison List storedChapters = null; @@ -65,7 +65,9 @@ public List parseImageListImpl(@NonNull Content onlineContent, @Nulla int imgOffset = ParseHelper.getMaxImageOrder(storedChapters); // 2. Open each chapter URL and get the image data until all images are found + long minEpoch = Long.MAX_VALUE; for (Chapter chp : extraChapters) { + if (chp.getUploadDate() > 0) minEpoch = Math.min(minEpoch, chp.getUploadDate()); if (processHalted.get()) break; doc = getOnlineDocument(chp.getUrl(), headers, Site.MANHWA18.useHentoidAgent(), Site.MANHWA18.useWebviewAgent()); if (doc != null) { @@ -80,6 +82,10 @@ public List parseImageListImpl(@NonNull Content onlineContent, @Nulla } progressPlus(); } + if (minEpoch > 0) { + onlineContent.setUploadDate(minEpoch); + onlineContent.setUpdatedProperties(true); + } progressComplete(); // Add cover if it's a first download diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/ParseProgress.java b/app/src/main/java/me/devsaki/hentoid/parsers/images/ParseProgress.java index 18d4d5368d..ed660996f9 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/images/ParseProgress.java +++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/ParseProgress.java @@ -1,5 +1,7 @@ package me.devsaki.hentoid.parsers.images; +import java.util.concurrent.atomic.AtomicBoolean; + import me.devsaki.hentoid.parsers.ParseHelper; class ParseProgress { @@ -9,6 +11,7 @@ class ParseProgress { private int currentStep; private int maxSteps; private boolean hasStarted = false; + private final AtomicBoolean processHalted = new AtomicBoolean(false); void start(long contentId, long storedId, int maxSteps) { this.contentId = contentId; @@ -23,6 +26,14 @@ boolean hasStarted() { return hasStarted; } + boolean isProcessHalted() { + return processHalted.get(); + } + + void haltProcess() { + processHalted.set(true); + } + void advance() { ParseHelper.signalProgress(contentId, storedId, ++currentStep, maxSteps); } diff --git a/app/src/main/java/me/devsaki/hentoid/util/BundleX.kt b/app/src/main/java/me/devsaki/hentoid/util/BundleX.kt index 7c323af394..4bf6b052f2 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/BundleX.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/BundleX.kt @@ -16,6 +16,14 @@ fun Bundle.boolean(default: Boolean) = object : ReadWriteProperty putBoolean(property.name, value) } +fun Bundle.bundle() = object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>) = + getBundle(property.name) + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Bundle?) = + putBundle(property.name, value) +} + fun Bundle.byte(default: Byte) = object : ReadWriteProperty { override fun getValue(thisRef: Any, property: KProperty<*>) = getByte(property.name, default) @@ -64,11 +72,11 @@ fun Bundle.float(default: Float) = object : ReadWriteProperty { putFloat(property.name, value) } -fun Bundle.string(default: String?) = object : ReadWriteProperty { +fun Bundle.string(default: String) = object : ReadWriteProperty { override fun getValue(thisRef: Any, property: KProperty<*>) = getString(property.name, default) - override fun setValue(thisRef: Any, property: KProperty<*>, value: String?) = + override fun setValue(thisRef: Any, property: KProperty<*>, value: String) = putString(property.name, value) } @@ -118,4 +126,117 @@ fun Bundle.intArrayList() = object : ReadWriteProperty?> { override fun setValue(thisRef: Any, property: KProperty<*>, value: ArrayList?) = putIntegerArrayList(property.name, value) +} + +fun Bundle.boolean() = object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>) = + if (containsKey(property.name)) + getBoolean(property.name) + else + null + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Boolean?) { + if (value == null) + remove(property.name) + else + putBoolean(property.name, value) + } +} + +fun Bundle.byte() = object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>) = + if (containsKey(property.name)) + getByte(property.name) + else + null + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Byte?) = + if (value == null) + remove(property.name) + else + putByte(property.name, value) +} + +fun Bundle.char() = object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>) = + if (containsKey(property.name)) + getChar(property.name) + else + null + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Char?) = + if (value == null) + remove(property.name) + else + putChar(property.name, value) +} + +fun Bundle.short() = object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>) = + if (containsKey(property.name)) + getShort(property.name) + else + null + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Short?) = + if (value == null) + remove(property.name) + else + putShort(property.name, value) +} + +fun Bundle.int() = object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>) = + if (containsKey(property.name)) + getInt(property.name) + else + null + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Int?) = + if (value == null) + remove(property.name) + else + putInt(property.name, value) +} + +fun Bundle.long() = object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>) = + if (containsKey(property.name)) + getLong(property.name) + else + null + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Long?) = + if (value == null) + remove(property.name) + else + putLong(property.name, value) +} + +fun Bundle.float() = object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>) = + if (containsKey(property.name)) + getFloat(property.name) + else + null + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Float?) = + if (value == null) + remove(property.name) + else + putFloat(property.name, value) +} + +fun Bundle.string() = object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>) = + if (containsKey(property.name)) + getString(property.name) + else + null + + override fun setValue(thisRef: Any, property: KProperty<*>, value: String?) = + if (value == null) + remove(property.name) + else + putString(property.name, value) } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.java b/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.java index 8bfd4b153f..8c4ff01da7 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.java +++ b/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.java @@ -29,6 +29,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.URL; @@ -159,7 +160,7 @@ public static void viewContentGalleryPage(@NonNull final Context context, @NonNu Intent intent = new Intent(context, Content.getWebActivityClass(content.getSite())); BaseWebActivityBundle bundle = new BaseWebActivityBundle(); bundle.setUrl(content.getGalleryUrl()); - intent.putExtras(bundle.toBundle()); + intent.putExtras(bundle.getBundle()); if (wrapPin) intent = UnlockActivity.wrapIntent(context, intent); context.startActivity(intent); } @@ -177,8 +178,10 @@ public static void updateContentJson(@NonNull Context context, @NonNull Content if (null == file) throw new IllegalArgumentException("'" + content.getJsonUri() + "' does not refer to a valid file"); - try { - JsonHelper.updateJson(context, JsonContent.fromEntity(content), JsonContent.class, file); + try (OutputStream output = FileHelper.getOutputStream(context, file)) { + if (output != null) + JsonHelper.updateJson(JsonContent.fromEntity(content), JsonContent.class, output); + else Timber.w("JSON file creation failed for %s", file.getUri()); } catch (IOException e) { Timber.e(e, "Error while writing to %s", content.getJsonUri()); } @@ -265,7 +268,7 @@ public static boolean openHentoidViewer( Timber.d("Opening: %s from: %s", content.getTitle(), content.getStorageUri()); - ImageViewerActivityBundle.Builder builder = new ImageViewerActivityBundle.Builder(); + ImageViewerActivityBundle builder = new ImageViewerActivityBundle(); builder.setContentId(content.getId()); if (searchParams != null) builder.setSearchParams(searchParams); if (pageNumber > -1) builder.setPageNumber(pageNumber); @@ -942,7 +945,7 @@ public static void launchBrowserFor(@NonNull final Context context, @NonNull fin BaseWebActivityBundle bundle = new BaseWebActivityBundle(); bundle.setUrl(targetUrl); - intent.putExtras(bundle.toBundle()); + intent.putExtras(bundle.getBundle()); context.startActivity(intent); } 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 34adbe6975..df351a4f73 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/FileHelper.java +++ b/app/src/main/java/me/devsaki/hentoid/util/FileHelper.java @@ -284,6 +284,7 @@ public static OutputStream getOutputStream(@NonNull final File target) throws IO * @return New OutputStream opened on the given file * @throws IOException In case something horrible happens during I/O */ + @Nullable public static OutputStream getOutputStream(@NonNull final Context context, @NonNull final DocumentFile target) throws IOException { return context.getContentResolver().openOutputStream(target.getUri(), "rwt"); // Always truncate file to whatever data needs to be written } @@ -695,7 +696,7 @@ public static String getMimeTypeFromFileName(@NonNull String fileName) { public static void shareFile(final @NonNull Context context, final @NonNull Uri fileUri, final @NonNull String title) { Intent sharingIntent = new Intent(Intent.ACTION_SEND); sharingIntent.setType("text/*"); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, title); + if (!title.isEmpty()) sharingIntent.putExtra(Intent.EXTRA_SUBJECT, title); if (fileUri.toString().startsWith("file")) { Uri legitUri = FileProvider.getUriForFile( context, 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 42f74adb57..69326fd7c9 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/Helper.java +++ b/app/src/main/java/me/devsaki/hentoid/util/Helper.java @@ -27,6 +27,14 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics; +import org.threeten.bp.Instant; +import org.threeten.bp.ZoneId; +import org.threeten.bp.format.DateTimeFormatter; +import org.threeten.bp.format.DateTimeFormatterBuilder; +import org.threeten.bp.format.DateTimeParseException; +import org.threeten.bp.format.ResolverStyle; +import org.threeten.bp.temporal.ChronoField; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -36,6 +44,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Random; import java.util.Set; @@ -94,9 +103,9 @@ public static List getListFromPrimitiveArray(long[] input) { } public static Set getSetFromPrimitiveArray(long[] input) { - Set list = new HashSet<>(input.length); - for (long n : input) list.add(n); - return list; + Set set = new HashSet<>(input.length); + for (long n : input) set.add(n); + return set; } /** @@ -412,6 +421,41 @@ public static int getRandomInt(int maxExclude) { return rand.nextInt(maxExclude); } + // TODO doc + public static long parseDatetimeToEpoch(@NonNull String date, @NonNull String pattern) { + final String dateClean = date.trim().replaceAll("(?<=\\d)(st|nd|rd|th)", ""); + final DateTimeFormatter formatter = DateTimeFormatter + .ofPattern(pattern) + .withResolverStyle(ResolverStyle.LENIENT) + .withLocale(Locale.ENGLISH) // To parse english expressions (e.g. month name) + .withZone(ZoneId.systemDefault()); + + try { + return Instant.from(formatter.parse(dateClean)).toEpochMilli(); + } catch (DateTimeParseException e) { + Timber.w(e); + } + return 0; + } + + public static long parseDateToEpoch(@NonNull String date, @NonNull String pattern) { + final String dateClean = date.trim().replaceAll("(?<=\\d)(st|nd|rd|th)", ""); + final DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .appendPattern(pattern) + .parseDefaulting(ChronoField.NANO_OF_DAY, 0) // To allow passing dates without time + .toFormatter() + .withResolverStyle(ResolverStyle.LENIENT) + .withLocale(Locale.ENGLISH) // To parse english expressions (e.g. month name) + .withZone(ZoneId.systemDefault()); + + try { + return Instant.from(formatter.parse(dateClean)).toEpochMilli(); + } catch (DateTimeParseException e) { + Timber.w(e); + } + return 0; + } + /** * Update the JSON file that stores bookmarks with the current bookmarks * 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 33f0726f07..8e409aaabf 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/JsonHelper.java +++ b/app/src/main/java/me/devsaki/hentoid/util/JsonHelper.java @@ -10,7 +10,6 @@ import com.squareup.moshi.Types; import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -105,27 +104,6 @@ public static DocumentFile jsonToFile(@NonNull final Context context, K obje return file; } - /** - * Serialize and save the object contents to the given existing file using the JSON format - * - * @param context Context to be used - * @param object Object to serialize - * @param type Type of the output JSON structure to use - * @param file File to write to - * @param Type of the given object - * @throws IOException If anything happens during file I/O - */ - static void updateJson(@NonNull final Context context, K object, Type type, @Nonnull DocumentFile file) throws IOException { - if (!file.exists()) return; - - try (OutputStream output = FileHelper.getOutputStream(context, file)) { - if (output != null) updateJson(object, type, output); - else Timber.w("JSON file creation failed for %s", file.getUri()); - } catch (FileNotFoundException e) { - Timber.e(e); - } - } - /** * Serialize and save the object contents to the given OutputStream using the JSON format * @@ -135,7 +113,7 @@ static void updateJson(@NonNull final Context context, K object, Type type, * @param Type of the given object * @throws IOException If anything happens during file I/O */ - private static void updateJson(K object, Type type, @Nonnull OutputStream output) throws IOException { + public static void updateJson(K object, Type type, @Nonnull OutputStream output) throws IOException { byte[] bytes = serializeToJson(object, type).getBytes(); output.write(bytes); if (output instanceof FileOutputStream) FileHelper.sync((FileOutputStream) output); 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 64ce152878..24928e2076 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/Preferences.java +++ b/app/src/main/java/me/devsaki/hentoid/util/Preferences.java @@ -95,11 +95,11 @@ public static void performHousekeeping() { isDesc = true; break; case (Constant.ORDER_CONTENT_LAST_DL_DATE_FIRST): - field = Constant.ORDER_FIELD_DOWNLOAD_DATE; + field = Constant.ORDER_FIELD_DOWNLOAD_PROCESSING_DATE; isDesc = true; break; case (Constant.ORDER_CONTENT_LAST_DL_DATE_LAST): - field = Constant.ORDER_FIELD_DOWNLOAD_DATE; + field = Constant.ORDER_FIELD_DOWNLOAD_PROCESSING_DATE; break; case (Constant.ORDER_CONTENT_RANDOM): field = Constant.ORDER_FIELD_RANDOM; @@ -299,6 +299,16 @@ public static boolean getEndlessScroll() { return sharedPreferences.getBoolean(Key.ENDLESS_SCROLL, Default.ENDLESS_SCROLL); } + public static boolean isTopFabEnabled() { + return sharedPreferences.getBoolean(Key.TOP_FAB, Default.TOP_FAB); + } + + public static void setTopFabEnabled(boolean value) { + sharedPreferences.edit() + .putBoolean(Key.TOP_FAB, value) + .apply(); + } + public static boolean getRecentVisibility() { return sharedPreferences.getBoolean(Key.APP_PREVIEW, BuildConfig.DEBUG); } @@ -817,6 +827,7 @@ private Key() { static final String FIRST_RUN = "pref_first_run"; public static final String DRAWER_SOURCES = "pref_drawer_sources"; public static final String ENDLESS_SCROLL = "pref_endless_scroll"; + public static final String TOP_FAB = "pref_top_fab"; public static final String SD_STORAGE_URI = "pref_sd_storage_uri"; public static final String EXTERNAL_LIBRARY = "pref_external_library"; public static final String EXTERNAL_LIBRARY_URI = "pref_external_library_uri"; @@ -921,6 +932,7 @@ private Default() { static final boolean SEARCH_COUNT_ATTRIBUTE_RESULTS = true; static final boolean FIRST_RUN = true; static final boolean ENDLESS_SCROLL = true; + static final boolean TOP_FAB = true; static final int MEMORY_ALERT = 110; static final boolean IMPORT_QUEUE_EMPTY = false; static final boolean EXTERNAL_LIBRARY_DELETE = false; @@ -1010,13 +1022,14 @@ private Constant() { public static final int ORDER_FIELD_TITLE = 0; public static final int ORDER_FIELD_ARTIST = 1; public static final int ORDER_FIELD_NB_PAGES = 2; - public static final int ORDER_FIELD_DOWNLOAD_DATE = 3; + public static final int ORDER_FIELD_DOWNLOAD_PROCESSING_DATE = 3; public static final int ORDER_FIELD_UPLOAD_DATE = 4; public static final int ORDER_FIELD_READ_DATE = 5; public static final int ORDER_FIELD_READS = 6; public static final int ORDER_FIELD_SIZE = 7; public static final int ORDER_FIELD_CHILDREN = 8; // Groups only public static final int ORDER_FIELD_READ_PROGRESS = 9; + public static final int ORDER_FIELD_DOWNLOAD_COMPLETION_DATE = 10; public static final int ORDER_FIELD_CUSTOM = 98; public static final int ORDER_FIELD_RANDOM = 99; diff --git a/app/src/main/java/me/devsaki/hentoid/util/network/HttpHelper.java b/app/src/main/java/me/devsaki/hentoid/util/network/HttpHelper.java index 326daf400c..c43c71c477 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/network/HttpHelper.java +++ b/app/src/main/java/me/devsaki/hentoid/util/network/HttpHelper.java @@ -248,14 +248,23 @@ public static List> webkitRequestHeadersToOkHttpHeaders(@Nu /** * Add current cookies of the given URL to the given headers structure + * If the given header already has a cookie entry, it is removed and replaced with the one + * associated with the given URL. * * @param url URL to get cookies for - * @param headers Structure to populate + * @param headers Structure to populate or update */ public static void addCurrentCookiesToHeader(@NonNull final String url, @NonNull List> headers) { String cookieStr = getCookies(url); - if (!cookieStr.isEmpty()) + if (!cookieStr.isEmpty()) { + for (int i = 0; i < headers.size(); i++) { + if (headers.get(i).first.equals(HEADER_COOKIE_KEY)) { + headers.remove(i); + break; + } + } headers.add(new Pair<>(HEADER_COOKIE_KEY, cookieStr)); + } } /** diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.java b/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.java index 347ce044fe..5d7d5fcd2b 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.java +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.java @@ -314,7 +314,7 @@ public void bindView(@NotNull ContentItem item, @NotNull List payloads) { // Payloads are set when the content stays the same but some properties alone change if (!payloads.isEmpty()) { Bundle bundle = (Bundle) payloads.get(0); - ContentItemBundle.Parser bundleParser = new ContentItemBundle.Parser(bundle); + ContentItemBundle bundleParser = new ContentItemBundle(bundle); Boolean boolValue = bundleParser.isBeingDeleted(); if (boolValue != null) item.content.setIsBeingDeleted(boolValue); @@ -324,8 +324,8 @@ public void bindView(@NotNull ContentItem item, @NotNull List payloads) { if (boolValue != null) item.content.setCompleted(boolValue); Long longValue = bundleParser.getReads(); if (longValue != null) item.content.setReads(longValue); - longValue = bundleParser.getReadPagesCount(); - if (longValue != null) item.content.setReadPagesCount(longValue.intValue()); + Integer intValue = bundleParser.getReadPagesCount(); + if (intValue != null) item.content.setReadPagesCount(intValue); String stringValue = bundleParser.getCoverUri(); if (stringValue != null) item.content.getCover().setFileUri(stringValue); stringValue = bundleParser.getTitle(); diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.java b/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.java index 5d3405b172..d350be2b74 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.java +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.java @@ -220,9 +220,9 @@ public void bindView(@NotNull DuplicateItem item, @NotNull List payloads) { // Payloads are set when the content stays the same but some properties alone change if (!payloads.isEmpty()) { Bundle bundle = (Bundle) payloads.get(0); - DuplicateItemBundle.Parser bundleParser = new DuplicateItemBundle.Parser(bundle); + DuplicateItemBundle bundleParser = new DuplicateItemBundle(bundle); - Boolean boolValue = bundleParser.getKeep(); + Boolean boolValue = bundleParser.isKeep(); if (boolValue != null) item.keep = boolValue; boolValue = bundleParser.isBeingDeleted(); if (boolValue != null) item.isBeingDeleted = boolValue; diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.java b/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.java index 6055bcf813..6184255880 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.java +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.java @@ -175,7 +175,7 @@ public void bindView(@NotNull GroupDisplayItem item, @NotNull List payloads) // Payloads are set when the content stays the same but some properties alone change if (!payloads.isEmpty()) { Bundle bundle = (Bundle) payloads.get(0); - GroupItemBundle.Parser bundleParser = new GroupItemBundle.Parser(bundle); + GroupItemBundle bundleParser = new GroupItemBundle(bundle); String stringValue = bundleParser.getCoverUri(); if (stringValue != null) coverUri = stringValue; diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.java b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.java index 473aa97435..903fa7f67c 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.java +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.java @@ -165,7 +165,7 @@ public void bindView(@NotNull ImageFileItem item, @NotNull List payloads) { // Payloads are set when the content stays the same but some properties alone change if (!payloads.isEmpty()) { Bundle bundle = (Bundle) payloads.get(0); - ImageItemBundle.Parser bundleParser = new ImageItemBundle.Parser(bundle); + ImageItemBundle bundleParser = new ImageItemBundle(bundle); Boolean boolValue = bundleParser.isFavourite(); if (boolValue != null) item.image.setFavourite(boolValue); diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/TextItem.java b/app/src/main/java/me/devsaki/hentoid/viewholders/TextItem.java index 13b2ea2b89..d2a47035dc 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/TextItem.java +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/TextItem.java @@ -49,6 +49,19 @@ public TextItem(String text, T tag, boolean centered) { this.touchHelper = null; this.reformatCase = true; this.isHighlighted = false; + this.setSelectable(false); + } + + public TextItem(String text, T tag, boolean reformatCase, boolean isSelected) { + this.text = text; + this.tag = tag; + this.centered = false; + this.draggable = false; + this.touchHelper = null; + this.reformatCase = reformatCase; + this.isHighlighted = false; + this.setSelected(isSelected); + this.setSelectable(true); } public TextItem(String text, T tag, boolean draggable, boolean reformatCase, boolean isHighlighted, ItemTouchHelper touchHelper) { @@ -59,6 +72,7 @@ public TextItem(String text, T tag, boolean draggable, boolean reformatCase, boo this.touchHelper = touchHelper; this.reformatCase = reformatCase; this.isHighlighted = isHighlighted; + this.setSelectable(true); } @Nullable @@ -136,6 +150,7 @@ public void bindView(@NotNull TextItem item, @NotNull List list) { } if (item.isSelected()) checkedIndicator.setVisibility(View.VISIBLE); + else if (item.isSelectable()) checkedIndicator.setVisibility(View.INVISIBLE); else checkedIndicator.setVisibility(View.GONE); title.setText(item.getDisplayText()); diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/LibraryViewModel.java b/app/src/main/java/me/devsaki/hentoid/viewmodels/LibraryViewModel.java index 9789bf1c46..4d63ddc7c7 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewmodels/LibraryViewModel.java +++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/LibraryViewModel.java @@ -72,6 +72,7 @@ import me.devsaki.hentoid.util.exception.EmptyResultException; import me.devsaki.hentoid.util.network.HttpHelper; import me.devsaki.hentoid.widget.ContentSearchManager; +import me.devsaki.hentoid.widget.GroupSearchManager; import me.devsaki.hentoid.workers.DeleteWorker; import me.devsaki.hentoid.workers.PurgeWorker; import me.devsaki.hentoid.workers.data.DeleteData; @@ -82,43 +83,51 @@ public class LibraryViewModel extends AndroidViewModel { // Collection DAO private final CollectionDAO dao; - // Library search manager - private final ContentSearchManager searchManager; + // Content search manager + private final ContentSearchManager contentSearchManager; + // Groups search manager + private final GroupSearchManager groupSearchManager; // Cleanup for all RxJava calls private final CompositeDisposable compositeDisposable = new CompositeDisposable(); // Cleanup for all work observers private final List>> workObservers = new ArrayList<>(); - // Collection data + // Content data private LiveData> currentSource; private final LiveData totalContent; private final MediatorLiveData> libraryPaged = new MediatorLiveData<>(); + private final MutableLiveData contentSearchBundle = new MutableLiveData<>(); // Groups data private final MutableLiveData group = new MutableLiveData<>(); private LiveData> currentGroupsSource; private final MediatorLiveData> groups = new MediatorLiveData<>(); + private LiveData> currentGroupsTotalSource; private final MediatorLiveData currentGroupTotal = new MediatorLiveData<>(); private final MutableLiveData isCustomGroupingAvailable = new MutableLiveData<>(); // True if there's at least one existing custom group; false instead + private final MutableLiveData groupSearchBundle = new MutableLiveData<>(); - // Updated whenever a new search is performed - private final MediatorLiveData newSearch = new MediatorLiveData<>(); + // Updated whenever a new COntentsearch is performed + private final MediatorLiveData newContentSearch = new MediatorLiveData<>(); public LibraryViewModel(@NonNull Application application, @NonNull CollectionDAO collectionDAO) { super(application); dao = collectionDAO; - searchManager = new ContentSearchManager(dao); + contentSearchManager = new ContentSearchManager(dao); + groupSearchManager = new GroupSearchManager(dao); totalContent = dao.countAllBooks(); refreshCustomGroupingAvailable(); } public void onSaveState(Bundle outState) { - searchManager.saveToBundle(outState); + contentSearchManager.saveToBundle(outState); + groupSearchManager.saveToBundle(outState); } public void onRestoreState(@Nullable Bundle savedState) { if (savedState == null) return; - searchManager.loadFromBundle(savedState); + contentSearchManager.loadFromBundle(savedState); + groupSearchManager.loadFromBundle(savedState); } @Override @@ -149,8 +158,8 @@ public LiveData getTotalContent() { } @NonNull - public LiveData getNewSearch() { - return newSearch; + public LiveData getNewContentSearch() { + return newContentSearch; } @NonNull @@ -168,10 +177,14 @@ public LiveData isCustomGroupingAvailable() { return isCustomGroupingAvailable; } - public Bundle getSearchManagerBundle() { - Bundle bundle = new Bundle(); - searchManager.saveToBundle(bundle); - return bundle; + @NonNull + public LiveData getContentSearchManagerBundle() { + return contentSearchBundle; + } + + @NonNull + public LiveData getGroupSearchManagerBundle() { + return groupSearchBundle; } // ========================= @@ -182,14 +195,17 @@ public Bundle getSearchManagerBundle() { * Perform a new library search */ private void doSearchContent() { - if (currentSource != null) libraryPaged.removeSource(currentSource); - - searchManager.setContentSortField(Preferences.getContentSortField()); - searchManager.setContentSortDesc(Preferences.isContentSortDesc()); - - currentSource = searchManager.getLibrary(); + // Update search properties set directly through Preferences + Timber.v(">> doSearchContent"); + contentSearchManager.setContentSortField(Preferences.getContentSortField()); + contentSearchManager.setContentSortDesc(Preferences.isContentSortDesc()); + if (Preferences.getGroupingDisplay().equals(Grouping.FLAT)) + contentSearchManager.setGroup(null); + if (currentSource != null) libraryPaged.removeSource(currentSource); + currentSource = contentSearchManager.getLibrary(); libraryPaged.addSource(currentSource, libraryPaged::setValue); + contentSearchBundle.postValue(contentSearchManager.toBundle()); } /** @@ -198,9 +214,9 @@ private void doSearchContent() { * @param query Query to use for the universal search */ public void searchContentUniversal(@NonNull String query) { - searchManager.clearSelectedSearchTags(); // If user searches in main toolbar, universal search takes over advanced search - searchManager.setQuery(query); - newSearch.setValue(true); + contentSearchManager.clearSelectedSearchTags(); // If user searches in main toolbar, universal search takes over advanced search + contentSearchManager.setQuery(query); + newContentSearch.setValue(true); doSearchContent(); } @@ -211,9 +227,9 @@ public void searchContentUniversal(@NonNull String query) { * @param metadata Metadata to use for the search */ public void searchContent(@NonNull String query, @NonNull List metadata) { - searchManager.setQuery(query); - searchManager.setTags(metadata); - newSearch.setValue(true); + contentSearchManager.setQuery(query); + contentSearchManager.setTags(metadata); + newContentSearch.setValue(true); doSearchContent(); } @@ -225,23 +241,42 @@ public void clearContent() { } } - /** - * Perform a new group search using the given query - * - * @param query Query to use for the search - */ - public void searchGroup(Grouping grouping, @NonNull String query, int orderField, boolean orderDesc, int artistGroupVisibility, boolean groupFavouritesOnly) { + public void searchGroup() { + doSearchGroup(); + } + + private void doSearchGroup() { + Timber.v(">> doSearchGroup"); + // Update search properties set directly through Preferences + groupSearchManager.setSortField(Preferences.getGroupSortField()); + groupSearchManager.setSortDesc(Preferences.isGroupSortDesc()); + groupSearchManager.setGrouping(Preferences.getGroupingDisplay()); + groupSearchManager.setArtistGroupVisibility(Preferences.getArtistGroupVisibility()); + if (currentGroupsSource != null) groups.removeSource(currentGroupsSource); - currentGroupsSource = dao.selectGroupsLive(grouping.getId(), query, orderField, orderDesc, artistGroupVisibility, groupFavouritesOnly); + currentGroupsSource = groupSearchManager.getGroups(); groups.addSource(currentGroupsSource, groups::setValue); + + if (currentGroupsTotalSource != null) + currentGroupTotal.removeSource(currentGroupsTotalSource); + currentGroupsTotalSource = groupSearchManager.getAllGroups(); + currentGroupTotal.addSource(currentGroupsTotalSource, list -> currentGroupTotal.postValue(list.size())); + + groupSearchBundle.postValue(groupSearchManager.toBundle()); + refreshCustomGroupingAvailable(); + } + + public void refreshCustomGroupingAvailable() { + isCustomGroupingAvailable.postValue(dao.countGroupsFor(Grouping.CUSTOM) > 0); } + /** * Toggle the completed filter */ public void toggleCompletedFilter() { - searchManager.setFilterBookCompleted(!searchManager.isFilterBookCompleted()); - newSearch.setValue(true); + contentSearchManager.setFilterBookCompleted(!contentSearchManager.isFilterBookCompleted()); + newContentSearch.setValue(true); doSearchContent(); } @@ -249,8 +284,8 @@ public void toggleCompletedFilter() { * Toggle the completed filter */ public void toggleNotCompletedFilter() { - searchManager.setFilterBookNotCompleted(!searchManager.isFilterBookNotCompleted()); - newSearch.setValue(true); + contentSearchManager.setFilterBookNotCompleted(!contentSearchManager.isFilterBookNotCompleted()); + newContentSearch.setValue(true); doSearchContent(); } @@ -258,25 +293,54 @@ public void toggleNotCompletedFilter() { * Toggle the books favourite filter */ public void setContentFavouriteFilter(boolean value) { - searchManager.setFilterBookFavourites(value); - newSearch.setValue(true); + contentSearchManager.setFilterBookFavourites(value); + newContentSearch.setValue(true); doSearchContent(); } /** - * Set the mode (endless or paged) + * Toggle the groups favourite filter */ - public void setPagingMethod(boolean isEndless) { - searchManager.setLoadAll(!isEndless); - newSearch.setValue(true); + public void setGroupFavouriteFilter(boolean value) { + groupSearchManager.setFilterFavourites(value); + doSearchGroup(); + } + + public void setGroupQuery(String value) { + groupSearchManager.setQuery(value); + doSearchGroup(); + } + + public void setGrouping(int groupingId) { + int currentGrouping = Preferences.getGroupingDisplay().getId(); + if (groupingId != currentGrouping) { + Preferences.setGroupingDisplay(groupingId); + if (groupingId == Grouping.FLAT.getId()) doSearchContent(); + doSearchGroup(); + } + } + + public void clearGroupFilters() { + groupSearchManager.clearFilters(); + doSearchGroup(); + } + + public void clearContentFilters() { + contentSearchManager.clearFilters(); doSearchContent(); } /** - * Update the order of the content list + * Set Content paging mode (endless or paged) */ - public void updateContentOrder() { - newSearch.setValue(true); + public void setContentPagingMethod(boolean isEndless) { + contentSearchManager.setLoadAll(!isEndless); + newContentSearch.setValue(true); + doSearchContent(); + } + + public void searchContent() { + newContentSearch.setValue(true); doSearchContent(); } @@ -284,35 +348,19 @@ public void setGroup(Group group, boolean forceRefresh) { Group currentGroup = this.group.getValue(); if (!forceRefresh && Objects.equals(group, currentGroup)) return; + // Reset content sorting to TITLE when reaching the Ungrouped group with CUSTOM sorting (can't work) + if (!group.grouping.canReorderBooks() || (group.grouping.equals(Grouping.CUSTOM) && 1 == group.getSubtype())) + Preferences.setContentSortField(Preferences.Constant.ORDER_FIELD_TITLE); + this.group.postValue(group); - searchManager.setGroup(group); - newSearch.setValue(true); + contentSearchManager.setGroup(group); + + newContentSearch.setValue(true); // Don't search now as the UI will inevitably search as well upon switching to books view // TODO only useful when browsing custom groups ? doSearchContent(); } - public void setGrouping(@NonNull final Grouping grouping, int orderField, boolean orderDesc, int artistGroupVisibility, boolean groupFavouritesOnly) { - if (grouping.equals(Grouping.FLAT)) { - setGroup(null, false); - return; - } - - if (currentGroupsSource != null) groups.removeSource(currentGroupsSource); - currentGroupsSource = dao.selectGroupsLive(grouping.getId(), null, orderField, orderDesc, artistGroupVisibility, groupFavouritesOnly); - groups.addSource(currentGroupsSource, this::onGroupsChanged); - } - - private void onGroupsChanged(@NonNull final List newGroups) { - groups.setValue(newGroups); - currentGroupTotal.postValue(newGroups.size()); - refreshCustomGroupingAvailable(); - } - - public void refreshCustomGroupingAvailable() { - isCustomGroupingAvailable.postValue(dao.countGroupsFor(Grouping.CUSTOM) > 0); - } - // ========================= // ========= CONTENT ACTIONS // ========================= @@ -877,9 +925,9 @@ public void moveContentsToCustomGroup(long[] contentIds, @Nullable final Group g } public void resetCompletedFilter() { - if (searchManager.isFilterBookCompleted()) + if (contentSearchManager.isFilterBookCompleted()) toggleCompletedFilter(); - else if (searchManager.isFilterBookNotCompleted()) + else if (contentSearchManager.isFilterBookNotCompleted()) toggleNotCompletedFilter(); } diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/QueueViewModel.java b/app/src/main/java/me/devsaki/hentoid/viewmodels/QueueViewModel.java index 90c58d3843..b8b085f2ff 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewmodels/QueueViewModel.java +++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/QueueViewModel.java @@ -304,6 +304,7 @@ public void redownloadContent( StatusContent targetImageStatus = reparseImages ? StatusContent.ERROR : null; AtomicInteger errorCount = new AtomicInteger(0); + AtomicInteger okCount = new AtomicInteger(0); compositeDisposable.add( Observable.fromIterable(contentList) @@ -314,6 +315,7 @@ public void redownloadContent( Content content = res.right.get(); // Non-blocking performance bottleneck; run in a dedicated worker if (reparseImages) purgeItem(content); + okCount.incrementAndGet(); dao.addContentToQueue( content, targetImageStatus, position, ContentQueueManager.getInstance().isQueueActive(getApplication())); @@ -335,13 +337,13 @@ public void redownloadContent( errorCount.incrementAndGet(); onError.accept(new EmptyResultException("Redownload from scratch -> Content unreachable")); } - EventBus.getDefault().post(new ProcessEvent(ProcessEvent.EventType.PROGRESS, R.id.generic_progress, 0, contentList.size() - errorCount.get(), errorCount.get(), contentList.size())); + EventBus.getDefault().post(new ProcessEvent(ProcessEvent.EventType.PROGRESS, R.id.generic_progress, 0, okCount.get(), errorCount.get(), contentList.size())); }) .observeOn(AndroidSchedulers.mainThread()) .doOnComplete(() -> { if (Preferences.isQueueAutostart()) ContentQueueManager.getInstance().resumeQueue(getApplication()); - EventBus.getDefault().post(new ProcessEvent(ProcessEvent.EventType.COMPLETE, R.id.generic_progress, 0, contentList.size() - errorCount.get(), errorCount.get(), contentList.size())); + EventBus.getDefault().post(new ProcessEvent(ProcessEvent.EventType.COMPLETE, R.id.generic_progress, 0, okCount.get(), errorCount.get(), contentList.size())); onSuccess.accept(contentList.size() - errorCount.get()); }) .subscribe( diff --git a/app/src/main/java/me/devsaki/hentoid/widget/ContentSearchManager.java b/app/src/main/java/me/devsaki/hentoid/widget/ContentSearchManager.java deleted file mode 100644 index 6e7863a38f..0000000000 --- a/app/src/main/java/me/devsaki/hentoid/widget/ContentSearchManager.java +++ /dev/null @@ -1,171 +0,0 @@ -package me.devsaki.hentoid.widget; - -import android.net.Uri; -import android.os.Bundle; - -import androidx.lifecycle.LiveData; -import androidx.paging.PagedList; - -import java.util.ArrayList; -import java.util.List; - -import javax.annotation.Nonnull; - -import io.reactivex.Single; -import me.devsaki.hentoid.activities.bundles.SearchActivityBundle; -import me.devsaki.hentoid.database.CollectionDAO; -import me.devsaki.hentoid.database.domains.Attribute; -import me.devsaki.hentoid.database.domains.Content; -import me.devsaki.hentoid.database.domains.Group; -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_GROUP = "group"; - private static final String KEY_FILTER_FAVOURITES = "filter_favs"; - private static final String KEY_FILTER_COMPLETED_YES = "filter_completed_yes"; - private static final String KEY_FILTER_COMPLETED_NO = "filter_completed_no"; - private static final String KEY_QUERY = "query"; - private static final String KEY_SORT_FIELD = "sort_field"; - private static final String KEY_SORT_DESC = "sort_desc"; - - private final CollectionDAO collectionDAO; - - // Book favourite filter - private boolean filterBookFavourites = false; - // Book completed filter - private boolean filterBookCompleted = false; - private boolean filterBookNotCompleted = false; - // Page favourite filter - private boolean filterPageFavourites = false; - // Full-text query - private String query = ""; - // Current search tags - private List tags = new ArrayList<>(); - // Current search tags - private boolean loadAll = false; - // Group - private long groupId; - // Sort field and direction - private int contentSortField = Preferences.getContentSortField(); - private boolean contentSortDesc = Preferences.isContentSortDesc(); - - - public ContentSearchManager(CollectionDAO collectionDAO) { - this.collectionDAO = collectionDAO; - } - - public void setFilterBookFavourites(boolean filterBookFavourites) { - this.filterBookFavourites = filterBookFavourites; - } - - public boolean isFilterBookFavourites() { - return filterBookFavourites; - } - - public void setFilterBookCompleted(boolean filterBookCompleted) { - this.filterBookCompleted = filterBookCompleted; - } - - public void setFilterBookNotCompleted(boolean filterBookNotCompleted) { - this.filterBookNotCompleted = filterBookNotCompleted; - } - - public boolean isFilterBookCompleted() { - return filterBookCompleted; - } - - public boolean isFilterBookNotCompleted() { - return filterBookNotCompleted; - } - - public void setFilterPageFavourites(boolean filterPageFavourites) { - this.filterPageFavourites = filterPageFavourites; - } - - public void setLoadAll(boolean loadAll) { - this.loadAll = loadAll; - } - - 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 void setContentSortField(int contentSortField) { - this.contentSortField = contentSortField; - } - - public void setContentSortDesc(boolean contentSortDesc) { - this.contentSortDesc = contentSortDesc; - } - - public void setGroup(Group group) { - if (group != null) - groupId = group.id; - else - groupId = -1; - } - - public List getTags() { - return tags; - } - - public void clearSelectedSearchTags() { - if (tags != null) tags.clear(); - } - - - public void saveToBundle(@Nonnull Bundle outState) { - outState.putBoolean(KEY_FILTER_FAVOURITES, filterBookFavourites); - outState.putBoolean(KEY_FILTER_COMPLETED_YES, filterBookCompleted); - outState.putBoolean(KEY_FILTER_COMPLETED_NO, filterBookNotCompleted); - outState.putString(KEY_QUERY, query); - outState.putInt(KEY_SORT_FIELD, contentSortField); - outState.putBoolean(KEY_SORT_DESC, contentSortDesc); - String searchUri = SearchActivityBundle.Builder.buildSearchUri(tags).toString(); - outState.putString(KEY_SELECTED_TAGS, searchUri); - outState.putLong(KEY_GROUP, groupId); - } - - public void loadFromBundle(@Nonnull Bundle state) { - filterBookFavourites = state.getBoolean(KEY_FILTER_FAVOURITES, false); - filterBookCompleted = state.getBoolean(KEY_FILTER_COMPLETED_YES, false); - filterBookNotCompleted = state.getBoolean(KEY_FILTER_COMPLETED_NO, false); - query = state.getString(KEY_QUERY, ""); - contentSortField = state.getInt(KEY_SORT_FIELD, Preferences.getContentSortField()); - contentSortDesc = state.getBoolean(KEY_SORT_DESC, Preferences.isContentSortDesc()); - - String searchUri = state.getString(KEY_SELECTED_TAGS); - tags = SearchActivityBundle.Parser.parseSearchUri(Uri.parse(searchUri)); - groupId = state.getLong(KEY_GROUP); - } - - public LiveData> getLibrary() { - if (!getQuery().isEmpty()) - return collectionDAO.searchBooksUniversal(getQuery(), groupId, contentSortField, contentSortDesc, filterBookFavourites, loadAll, filterBookCompleted, filterBookNotCompleted); // Universal search - else if (!tags.isEmpty()) - return collectionDAO.searchBooks("", groupId, tags, contentSortField, contentSortDesc, filterBookFavourites, loadAll, filterBookCompleted, filterBookNotCompleted); // Advanced search - else - return collectionDAO.selectRecentBooks(groupId, contentSortField, contentSortDesc, filterBookFavourites, loadAll, filterBookCompleted, filterBookNotCompleted); // Default search (display recent) - } - - public Single> searchLibraryForId() { - if (!getQuery().isEmpty()) - return collectionDAO.searchBookIdsUniversal(getQuery(), groupId, contentSortField, contentSortDesc, filterBookFavourites, filterPageFavourites, filterBookCompleted, filterBookNotCompleted); // Universal search - else if (!tags.isEmpty()) - return collectionDAO.searchBookIds("", groupId, tags, contentSortField, contentSortDesc, filterBookFavourites, filterPageFavourites, filterBookCompleted, filterBookNotCompleted); // Advanced search - else - return collectionDAO.selectRecentBookIds(groupId, contentSortField, contentSortDesc, filterBookFavourites, filterPageFavourites, filterBookCompleted, filterBookNotCompleted); // Default search (display recent) - } -} diff --git a/app/src/main/java/me/devsaki/hentoid/widget/ContentSearchManager.kt b/app/src/main/java/me/devsaki/hentoid/widget/ContentSearchManager.kt new file mode 100644 index 0000000000..e28f73331e --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/widget/ContentSearchManager.kt @@ -0,0 +1,208 @@ +package me.devsaki.hentoid.widget + +import android.net.Uri +import android.os.Bundle +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import io.reactivex.Single +import me.devsaki.hentoid.activities.bundles.SearchActivityBundle +import me.devsaki.hentoid.activities.bundles.SearchActivityBundle.Companion.parseSearchUri +import me.devsaki.hentoid.database.CollectionDAO +import me.devsaki.hentoid.database.domains.Attribute +import me.devsaki.hentoid.database.domains.Content +import me.devsaki.hentoid.database.domains.Group +import me.devsaki.hentoid.util.* +import java.util.* + +class ContentSearchManager(val dao: CollectionDAO) { + + private val values = ContentSearchBundle() + + + fun toBundle(): Bundle { + val result = Bundle() + saveToBundle(result) + return result + } + + fun saveToBundle(b: Bundle) { + b.putAll(values.bundle) + } + + fun loadFromBundle(b: Bundle) { + values.bundle.putAll(b) + } + + + fun setFilterBookFavourites(value: Boolean) { + values.filterBookFavourites = value + } + + fun setFilterBookCompleted(value: Boolean) { + values.filterBookCompleted = value + } + + fun isFilterBookCompleted(): Boolean { + return values.filterBookCompleted + } + + fun setFilterBookNotCompleted(value: Boolean) { + values.filterBookNotCompleted = value + } + + fun isFilterBookNotCompleted(): Boolean { + return values.filterBookNotCompleted + } + + fun setFilterPageFavourites(value: Boolean) { + values.filterPageFavourites = value + } + + fun setLoadAll(value: Boolean) { + values.loadAll = value + } + + fun setQuery(value: String) { + values.query = value + } + + fun setContentSortField(value: Int) { + values.sortField = value + } + + fun setContentSortDesc(value: Boolean) { + values.sortDesc = value + } + + fun setGroup(value: Group?) { + if (value != null) values.groupId = value.id else values.groupId = -1 + } + + fun setTags(tags: List?) { + if (tags != null) { + values.searchUri = SearchActivityBundle.buildSearchUri(tags).toString(); + } else clearSelectedSearchTags(); + } + + fun clearSelectedSearchTags() { + values.searchUri = SearchActivityBundle.buildSearchUri(Collections.emptyList()).toString() + } + + fun clearFilters() { + clearSelectedSearchTags() + setQuery("") + setFilterBookFavourites(false) + setFilterBookCompleted(false) + setFilterBookNotCompleted(false) + setFilterPageFavourites(false) + } + + fun getLibrary(): LiveData> { + val tags = parseSearchUri(Uri.parse(values.searchUri)) + return when { + values.query.isNotEmpty() -> dao.searchBooksUniversal( + values.query, + values.groupId, + values.sortField, + values.sortDesc, + values.filterBookFavourites, + values.loadAll, + values.filterBookCompleted, + values.filterBookNotCompleted + ) // Universal search + tags.isNotEmpty() -> dao.searchBooks( + "", + values.groupId, + tags, + values.sortField, + values.sortDesc, + values.filterBookFavourites, + values.loadAll, + values.filterBookCompleted, + values.filterBookNotCompleted + ) // Advanced search + else -> dao.selectRecentBooks( + values.groupId, + values.sortField, + values.sortDesc, + values.filterBookFavourites, + values.loadAll, + values.filterBookCompleted, + values.filterBookNotCompleted + ) + } // Default search (display recent) + } + + fun searchLibraryForId(): Single> { + val tags = parseSearchUri(Uri.parse(values.searchUri)) + return when { + values.query.isNotEmpty() -> dao.searchBookIdsUniversal( + values.query, + values.groupId, + values.sortField, + values.sortDesc, + values.filterBookFavourites, + values.filterPageFavourites, + values.filterBookCompleted, + values.filterBookNotCompleted + ) // Universal search + tags.isNotEmpty() -> dao.searchBookIds( + "", + values.groupId, + tags, + values.sortField, + values.sortDesc, + values.filterBookFavourites, + values.filterPageFavourites, + values.filterBookCompleted, + values.filterBookNotCompleted + ) // Advanced search + else -> dao.selectRecentBookIds( + values.groupId, + values.sortField, + values.sortDesc, + values.filterBookFavourites, + values.filterPageFavourites, + values.filterBookCompleted, + values.filterBookNotCompleted + ) + } // Default search (display recent) + } + + + // INNER CLASS + + class ContentSearchBundle(val bundle: Bundle = Bundle()) { + + var loadAll by bundle.boolean(default = false) + + var filterPageFavourites by bundle.boolean(default = false) + + var filterBookFavourites by bundle.boolean(default = false) + + var filterBookCompleted by bundle.boolean(default = false) + + var filterBookNotCompleted by bundle.boolean(default = false) + + var query by bundle.string(default = "") + + var sortField by bundle.int(default = Preferences.getContentSortField()) + + var sortDesc by bundle.boolean(default = Preferences.isContentSortDesc()) + + var searchUri by bundle.string(default = "") + + var groupId by bundle.long(default = -1) + + + fun isFilterActive(): Boolean { + val tags = SearchActivityBundle.parseSearchUri(Uri.parse(searchUri)) + return query.isNotEmpty() + || tags.isNotEmpty() + || filterBookFavourites + || filterBookCompleted + || filterBookNotCompleted + || filterPageFavourites + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/widget/GroupSearchManager.kt b/app/src/main/java/me/devsaki/hentoid/widget/GroupSearchManager.kt new file mode 100644 index 0000000000..db1884b8d3 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/widget/GroupSearchManager.kt @@ -0,0 +1,104 @@ +package me.devsaki.hentoid.widget + +import android.os.Bundle +import androidx.lifecycle.LiveData +import me.devsaki.hentoid.database.CollectionDAO +import me.devsaki.hentoid.database.domains.Group +import me.devsaki.hentoid.enums.Grouping +import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.boolean +import me.devsaki.hentoid.util.int +import me.devsaki.hentoid.util.string + +class GroupSearchManager(val dao: CollectionDAO) { + + private val values = GroupSearchBundle() + + + fun toBundle(): Bundle { + val result = Bundle() + saveToBundle(result) + return result + } + + fun saveToBundle(b: Bundle) { + b.putAll(values.bundle) + } + + fun loadFromBundle(b: Bundle) { + values.bundle.putAll(b) + } + + fun setFilterFavourites(value: Boolean) { + values.filterFavourites = value + } + + fun setQuery(value: String) { + values.query = value + } + + fun setGrouping(value: Grouping) { + values.groupingId = value.id + } + + fun setArtistGroupVisibility(value: Int) { + values.artistGroupVisibility = value + } + + fun setSortField(value: Int) { + values.sortField = value + } + + fun setSortDesc(value: Boolean) { + values.sortDesc = value + } + + fun clearFilters() { + setQuery("") + setArtistGroupVisibility(Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS_GROUPS) + setFilterFavourites(false) + } + + fun getGroups(): LiveData> { + return dao.selectGroupsLive( + values.groupingId, + values.query, + values.sortField, + values.sortDesc, + values.artistGroupVisibility, + values.filterFavourites + ) + } + + fun getAllGroups(): LiveData> { + return dao.selectGroupsLive( + values.groupingId, + "", + values.sortField, + values.sortDesc, + Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS_GROUPS, + false + ) + } + + class GroupSearchBundle(val bundle: Bundle = Bundle()) { + + var filterFavourites by bundle.boolean(default = false) + + var artistGroupVisibility by bundle.int(default = Preferences.getArtistGroupVisibility()) + + var query by bundle.string(default = "") + + var groupingId by bundle.int(default = Preferences.getGroupingDisplay().id) + + var sortField by bundle.int(default = Preferences.getGroupSortField()) + + var sortDesc by bundle.boolean(default = Preferences.isGroupSortDesc()) + + fun isFilterActive(): Boolean { + return query.isNotEmpty() + || artistGroupVisibility != Preferences.Constant.ARTIST_GROUP_VISIBILITY_ARTISTS_GROUPS + || filterFavourites + } + } +} \ No newline at end of file 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 5b8818a0ae..58e8f62698 100644 --- a/app/src/main/java/me/devsaki/hentoid/widget/ScrollPositionListener.java +++ b/app/src/main/java/me/devsaki/hentoid/widget/ScrollPositionListener.java @@ -74,16 +74,20 @@ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newStat if (!(llm.findLastVisibleItemPosition() == llm.getItemCount() - 1 || 0 == llm.findFirstVisibleItemPosition())) return; + if (null == onEndOutOfBoundScroll || null == onStartOutOfBoundScroll) return; + int scrollDirection = 0; if (llm instanceof PrefetchLinearLayoutManager) scrollDirection = ((PrefetchLinearLayoutManager) llm).getRawDeltaPx(); if (recyclerView.computeHorizontalScrollOffset() == dragStartPositionX && !isSettlingX && llm.canScrollHorizontally()) { - if ((!llm.getReverseLayout() && scrollDirection >= 0) || (llm.getReverseLayout() && scrollDirection < 0)) onEndOutOfBoundScroll.run(); + if ((!llm.getReverseLayout() && scrollDirection >= 0) || (llm.getReverseLayout() && scrollDirection < 0)) + onEndOutOfBoundScroll.run(); else onStartOutOfBoundScroll.run(); } if (recyclerView.computeVerticalScrollOffset() == dragStartPositionY && !isSettlingY && llm.canScrollVertically()) { - if ((!llm.getReverseLayout() && scrollDirection >= 0) || (llm.getReverseLayout() && scrollDirection < 0)) onEndOutOfBoundScroll.run(); + if ((!llm.getReverseLayout() && scrollDirection >= 0) || (llm.getReverseLayout() && scrollDirection < 0)) + onEndOutOfBoundScroll.run(); else onStartOutOfBoundScroll.run(); } } diff --git a/app/src/main/java/me/devsaki/hentoid/workers/ContentDownloadWorker.java b/app/src/main/java/me/devsaki/hentoid/workers/ContentDownloadWorker.java index 89d9aad405..1e1af46b05 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/ContentDownloadWorker.java +++ b/app/src/main/java/me/devsaki/hentoid/workers/ContentDownloadWorker.java @@ -725,12 +725,17 @@ private void completeDownload(final long contentId, @NonNull final String title, // Compute perceptual hash for the cover picture ContentHelper.computeAndSaveCoverHash(getApplicationContext(), content, dao); - // Mark content as downloaded + // Mark content as downloaded (download processing date; if none set before) if (0 == content.getDownloadDate()) content.setDownloadDate(Instant.now().toEpochMilli()); - content.setStatus((0 == pagesKO && !hasError) ? StatusContent.DOWNLOADED : StatusContent.ERROR); - // Clear download params from content - if (0 == pagesKO && !hasError) content.setDownloadParams(""); + + if (0 == pagesKO && !hasError) { + content.setDownloadParams(""); + content.setDownloadCompletionDate(Instant.now().toEpochMilli()); + content.setStatus(StatusContent.DOWNLOADED); + } else { + content.setStatus(StatusContent.ERROR); + } content.computeSize(); // Save JSON file diff --git a/app/src/main/res/color/secondary_color_selector_check.xml b/app/src/main/res/color/secondary_color_selector_check.xml new file mode 100644 index 0000000000..3243206a5a --- /dev/null +++ b/app/src/main/res/color/secondary_color_selector_check.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/secondary_text_color_selector_check.xml b/app/src/main/res/color/secondary_text_color_selector_check.xml new file mode 100644 index 0000000000..2cf6a5dcf3 --- /dev/null +++ b/app/src/main/res/color/secondary_text_color_selector_check.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_action_sort.xml similarity index 100% rename from app/src/main/res/drawable/ic_sort.xml rename to app/src/main/res/drawable/ic_action_sort.xml diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_checked.xml similarity index 100% rename from app/src/main/res/drawable/ic_check.xml rename to app/src/main/res/drawable/ic_checked.xml diff --git a/app/src/main/res/drawable/ic_checked_circle.xml b/app/src/main/res/drawable/ic_checked_circle.xml index e545abcc69..0c61bb1852 100644 --- a/app/src/main/res/drawable/ic_checked_circle.xml +++ b/app/src/main/res/drawable/ic_checked_circle.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_check_square.xml b/app/src/main/res/drawable/ic_checked_square.xml similarity index 100% rename from app/src/main/res/drawable/ic_check_square.xml rename to app/src/main/res/drawable/ic_checked_square.xml diff --git a/app/src/main/res/drawable/ic_completed_single.xml b/app/src/main/res/drawable/ic_completed_empty.xml similarity index 100% rename from app/src/main/res/drawable/ic_completed_single.xml rename to app/src/main/res/drawable/ic_completed_empty.xml diff --git a/app/src/main/res/drawable/ic_completed_filter_off.xml b/app/src/main/res/drawable/ic_completed_filter_off.xml deleted file mode 100644 index 64237f0c40..0000000000 --- a/app/src/main/res/drawable/ic_completed_filter_off.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_completed_filter_on.xml b/app/src/main/res/drawable/ic_completed_filter_on.xml deleted file mode 100644 index 13ae5345c2..0000000000 --- a/app/src/main/res/drawable/ic_completed_filter_on.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_uncheck.xml b/app/src/main/res/drawable/ic_unchecked.xml similarity index 100% rename from app/src/main/res/drawable/ic_uncheck.xml rename to app/src/main/res/drawable/ic_unchecked.xml diff --git a/app/src/main/res/drawable/ic_action_gallery.xml b/app/src/main/res/drawable/ic_view_gallery.xml similarity index 100% rename from app/src/main/res/drawable/ic_action_gallery.xml rename to app/src/main/res/drawable/ic_view_gallery.xml diff --git a/app/src/main/res/drawable/ic_view_list.xml b/app/src/main/res/drawable/ic_view_list.xml new file mode 100644 index 0000000000..e354868f09 --- /dev/null +++ b/app/src/main/res/drawable/ic_view_list.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml index 8b5bf8bb67..d46c8c628b 100644 --- a/app/src/main/res/layout/fragment_library.xml +++ b/app/src/main/res/layout/fragment_library.xml @@ -28,7 +28,7 @@ app:menu="@menu/library_selection_menu" app:navigationIcon="@drawable/ic_arrow_back" /> - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/include_library_search_sort_bar.xml b/app/src/main/res/layout/include_library_search_sort_bar.xml deleted file mode 100644 index a76f790141..0000000000 --- a/app/src/main/res/layout/include_library_search_sort_bar.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/include_library_search_subbar.xml b/app/src/main/res/layout/include_library_search_subbar.xml new file mode 100644 index 0000000000..cc5bcfcf93 --- /dev/null +++ b/app/src/main/res/layout/include_library_search_subbar.xml @@ -0,0 +1,44 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/include_library_sort_filter_bottom_panel.xml b/app/src/main/res/layout/include_library_sort_filter_bottom_panel.xml new file mode 100644 index 0000000000..565f62a020 --- /dev/null +++ b/app/src/main/res/layout/include_library_sort_filter_bottom_panel.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/include_search_filter_category.xml b/app/src/main/res/layout/include_search_bottom_panel.xml similarity index 93% rename from app/src/main/res/layout/include_search_filter_category.xml rename to app/src/main/res/layout/include_search_bottom_panel.xml index 24e84fa3ca..b809a6e3b8 100644 --- a/app/src/main/res/layout/include_search_filter_category.xml +++ b/app/src/main/res/layout/include_search_bottom_panel.xml @@ -12,17 +12,17 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:gravity="center" - android:text="@string/app_intro" android:textSize="24sp" app:layout_constraintBottom_toTopOf="@+id/tag_wait_image" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:text="Category search" /> @@ -32,10 +32,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" - android:text="@string/app_intro" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/tag_suggestion" - app:layout_constraintTop_toBottomOf="@+id/tag_wait_image" /> + app:layout_constraintTop_toBottomOf="@+id/tag_wait_image" + tools:text="X results" /> diff --git a/app/src/main/res/menu/library_books_sort_popup.xml b/app/src/main/res/menu/library_books_sort_popup.xml deleted file mode 100644 index a73663dc99..0000000000 --- a/app/src/main/res/menu/library_books_sort_popup.xml +++ /dev/null @@ -1,37 +0,0 @@ - -

- - - - - - - - - - - diff --git a/app/src/main/res/menu/library_groups_popup.xml b/app/src/main/res/menu/library_groups_popup.xml deleted file mode 100644 index 5909f95f15..0000000000 --- a/app/src/main/res/menu/library_groups_popup.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/menu/library_groups_sort_popup.xml b/app/src/main/res/menu/library_groups_sort_popup.xml deleted file mode 100644 index e78d04aee7..0000000000 --- a/app/src/main/res/menu/library_groups_sort_popup.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/menu/library_groups_visibility_popup.xml b/app/src/main/res/menu/library_groups_visibility_popup.xml deleted file mode 100644 index 03ef2bd418..0000000000 --- a/app/src/main/res/menu/library_groups_visibility_popup.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/app/src/main/res/menu/library_menu.xml b/app/src/main/res/menu/library_menu.xml index f80a415b4d..dfe2916caa 100644 --- a/app/src/main/res/menu/library_menu.xml +++ b/app/src/main/res/menu/library_menu.xml @@ -12,6 +12,13 @@ android:tooltipText="@string/menu_search" app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="collapseActionView|always" /> + - - + app:showAsAction="ifRoom" /> + + + diff --git a/app/src/main/res/menu/library_selection_menu.xml b/app/src/main/res/menu/library_selection_menu.xml index b977b80a99..57537e53e1 100644 --- a/app/src/main/res/menu/library_selection_menu.xml +++ b/app/src/main/res/menu/library_selection_menu.xml @@ -19,7 +19,7 @@ app:showAsAction="always" /> diff --git a/app/src/main/res/raw/sites.json b/app/src/main/res/raw/sites.json index 43d05655b6..51c8085796 100644 --- a/app/src/main/res/raw/sites.json +++ b/app/src/main/res/raw/sites.json @@ -7,7 +7,8 @@ "requestsCapPerSecond": 2 }, "NHENTAI": { - "useHentoidAgent": true + "useHentoidAgent": false, + "useWebviewAgent": false }, "TSUMINO": { "useHentoidAgent": true diff --git a/app/src/main/res/values-es/strings_slides.xml b/app/src/main/res/values-es/strings_slides.xml index df09794f80..659a74a316 100644 --- a/app/src/main/res/values-es/strings_slides.xml +++ b/app/src/main/res/values-es/strings_slides.xml @@ -1,19 +1,19 @@ -¡Bienvenido a Hentoid! + ¡Bienvenido a Hentoid! Hentoid es una aplicación para archivar Doujinshi y manga Hentai. -Permisos + Permisos Necesitamos de tu permiso para que se pueda almacenar archivos en tu dispositivo. -Librería + Librería Hentoid requiere una carpeta dedicada para almacenar tus archivos. Omitir ¿Omitir? No se podrá hacer una descarga si omites este paso. Para hacer una descarga tendrás que configurar una carpeta desde la configuración de la aplicación. -Tema + Tema Escoge un tema. -Fuentes + Fuentes Selecciona una fuente de donde quieres descargar. Puedes cambiar esta selección en cualquier momento al utilizar el botón Edición en el menú izquierdo. -¡Todo esta configurado! + ¡Todo esta configurado! ¡Estamos listos! Disfruta de la aplicación. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 30cff11ab3..47517e7042 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -20,7 +20,6 @@ fichiers fichiers - Annuler Arrêter Reprendre Annuler @@ -60,7 +59,6 @@ Le nouvel ordre personnalisé que vous êtes sur le point de créer écrasera le précédent. Ordonner les livres Supprimer - Supprimer des éléments Déplacer dans le groupe… Ouvrir le dossier contenant Retélécharger @@ -93,10 +91,7 @@ Inverser la file d\'attente Tout retélécharger Annuler toutes les erreurs - Annulé Déplacer vers le haut - Monter - Descendre Déplacer vers le bas File d\'attente en pause diff --git a/app/src/main/res/values-fr/strings_slides.xml b/app/src/main/res/values-fr/strings_slides.xml index 262fafa597..c27496736a 100644 --- a/app/src/main/res/values-fr/strings_slides.xml +++ b/app/src/main/res/values-fr/strings_slides.xml @@ -1,19 +1,19 @@ -Bienvenue sur Hentoid ! + Bienvenue sur Hentoid ! Hentoid est une appli d\'archivage \nde doujinshi et de manga h. -Autorisations + Autorisations Nous devons vous demander la permission de stocker des fichiers sur votre appareil. -Bibliothèque + Bibliothèque Hentoid nécessite un dossier dédié pour le stockage de vos médias. Ignorer Ignorer \? Vous ne pourrez pas télécharger si vous sautez cette étape. \nPour pouvoir télécharger, vous devrez définir le dossier de téléchargement dans les paramètres de l\'application. -Thème + Thème Choisissez un thème d\'affichage. -Sources + Sources Sélectionnez les sources de téléchargement. Vous pouvez modifier cette sélection ultérieurement, à tout moment, en utilisant le bouton d\'édition du menu de gauche. -Tout est prêt ! + Tout est prêt ! Nous sommes prêts ! \nProfitez bien de l\'appli. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index b70e87aa8d..0759c89367 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -8,9 +8,9 @@ Ki Összes kiválasztása Egyik sem - Tovább az oldalra... + Tovább az oldalra… Elfogadás - Kérlek várj... + Kérlek várj… könyv könyvek @@ -23,7 +23,6 @@ fájl fájlok - Visszavonás Megállítás Leállítás Folytatás @@ -49,9 +48,9 @@ Hentoid mappa kiválasztása Külső mappa kiválasztása Webhely mappáinak beolvasása - Könyvtár beolvasása... + Könyvtár beolvasása… Könyvtár beolvasása (%1$d / %2$d) - A várólista importálása... + A várólista importálása… Kérjük, válasszon egy már létező Hentoid mappát. A mappa helye megjelenik a képernyőn. Kérjük, engedélyezze az írási jogosultságot Keresés @@ -65,8 +64,7 @@ Az új rendezés felülfogja írni az előzőt. Könyvek rendezése Törlés - Elemek törlése - Csoporthoz hozzárendelés... + Csoporthoz hozzárendelés… Tartalmazó mappa megnyitása Újratöltés Újratöltés a nulláról @@ -97,10 +95,7 @@ Várakozási sor megfordítása Összes újbóli letöltése Összes hibás visszavonása - Visszavonva Mozgatás a legtetejére - Mozgatás fölfelé - Mozgatás lefelé Mozgatás az aljára Várakozási sor megállítva Letöltés előkészítése @@ -184,10 +179,9 @@ Hentoid könyvtár Külső könyvtár Az összes mappa átnevezése az aktuális elnevezési és maximális hosszúsági beállításoknak megfelelően - Távolítsa el a mappákat, ahol... + Távolítsa el a mappákat, ahol… JSON fájl hiányzik (ajánlott) Nincs letöltött kép - A JSON fájl nem olvasható (nem ajánlott) vagy Törölje az összeset a kedvencek kivételével %1$d / %2$d %3$s @@ -251,7 +245,6 @@ EXPORTÁLÁS ELINDÍTÁSA Beállítások importálása Importálva -Alkalmazás beállításainak megnyitása A folytatáshoz szükségünk lenne néhány engedély elfogadásához A folytatás előtt manuálisan meg kell adnia az engedélyeket. Az importálás megszakítva @@ -267,10 +260,7 @@ Importált könyvtárak Importálás elkezdése Könyvtár importálása - Importálás, kérjük várjon... - Frissítés, kérjük várjon... Importálás befejezve - Importálás befejezve %s sikeresen importálva %s sikeresen importálva @@ -302,7 +292,6 @@ Érvénytelen mappa Hiba a fájl megnyitásakor: nem találtunk ehhez a fájlhoz releváns alkalmazást Hiba a link megnyitásakor: nem találtunk böngészőt az eszközén - Hiba az archívum elküldésekor: nem találtunk az archívum elküldéséhez releváns alkalmazást Megpróbáljuk újra letölteni\? Letöltött képek száma: %1$d\nHibamentesen letöltött képek: %2$d\nHibásan letöltött képek: %3$d Első hiba: %s @@ -361,7 +350,6 @@ Betöltés~ Nincsen eredmény :( Ez nincs itt, menj és töltsd le. - Fájlok becsomagolása. \nKérjük, várjon... Keresés \'%s\' ban/ben Indítási kód: %s @@ -372,7 +360,6 @@ %1$d/%2$d eredmény %1$d/%2$d eredmények - Az engedélyek visszaállítása megtörtént.\nAz alkalmazás visszaállítása. \"A változtatások alkalmazásra kerülnek a Hentoid újraindításakor\" A kiválasztott könyv újbóli letöltése a nulláról\? @@ -398,7 +385,6 @@ Egy kiválasztott könyv már beállítva streamelésre vagy már a külső könyvtárban van\nEz a könyv figyelmen kívül lesz hagyva %d kiválasztott könyv már beállítva streamelésre vagy már a külső könyvtárban vannak\nEzek a kiválasztott könyvek figyelmen kívül lesznek hagyva, - A könyvet hamarosan töröljük... Keressen bármire: cím, szerző, címke\\és indítókód is! Gyorsabban böngészhet az oldalszámok közötti csúsztatással @@ -414,7 +400,7 @@ Beállítás mint, a csoport borítójaként Ennek a könyvnek a borítóképe beállítása a jelenlegi csoport borítóképére\? - Egyéni csoporthoz hozzárendelés... + Egyéni csoporthoz hozzárendelés… Létező csoport Új létrehozása Új egyedi csoport név @@ -437,8 +423,8 @@ Nincs mit törölni - ellenőrizze a \"Külső tartalom törlésének engedélyezése\" lehetőséget, hogy ez törölhető lehessen Egyesítés - Könyvek egyesítése... - Könyvek szétválasztása... + Könyvek egyesítése… + Könyvek szétválasztása… Szétválasztás Új cím Ezen könyvek törlése egyesítés után @@ -483,10 +469,6 @@ Minden kijelölt fejezet egy új könyvet hoz létre
Frissítés letöltése Készen állunk a frissítés telepítésére! Koppintson a telepítéshez! - Hiba a frissítési függőség elemzése során.\nKérlek, próbáld meg később újra. - Hálózati hiba történt, kérjük próbálja meg később. - Nem található meg a frissítési fájl a szerveren - Frissítés ellenőrző: Nincsen új frissítés. Kérjük figyeljen arra, hogy a letöltések ezen az oldalon meghiúsulhatnak A letöltési gomb jelenleg nem használható ezen az oldalon Ez az oldal jelenleg komoly stabilitási problémákkal küzd. Az alkalmazás nem tud mit kezdeni vele. Kérjük, próbálja meg később újra. @@ -494,12 +476,10 @@ Minden kijelölt fejezet egy új könyvet hoz létre A Hentoid fejlesztőcsapat dolgozik a javításon. Az alkalmazás értesítést fog küldeni, ha az új verzió elkészül. A javítás elérhető az alkalmazás legújabb verziójában. Kérjük, frissítse az alkalmazást. Kérjük írja be a PIN-kódot - Alkalmazászár PIN-kód Alkalmazászár bekapcsolva Alkalmazászár kikapcsolva PIN-kód alaphelyzetbe állítása PIN-kód alaphelyzetbe állítása - PIN RENDBEN! Kérjük írja be a jelenlegi PIN-kódját Kérjük írja be az új PIN-kódot Kérjük írja be újra az új PIN-kódot @@ -535,8 +515,6 @@ háttérből Kezdőlap Torrent letöltése sikertelen: %s Tárolási engedély megtagadva - a letöltő nem használható -Kérjük, hogy használjon vagy hozzon létre egy legális FAKKU fiókot.\nHentoid nem fog működni anélkül. - Ezt a könyvet a jelenlegi FAKKU hozzáféréseddel nem tudod megtekinteni. Kérjük, használjon vagy hozzon létre egy exHentai fiókot. Érvénytelen belépési adatok, kérjük jelentkezzen be újra Nem teljesek a belépési adatok, kérjük, jelentkezzen be újra. Ha a probléma továbbra is fennáll, fontolja meg, hogy más országból lépjen be a webhelyre, vagy hozzon létre új fiókot. @@ -566,11 +544,9 @@ háttérből %s találat Szűrő kiválasztása -Karbantartás elvégzése Nem sikerült betölteni a könyvet: Nem található kép Betöltés: %1$d/%2$d Kép betöltése: %d%% - A kép nem található %1$d x %2$d (skála %3$.0f%%) - %4$s Válassza ki az olvasás irányát Balról, jobbra @@ -585,12 +561,9 @@ háttérből Összes oldal mutatása A kedvenc oldal megjelenítésének beállítása Borítóképként beállítás - Oldalmenü - Oldal tulajdonságainak megnyitása Könyv törlése Átmásolva a Letöltések mappába Nem lehet írni a Letöltések mappába - Töröljük ezt a könyvet\? Töröljük ezt az oldalt\? Információ Előző könyv @@ -607,14 +580,10 @@ háttérből Szeretné beállítani ezt az oldalt a könyv borítójára? A kép nem található - Kép streamelése... + Kép streamelése… Könyv beállítások Alkalmazás beállítások Alkalmazás beállításainak használata (%s) - Az oldal részletei - A könyv részletei - Kedvenc oldalra váltás - Kedvenc könyvre váltás Oldal kedvencnek jelölve Oldal kedvencekből kijelölés Könyv kedvencnek jelölve @@ -675,7 +644,6 @@ háttérből Borítókép: nincsen adat Szerző: %.0f%% Szerző: nincsen adat - Elfogadás (%d választási lehetőséget) Választások elmentése Duplikátum felderítés elkezdődött Duplikátum felderítés befejeződött @@ -684,7 +652,7 @@ háttérből %d duplikátumok detektálva Semmi sem jeleníthető meg - Új duplikátum keresés futtatása - Duplikátumok felderítése... + Duplikátumok felderítése… %d duplikátum %d duplikátumok @@ -695,14 +663,12 @@ háttérből Duplikátum figyelmeztetés! A letölteni kívánt könyv lehet, hogy egy olyan könyv másolata, amely már a tulajdonában van. A könyv, amelyből oldalakat fog letölteni, nem feltétlenül az a könyv, amelyik már a tulajdonában van. - Könyvtárban - Várakozási sorban van Mindig töltsd le, és ne kérdezd újra Soha többé ne adjon hozzá oldalakat potenciális másodpéldányokból Hasonlóság: %.0f%% Könyv letöltése Extra oldalak letöltése - Indexelés... + Indexelés… Tartalom archiválása Tartalom törlése A törlés sikeres @@ -713,7 +679,7 @@ háttérből A törlés sikertelen Legalább egy könyvet nem sikerült törölni Indítás - Kezdjük... + Kezdjük… Indítás befejezve Ma Az elmúlt 7napban @@ -743,7 +709,6 @@ háttérből török cseh Válassza ki a fájlkezelőjét - Alapértelmezett jogosultság bájt/bájtok KB MB diff --git a/app/src/main/res/values-hu/strings_slides.xml b/app/src/main/res/values-hu/strings_slides.xml index f34af8f65c..6be930dc54 100644 --- a/app/src/main/res/values-hu/strings_slides.xml +++ b/app/src/main/res/values-hu/strings_slides.xml @@ -1,19 +1,19 @@ -Üdvözlünk a Hentoid alkalmazásban! + Üdvözlünk a Hentoid alkalmazásban! A Hentoid az egy Doujinshi\n& Hentai Manga archiváló alkalmazás. -Engedélyek + Engedélyek Szükségünk van néhány engedélyre, hogy adatokat tárolhassunk a eszközén. -Könyvtár + Könyvtár A Hentoid alkalmazásnak szüksége van egy dedikált mappának, a médiád eltárolásához. Átugrás Átugrás\? Nem fogsz tudni letölteni, hogyha kihagyod ezt a lépést.\nAhhoz hogy letudj tölteni, be kell állítanod a letöltési mappát az alkalmazás beállításában. -Téma + Téma Kérjük, válasszon egy megjelenítési témát. -Források + Források Válaszd ki a forrásokat ahonnan leszeretnél tölteni. A választását később bármikor megváltoztathatja a bal oldali menüben található szerkesztés gomb segítségével. -Minden készen áll! + Minden készen áll! Készen is vagyunk!\nÉlvezd az alkalmazást. diff --git a/app/src/main/res/values-it/array_preferences.xml b/app/src/main/res/values-it/array_preferences.xml index b4b70ac19f..4127214385 100644 --- a/app/src/main/res/values-it/array_preferences.xml +++ b/app/src/main/res/values-it/array_preferences.xml @@ -38,7 +38,7 @@ 100 caratteri 120 caratteri Scarica tutte le pagine - Trasmetti in streaming le pagine durante la lettura + Mostra le pagine mentre leggi Alfabetico Conteggio degli elementi (i più usati per primi) Nessuno diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c05c08cd88..57040cd86e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -23,6 +23,7 @@ File Files + Gruppi Pausa Ferma Riprendi @@ -33,6 +34,7 @@ Apri cartella predefinito vuoto + Nessuno Migrazione API 29 Libreria Coda @@ -55,11 +57,11 @@ Si prega di consentire il permesso di scrittura Cerca Attiva/disattiva filtro preferito - Attiva/disattiva filtro completato Attiva/disattiva preferito Nuovo gruppo - Modifica ordine - Modifica nome + Riordina + Rinomina + Conferma modifica Annulla modifica Il nuovo ordine personalizzato che stai per creare sovrascriverà quello precedente. Ordina libri @@ -303,9 +305,6 @@ Riprendere a leggere da dove l\'hai lasciato\? Apri il cassetto Chiudi il cassetto - Come vorresti filtrare i libri completati\? - Mostra solo completati - Mostra solo i non completati Cancellare l\'elemento selezionato\? Cancellare %d elementi selezionati\? @@ -437,13 +436,17 @@ Questo libro sarà ignorato. Contenuto diviso Tutti i libri Per artista - Per data di download - Personalizzato + Per data di download (processato) + Gruppi personalizzati [nessun artista] -Titolo +Ascendente + Discendente + Rimescola + Titolo Artista Pagine - Data di download + Data download (processato) + Data download (completato) Leggi data Legge Dimensione @@ -452,15 +455,21 @@ Questo libro sarà ignorato. Personalizzato -invalido- Libri + Data di caricamento + Filtri + Attiva/disattiva visualizzazione dei libri preferiti + Mostra solo libri finiti + Mostra solo libri non finiti +Mostra gruppi + Mostra + Artisti + Gruppi Trascina elemento Appartiene alla libreria esterna Libro in streaming Visualizza sorgente nel browser Segna/deseleziona come preferito -Mostra artisti - Mostra gruppi - Mostra artisti e gruppi - Hai già questo libro. Cercare altre fonti\? +Hai già questo libro. Cercare altre fonti\? Aggiornamenti Verifica aggiornamenti È disponibile un aggiornamento! @@ -560,7 +569,7 @@ Questo libro sarà ignorato. Attiva/disattiva il rimescolamento delle pagine Mostra solo le pagine preferite Mostra tutte le pagine - Attiva/disattiva la visualizzazione della pagina preferita + Attiva/disattiva visualizzazione delle pagine preferite Imposta come copertina Cancella libro Copiato nella cartella Downloads diff --git a/app/src/main/res/values-it/strings_settings.xml b/app/src/main/res/values-it/strings_settings.xml index 6d2c75f770..3fc612faae 100644 --- a/app/src/main/res/values-it/strings_settings.xml +++ b/app/src/main/res/values-it/strings_settings.xml @@ -11,6 +11,9 @@ Scorri fino al contenuto del tuo cuore. Quantità per pagina Numero di elementi per pagina nell\'elenco dei download. \nAttualmente: %s elementi + Pulsante vai in cima + Pulsante disattivato + Pulsante attivato Ricerca avanzata Ordine di selezione del metadata visualizzato Conta i libri disponibili @@ -35,7 +38,7 @@ Riduci il nome della cartella \nAttualmente: %s Utilizzo della memoria Spazio libero attuale: %.2f%% (tocca per i dettagli) - Avvisa con memoria bassa + Avvisa con memoria bassa Libreria esterna Libreria esterna Nessuna libreria esterna impostata diff --git a/app/src/main/res/values-it/strings_slides.xml b/app/src/main/res/values-it/strings_slides.xml index 8d2dbe0b0b..a2117e9023 100644 --- a/app/src/main/res/values-it/strings_slides.xml +++ b/app/src/main/res/values-it/strings_slides.xml @@ -1,19 +1,19 @@ -Benvenuto su Hentoid ! - Hentoid è un\'app di archiviazione \ndi Doujinshi e H-manga. -Permessi + Benvenuto su Hentoid! + Hentoid è un app di archiviazione per\n& Doujinshi e H-manga. + Permessi Dobbiamo chiederti l\'autorizzazione per archiviare i file sul tuo dispositivo. -Libreria + Libreria Hentoid richiede una cartella dedicata per salvare i vostri media. Salta Saltare\? - Non potrai scaricare se salti questo passaggio. \nPer scaricare, dovrai impostare la cartella di download tramite le impostazioni dell\'app. -Tema + Non potrai scaricare se salti questo passaggio.\nPer scaricare, dovrai impostare la cartella di download tramite le impostazioni dell\'app. + Tema Scegli un tema di visualizzazione. -Fonti + Fonti Seleziona le fonti da cui scaricare. - Puoi cambiare questa selezione in un\'altro momento utilizzando il pulsante di modifica nel menu a sinistra. -Tutto pronto! + Puoi cambiare questa selezione in un altro momento utilizzando il pulsante di modifica nel menu a sinistra. + Tutto pronto! Siamo pronti! \nGodetevi l\'applicazione. diff --git a/app/src/main/res/values-nb-rNO/strings_slides.xml b/app/src/main/res/values-nb-rNO/strings_slides.xml index a9c272bbcc..6d3f277311 100644 --- a/app/src/main/res/values-nb-rNO/strings_slides.xml +++ b/app/src/main/res/values-nb-rNO/strings_slides.xml @@ -1,19 +1,19 @@ -Velkommen til Hentoid. + Velkommen til Hentoid. Hentoid is a Doujinshi \nog H-Manga-arkiveringsprogram -Tilganger + Tilganger Lagringstilgang er nødvendig for å lagre filer på enheten din. -Bibliotek + Bibliotek Hentoid krever at du har en egen mappe til lagring av media. Hopp over Hopp over\? Du vil ikke kunne laste ned hvis du hopper over dette steget \nDet er mulig å sette opp en nedlastingsmappe i programinnstillingene -Drakt + Drakt Velg en visningsdrakt. -Kilder + Kilder Velg kilder å laste ned fra. Du kan endre dette senere ved å bruke redigeringsknappen i menyen til venstre. -Alt er satt opp. + Alt er satt opp. Alt klart. \nNyt programmet. diff --git a/app/src/main/res/values-pt-rBR/array_plug_reactions.xml b/app/src/main/res/values-pt-rBR/array_plug_reactions.xml new file mode 100644 index 0000000000..d9c0b25e89 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/array_plug_reactions.xml @@ -0,0 +1,16 @@ + + +yametee~~♡ + pare, esse é… o buraco errado~ + ughhh~ + ewww ♡ ♡ + não aqui~~♡ + mau, menino mau~~ + muito... apertado... ♡ +Eu gosto de lá~~ + empurre-o ainda mais ~~~♡ + ikuuuu~~ + ahh ♡ + ohhh~ + me dê mais ♡ + diff --git a/app/src/main/res/values-pt-rBR/strings_download_service.xml b/app/src/main/res/values-pt-rBR/strings_download_service.xml new file mode 100644 index 0000000000..b6bcff40e0 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings_download_service.xml @@ -0,0 +1,16 @@ + + + + Download concluído + %d downloads concluídos + + Download concluído + Download cancelado + Download concluído, mas ocorreu um erro + + %1$d/%2$ditem adicionado à fila + %1$d/%2$ditens adicionados à fila + + Já baixado + Já na fila + \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings_slides.xml b/app/src/main/res/values-pt-rBR/strings_slides.xml new file mode 100644 index 0000000000..d53b6b5ed2 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings_slides.xml @@ -0,0 +1,19 @@ + + + Bem-vindo ao Hentoid! + Hentoid é um aplicativo de arquivamento Doujinshi\n& H-Manga. + Permissões + Precisamos pedir sua permissão para armazenar arquivos em seu dispositivo. + Biblioteca + Hentoid requer uma pasta dedicada para armazenar sua mídia. +Pular +Pular\? + Você não poderá fazer o download se pular esta etapa.\nln para fazer o download, você terá que definir a pasta de download nas configurações do aplicativo. + Tema + Escolha um tema de exibição. + Fontes +Selecione as fontes para download + Você pode alterar essa seleção mais tarde a qualquer momento usando o botão de edição no menu à esquerda. + Tudo configurado! + Estamos definidos!\nAproveite o aplicativo. + diff --git a/app/src/main/res/values-pt-rBR/strings_tools.xml b/app/src/main/res/values-pt-rBR/strings_tools.xml new file mode 100644 index 0000000000..a2f1cf4447 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings_tools.xml @@ -0,0 +1,23 @@ + + +Ferramentas +Importar / Exportar +Exportar metadados +Exportar livros Hentoid para um arquivo JSON +Importar metadados +Restaurar livros Hentoid exportados de um arquivo JSON +Configurações +Exportar configurações +Exportar configurações para um arquivo JSON +Importar configurações +Restaurar configurações exportadas de um arquivo JSON +Detector de duplicata +Identificar e remover livros duplicados +Gerenciamento de cache +Limpar cache do navegador +O cache do WebView foi limpo com sucesso +Limpar cache do aplicativo +O cache do aplicativo foi limpo com sucesso +Registros do aplicativo +Ver os registros mais recentes do aplicativo + diff --git a/app/src/main/res/values-ru/array_preferences.xml b/app/src/main/res/values-ru/array_preferences.xml index c05e4ca329..9755a3392f 100644 --- a/app/src/main/res/values-ru/array_preferences.xml +++ b/app/src/main/res/values-ru/array_preferences.xml @@ -1,76 +1,96 @@ -Список (по умолчанию) + + Список (по умолчанию) Сетка -Не уведомлять + + Не уведомлять когда заполнена на 90% когда заполнена на 95% когда заполнена на 98% -500 мс (по умолчанию) - 1 с - 1.5 с - 2 с - 3 с -Нет -Вверху + + 500 мс (по умолчанию) + 1 с + 1.5 с + 2 с + 3 с + + Нет + + Вверху Внизу Всегда спрашивать -Без ограничения на размер - 20 МБ - 40 МБ - 100 МБ - 200 МБ -Без ограничения на кол-во страниц + + Без ограничения на размер + 20 МБ + 40 МБ + 100 МБ + 200 МБ + + Без ограничения на кол-во страниц 60 страниц 120 страниц 200 страниц 500 страниц -Автоматически (по умолчанию) + + Автоматически (по умолчанию) 2 (медленный интернет) 10 (быстрый интернет и производительное устройство) + Название - ID Художник - Название - ID Название - Художник - ID -Не сокращать + + Не сокращать 60 символов 80 символов 100 символов 120 символов -Загружать все страницы + + Загружать все страницы Передавать страницы потоком (стримить) -По алфавиту + + По алфавиту Сначала наиболее используемые -Нет -Не добавлять в очередь + + Нет + + Не добавлять в очередь Добавлять в очередь как неудачные загрузки -Слева направо + + Слева направо Справа налево Сверху вниз -Выкл. (по умолчанию) + + Выкл. (по умолчанию) Маленькие Обычные Большие -1 страница (по умолчанию) + + 1 страница (по умолчанию) 2 страницы 5 страниц Все страницы -500 мс - 1 с + + 500 мс + 1 с 2 с (по умолчанию) - 4 с - 8 с - 16 с -По размеру экрана (по умолчанию) + 4 с + 8 с + 16 с + + По размеру экрана (по умолчанию) Заполнить экран Растянуть на весь экран -Чёткая отрисовка (по умолчанию) + + Чёткая отрисовка (по умолчанию) Плавная отрисовка -Не ограничивать - - -Сразу - 10 с + + Не ограничивать + + Сразу + 10 с 30 с (по умолчанию) - 1 мин. + 1 мин. 2 мин. \ No newline at end of file diff --git a/app/src/main/res/values-ru/array_splash_quotes.xml b/app/src/main/res/values-ru/array_splash_quotes.xml index 98a5aeb355..ee8664d88d 100644 --- a/app/src/main/res/values-ru/array_splash_quotes.xml +++ b/app/src/main/res/values-ru/array_splash_quotes.xml @@ -2,7 +2,6 @@ искусство в приложении - опять _эти_ книги? опять эти книги\? приложение для тренировки руки пятиминутное упражнение для руки @@ -23,7 +22,6 @@ Папочка.... и вверх, и вниз… ВОТ ЭТО КОЛБАСА - Ты серьёзно дрочишь один? Ты серьёзно дрочишь один\? Пригласи друзей ;3 Попроси девушку- а, стоп… @@ -32,20 +30,17 @@ Эректус милая палочка, анон милый, пора вертеть колбаску - Колбаска *БРРРРРРР* Колбаска *БРРРРРРР* Прыгающие дыньки Cолёно, но вкусно Есть дырка — есть и способ входи в меня полностью - а? это что, член? а\? это что, член\? Фууу, убери это то ещё приложеньице используй смазку, брат https://youtu.be/dQw4w9WgXcQ книги мои книги - Стоп, не манга? Стоп, не манга\? Дьявольски соблазнительно Я думал, мы будем есть моллюсков на пару diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 04e2d9bb80..be6214aacf 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,9 +1,12 @@ -Добро пожаловать -Да -Нет - ОК + + Добро пожаловать + + Да + + Нет + ОК Вкл. Выкл. Выбрать всё @@ -27,7 +30,7 @@ файла файлов - Отменить + Группы Приостановить Остановить Возобновить @@ -37,14 +40,20 @@ Сервис уже запущен Открыть папку по умолчанию -Переход на API 29 + пусто + Никак + + + Переход на API 29 Библиотека Очередь Настройки О Hentoid Код-пароль Расширенный поиск -Hentoid v12\nОбновление управления файлами + + + Hentoid v12\nОбновление управления файлами Hentoid обновляет вашу коллекцию.\nЭто может занять несколько минут. Расположение папки Hentoid\n%s Папка Hentoid @@ -57,30 +66,31 @@ Импорт очереди… Пожалуйста, выберите существующую папку Hentoid. Её расположение показано на экране. Пожалуйста, подтвердите разрешение на запись -Поиск + + + Поиск Показать избранное - Показать завершённые Избранные Новая группа - Редактировать порядок - Редактировать название + Пересортировать + Переименовать + Подтвердить редактирование Отменить редактирование Новая пользовательская сортировка заменит предыдущую. Сортировать книги Удалить - Удалить элементы Переместить в группу… Открыть родительскую папку Загрузить снова Загрузить с нуля - Переключиться на потоковую передачу + Передавать потоком Загрузить Выбрать всё Поделиться Поделиться ссылкой на книгу и её названием через стороннее приложение - Ссылка на книгу В архив Заархивировать книгу в ZIP + Ссылка на книгу Название, художник, тэг или ID книги Поделиться через: Настройки @@ -90,10 +100,12 @@ Заканчивается память на устройстве! Исправить Просмотреть очередь -Загрузчик + + + Загрузчик В процессе Ошибки - + Отменить выбранный элемент\? Отменить выбранные элементы\? Отменить выбранные элементы\? @@ -101,10 +113,7 @@ Инвертировать порядок Загрузить всё снова Отменить все ошибки - Отменено В начало - Вверх - Вниз В конец Очередь приостановлена Подготовка загрузки @@ -136,6 +145,7 @@ Подготовка папки загрузок Подготовка цепочки загрузок Сохранение очереди + Очистка в процессе… Начата цепочка загрузок! Вы можете отменять загрузки свайпом влево Возникла проблема при повторной загрузке — отменено @@ -160,11 +170,13 @@ (%d ошибок) [попытка %1$d из %2$d] - %d Кб/с - %1$d%2$s МБ, %3$d Кб/c + %d Кб/с + %1$d%2$s МБ, %3$d Кб/c Загрузка не удалась Невозможно загрузить %1$s: не удалось создать папку %2$s. Пожалуйста, проверьте папку Hentoid и повторите загрузку с помощью кнопки \'!\'. -Сеттинг: %s + + + Сеттинг: %s Художник: %s (%d пропущена) @@ -191,11 +203,8 @@ Книга помечена заблокированным тэгом Неизвестно - - - - -Обновить библиотеку + + Обновить библиотеку Автоматически обновить библиотеку, сканируя файлы и папки снова. Опции очистки Библиотека Hentoid @@ -204,10 +213,11 @@ Удалить папки, в которых… Отсутствует JSON-файл (рекомендуется) Отсутствуют изображения - JSON-файл не читаем (не рекомендуется) или -Удалить всё, кроме избранных - %1$d из %2$d %3$s + + + Удалить всё, кроме избранных + %1$d из %2$d %3$s %1$d книга из %2$d %1$d книги из %2$d @@ -218,7 +228,9 @@ %1$d группы из %2$d %1$d групп из %2$d -Импорт метаданных + + + Импорт метаданных Добавить импортированные элементы Заменить импортированными элементами Выбор файла для импорта @@ -256,7 +268,9 @@ %d закладок успешно импортировано Выберите папку -Экспорт метаданных + + + Экспорт метаданных Экспорт библиотеки (%d книга) Экспорт библиотеки (%d книги) @@ -279,7 +293,8 @@ ЗАПУСТИТЬ ЭКСПОРТ Импорт настроек Успешно импортировано -Открыть настройки приложения + + Вы должны предоставить разрешения перед тем, как продолжить Вы должны вручную предоставить разрешения перед тем, как продолжить Импорт отменён @@ -295,10 +310,7 @@ Импорт библиотеки Импорт начинается… Импорт библиотеки - Импортирую, пожалуйста, подождите… - Обновляю, пожалуйста, подождите… Импорт завершён - Импорт завершён Успешно импортировано: %s Успешно импортировано: %s @@ -309,7 +321,9 @@ Не удалось импортировать: %s Не удалось импортировать: %s -Использование памяти + + + Использование памяти Всего: %s Свободно: %s Hentoid (основное хранилище): %s @@ -319,7 +333,9 @@ Источник Книги Размер -Последние логи + + + Последние логи Открыть лог Читать лог Поделиться логом @@ -328,24 +344,26 @@ Обновление/импорт основной библиотеки Очистка основной библиотеки Миграция библиотеки с Hentoid 1.11 -У вас нет прав на запись в этой папке, попробуйте другую. + + + У вас нет прав на запись в этой папке, попробуйте другую. Ошибка создания папки. Ошибка открытия файла: не найдено подходящего приложения Ошибка открытия ссылки: не обнаружен браузер - Ошибка отправки архива: не найдено подходящего приложения -Попытаться загрузить снова\? + + + Попытаться загрузить снова\? Всего изображений в загрузке: %1$d\nИзображений без ошибок: %2$d\nИзображений с ошибками: %3$d Первая ошибка: %s ЗАГРУЗИТЬ СНОВА Открыть лог ошибок Поделиться логами ошибок Генерирую файл логов… -Возобновить чтение\? + + + Возобновить чтение\? Открыть меню Закрыть меню - Как бы вы хотели обращаться с дочитанными книгами? - Показывать только дочитанные - Показывать только недочитанные Удалить %d выбранный элемент\? Удалить %d выбранных элемента\? @@ -397,7 +415,6 @@ Загружаю~ Нет результатов :( Тут такого нет, иди, скачай это. - Упаковываю файлы.\nПожалуйста, подождите… Поиск \'%s\' в… ID: \'%s\' @@ -410,11 +427,10 @@ %1$d результата из %2$d %1$d результатов из %2$d - Разрешения были сброшены.\nПриложение перезапускается. - \"Изменения будут применены после перезапуска Hentoid\" + Изменения будут применены после перезапуска Hentoid Загрузить эту книгу снова\? - Загрузить эту книгу снова\? + Загрузить эти книги снова\? Загрузить эти книги снова\? @@ -442,7 +458,6 @@ %d книги уже стримятся или находятся во внешней библиотеке\nЭти книги будут пропущены. %d книг уже стримятся или находятся во внешней библиотеке\nЭти книги будут пропущены. - Книга будет удалена… Ищите по чему угодно: названию, художнику, тэгам\nи даже ID книги! Вы можете проводить пальцем по номерам страниц @@ -485,7 +500,9 @@ Удалить группы и книги Нечего удалять — включите опцию \"Разрешить удалять внешний контент\" -Совместить + + + Совместить Совмещаю книги… Разделяю книгу… Разделить @@ -496,15 +513,23 @@ Создать главы Успешно совмещено Успешно разделено -Все книги + + + Все книги По художникам По дате загрузки - Своё + Свои группы [без художника] -По названиям + + + По возрастающей + По убывающей + Пересортировать + По названиям По художникам По страницам - По дате загрузки + По дате начала загрузки + По дате завершения загрузки По дате чтения По прочтениям По размеру @@ -513,16 +538,30 @@ Своё -неверно- По кол-ву книг -Переместить элемент + По дате начала загрузки + Фильтры + Отображение избранных книг + Показывать только завершённое + Показывать не только завершённое + + + Показать группы + Показать + Художники + Группы + + + Переместить элемент Находится во внешней библиотеке Книга передаётся потоком Просмотреть источник в браузере Добавить/убрать из избранного -Художники - Группы - Художники и группы + + У вас уже есть эта книга. Искать в других источниках\? -Обновления + + + Обновления Проверка обновлений Доступно обновление! Нажмите для загрузки @@ -531,36 +570,38 @@ Загружается обновление Обновление готово! Нажмите для установки - Произошла ошибка при сравнении зависимостей обновления.\nПожалуйста, попробуйте позднее. - Ошибка сети, пожалуйста, попробуйте позднее. - Не удалось найти файл обновления на сервере. - Проверка обновлений: Нет новых версий. -Учтите, что загрузки могут не работать на этом сайте. + + + Учтите, что загрузки могут не работать на этом сайте. Кнопка загрузки пока что не работает на этом сайте. Этот сайт сейчас работает нестабильно. Приложение не может ничего с этим сделать. Пожалуйста, попробуйте позднее. Этот сайт сейчас недоступен. Приложение не может ничего с этим сделать. Пожалуйста, попробуйте позднее. Команда разработчиков Hentoid работает над исправлением. Мы уведомим вас, когда новая версия приложения будет доступна. Исправление доступно в последней версии приложения. Пожалуйста, установите обновление. -Введите код-пароль - Код-пароль приложения + + + Введите код-пароль Блокировка включена Блокировка выключена Изменить код-пароль Код-пароль изменён - Код-пароль верный! Введите текущий код-пароль Введите новый код-пароль Подтвердите новый код-пароль Блокировка кодом-паролем предотвращает использование этого приложения без предварительной авторизации с помощью 4-значного PIN-кода Блокировать при\nповторном открытии -Hentoid — это приложение для скачивания додзинси и хентай-манги. Hentoid изначально был создан Сесаром Арасаки. + + + Hentoid — это приложение для скачивания додзинси и хентай-манги. Hentoid изначально был создан Сесаром Арасаки. ЗАЯВЛЕНИЕ О КОНФИДЕНЦИАЛЬНОСТИ ПЕРСОНАЛЬНЫХ ДАННЫХ\n\nHentoid — это бесплатное програмное обеспечение с открытым исходным кодом, разрабатываемое сообществом Hentoid (присоединяйтесь к нам в Discord по ссылке ниже) Hentoid собирает следующие персональные данные и делится ими с Google для обработки с помощью библиотеки Firebase:\n - Идентификатор сессии\n - IP-адрес Вышеупомянутые данные удаляются как из действующей, так и из запасной систем Firebase в течение 180 дней (подробности доступны на https://firebase.google.com/support/privacy/)\n\nНикакие другие персональные данные не собираются и не передаются третьим лицам. Показать лицензии - Версия Hentoid: %1$s (%2$d) - Версия Chrome: %1$d -Назад к последней странице галереи + Версия Hentoid: %1$s (%2$d) + Версия Chrome: %1$d + + + Назад к последней странице галереи Назад к галерее Назад Вперёд @@ -581,22 +622,25 @@ Домашняя страница Не удалось начать стриминг: %s Отсутствует разрешение на запись — невозможно использовать загрузчик -Hentoid не может работать с FAKKU без их аккаунта. Пожалуйста, создайте его или используйте существующий. - Эта книга не может быть просмотрена с вашим аккаунтом FAKKU. + Пожалуйста, создайте или используйте существующий аккаунт exHentai. Неверные данные, пожалуйста, повторите попытку входа Авторизация не завершена, пожалуйста, повторите попытку входа. Если проблема сохраняется, попробуйте посетить сайт из другой страны или создайте новый аккаунт. -Любое + + + Любое Исключить Ищем %s Искать %s -Художник (%1$d) + + Художник (%1$d) Тэг (%1$d) Персонаж (%1$d) Сеттинг (%1$d) Язык (%1$d) Источник (%1$d) -художника + + художника тэг персонажа сеттинг @@ -613,11 +657,11 @@ Найдено %s результатов Выберите фильтр -Выполняется обслуживание -Не удалось загрузить книгу: Не найдено ни одного изображения + + + Не удалось загрузить книгу: Не найдено ни одного изображения Загрузка: %1$d из %2$d Загрузка изображения: %d%% - Изображение не найдено %1$d x %2$d (масштаб %3$.0f%%) - %4$s Выберите направление чтения Слева направо @@ -632,12 +676,9 @@ Показывать все страницы Показ избранных страниц Установить как обложку - Меню страницы - Открыть настройки страницы Удалить книгу Скопировано в Загрузки Не удалось скопировать в Загрузки - Удалить эту книгу\? Удалить эту страницу\? Информация Предыдущая книга @@ -658,10 +699,6 @@ Настройки книги Настройки приложения Использовать настройки приложения (%s) - Подробности о странице - Подробности о книге - Избранное (страница) - Избранное (книга) Страница добавлена в избранное Страница удалена из избранного Книга добавлена в избранное @@ -690,14 +727,20 @@ Начало слайдшоу (задержка в %.1f с) Слайдшоу остановлено Удалить все главы? -Что нового\? + + + Что нового\? Что нового\?* Что нового\? Доступна новая версия (%1$s) Обновление завершено! Загрузка начата -Меню редактирования -Поиск дубликатов + + + Меню редактирования + + + Поиск дубликатов Название Обложка Художник @@ -723,7 +766,6 @@ Обложка: нет данных Художник: %.0f%% Художник: нет данных - Применить (осталось %d) Применить Начинаю определение дубликатов Определение дубликатов завершено @@ -742,18 +784,20 @@ Дубликатов не обнаружено Оставить Удалить -Внимание: дубликат! + + + Внимание: дубликат! Книга, которую вы собираетесь загрузить, может быть дубликатом книги, которая у вас уже есть. Книга, страницы которой вы собираетесь загрузить, может быть не той, которая у вас есть. - В библиотеке - В очереди Всегда загружать и больше не спрашивать Никогда больше не добавлять страницы из потенциальных дубликатов Схожесть: %.0f%% Загрузить книгу Загрузить дополнительные страницы Пожалуйста, подождите — идёт индексирование -Архивирование контента + + + Архивирование контента Удаление контента Удаление завершено @@ -763,21 +807,27 @@ Удаление не удалось Не удалось удалить по крайней мере одну книгу -Запуск + + + Запуск Запускается… Запуск завершён -Сегодня + + + Сегодня На неделе За этот месяц За последние 60 дней За последний год Давно Без группы -Светлая + + + Светлая Тёмно-красная Тёмная - + английский китайский японский @@ -796,17 +846,13 @@ турецкий чешский - - - - -Выберите ваш файловый менеджер - Стандартное разрешение - б. - КБ - МБ - ГБ - ТБ - ПБ - ЭБ + + Выберите ваш файловый менеджер + б. + КБ + МБ + ГБ + ТБ + ПБ + ЭБ \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings_download_service.xml b/app/src/main/res/values-ru/strings_download_service.xml index da3431a890..b384674b5d 100644 --- a/app/src/main/res/values-ru/strings_download_service.xml +++ b/app/src/main/res/values-ru/strings_download_service.xml @@ -7,11 +7,7 @@ Загрузка завершена Загрузка отменена - Загрузка пропущена - Загрузка приостановлена Загрузка завершена, но с ошибками - Необрабатываемая ошибка при чтении ссылок на изображения - Загружается %1$d элемент из %2$d добавлен в очередь %1$d элемента из %2$d добавлены в очередь diff --git a/app/src/main/res/values-ru/strings_settings.xml b/app/src/main/res/values-ru/strings_settings.xml index 707baae8fb..0007dbcc71 100644 --- a/app/src/main/res/values-ru/strings_settings.xml +++ b/app/src/main/res/values-ru/strings_settings.xml @@ -1,6 +1,8 @@ -Интерфейс и навигация + + + Интерфейс и навигация Библиотека Управление источниками Включайте, выключайте и реорганизовывайте источники, показываемые в левом меню @@ -11,6 +13,9 @@ Прокручивайте, сколько вашей душе угодно Кол-во элементов на странице Определяет, сколько элементов отображается на одной странице в списке загрузок\nСейчас: %s элементов + Кнопка перехода наверх + Отключена + Включена Расширенный поиск Способ сортировки отображаемых метаданных Отображать число доступных книг @@ -19,7 +24,9 @@ Тема приложения Цветовая схема Текущая тема: %s -Хранилище + + + Хранилище Библиотека Hentoid Выбор папки загрузок Выберите, где Hentoid будет хранить ваши загрузки @@ -54,7 +61,9 @@ Удалить %d книги\? Удалить %d книг\? -Браузер + + + Браузер Поведение Восстанавливать последнюю страницу Сайты будут открываться со своих главных страниц @@ -62,6 +71,7 @@ Загрузка Быстрая загрузка Не открывая книгу, нажмите на неё для загрузки + Заблокированные на nhentai книги отображаются… Отображаются с размытием Не отображаются Порог долгого нажатия для быстрой загрузки @@ -86,7 +96,9 @@ Уровень масштаба при загрузке страницы\nСейчас: %s%% Сторонний DNS DNS через HTTPS -Загрузчик + + + Загрузчик Автоматически начинать очередь загрузки Очередь загрузки начнётся как только загрузка завершится Очередь загрузки запустится только вручную @@ -109,12 +121,11 @@ Заблокированные тэги Книги с определёнными тэгами не будут загружаться\nПодсказка: тэги разделяются запятыми Плохие тэги нужно… - Предпочитать загрузку WebP - Загружать изображения в других форматах (PNG, JPG) - Загружать WebP, когда возможно Кол-во параллельных загрузок Число изображений, загружающихся одновременно\nСейчас: %s -Конфиденциальность + + + Конфиденциальность Предпросмотр в списке недавних приложений В списке недавних приложений не отображается, что открыто в приложении\nВключите, если хотите делать скриншоты приложения Содержимое экрана не скрывается в списке недавних приложений @@ -124,7 +135,9 @@ Аналитика Firebase Сбор аналитики выключен.\nВключите его, чтобы мы могли улучшать приложение, отслеживая сбои и ошибки в нём Сбор аналитики включён.\nСпасибо, что помогаете нам улучшить Hentoid! -Обновления + + + Обновления Проверить обновления вручную Сделать проверку прямо сейчас Проверяю обновления… @@ -133,7 +146,9 @@ Автоматические обновления Вы также можете включить автоматическую проверку обновлений по мобильной сети Это не отключает возможность ручной проверки обновлений -Просмотр изображений + + + Просмотр изображений Управление Режим обзора Режим перелистывания страниц diff --git a/app/src/main/res/values-ru/strings_slides.xml b/app/src/main/res/values-ru/strings_slides.xml index 3d2c8dc489..beea88a66b 100644 --- a/app/src/main/res/values-ru/strings_slides.xml +++ b/app/src/main/res/values-ru/strings_slides.xml @@ -1,19 +1,19 @@ -Добро пожаловать в Hentoid! + Добро пожаловать в Hentoid! Hentoid — это приложение для скачивания\nдодзинси и хентай-манги. -Разрешения + Разрешения Нам необходимо ваше разрешение, чтобы хранить файлы на устройстве. -Библиотека + Библиотека Hentoid необходима отдельная папка для хранения ваших файлов. Пропустить Пропустить\? Вы не сможете ничего загрузить, если пропустите этот шаг.\nДля загрузки вам понадобится выбрать папку загрузок в настройках приложения. -Тема + Тема Выберите тему. -Источники + Источники Выберите источники для загрузок. Вы сможете изменить этот выбор позднее в любой момент, нажав на \'Изменить\' в меню слева. -Вот и всё! + Вот и всё! Всё готово!\nНаслаждайтесь приложением. diff --git a/app/src/main/res/values-uk/array_plug_reactions.xml b/app/src/main/res/values-uk/array_plug_reactions.xml new file mode 100644 index 0000000000..583ba6fbde --- /dev/null +++ b/app/src/main/res/values-uk/array_plug_reactions.xml @@ -0,0 +1,16 @@ + + + ямете~~♡ + стій, це… не та дірка~ + ах~ + фууу ♡ ♡ + не сюди~~♡ + поганий, поганий хлопчик~~ + так… тісно…♡ + мені так приємно там~~ + глибше~~~♡ + ікуууу~~ + а-ах ♡ + ооох~ + ще ♡ + diff --git a/app/src/main/res/values-uk/array_preferences.xml b/app/src/main/res/values-uk/array_preferences.xml new file mode 100644 index 0000000000..9876c82256 --- /dev/null +++ b/app/src/main/res/values-uk/array_preferences.xml @@ -0,0 +1,96 @@ + + + + Список (за замовчуванням) + Сітка + + Не повідомляти + коли заповнена на 90% + коли заповнена на 95% + коли заповнена на 98% + + 500 мс (за замовчуванням) + 1 с + 1.5 с + 2 с + 3 с + + Ніякий + + Вгорі + Внизу + Завжди запитувати + + Без обмеження на розмір + 20 МБ + 40 МБ + 100 МБ + 200 МБ + + Без обмеження на кількість сторінок + 60 сторінок + 120 сторінок + 200 сторінок + 500 сторінок + + Автоматично (за замовчуванням) + 2 (повільний інтернет) + 10 (швидкий інтернет та продуктивний пристрій) + + Назва - ID + Художник - Назва - ID + Назва - Художник - ID + + Не скорочувати + 60 символів + 80 символів + 100 символів + 120 символів + + Завантажувати усі сторінки + Передавати сторінки потоком (стріміти) + + За алфавітом + Спочатку найбільш використовувані + + Ніякий + + Не додавати в чергу + Додавати у чергу як невдалі завантаження + + Зліва направо + Справа наліво + Зверху вниз + + Вимк. (за замовчуванням) + Маленькі + Звичайні + Великі + + 1 сторінка (за замовчуванням) + 2 сторінки + 5 сторінок + Всі сторінки + + 500 мс + 1 с + 2 с (за замовчуванням) + 4 с + 8 с + 16 с + + За розміром екрану (за замовчуванням) + Заповнити екран + Розтягнути на весь екран + + Чітке відмальовування (за замовчуванням) + Плавне відмальовування + + Не обмежувати + + Відразу + 10 с + 30 с (за замовчуванням) + 1 хв. + 2 хв. + diff --git a/app/src/main/res/values-uk/array_splash_quotes.xml b/app/src/main/res/values-uk/array_splash_quotes.xml new file mode 100644 index 0000000000..46739ad764 --- /dev/null +++ b/app/src/main/res/values-uk/array_splash_quotes.xml @@ -0,0 +1,49 @@ + + + + мистецтво у додатку + знову ці книги\? + додаток для тренування руки + п\'ятихвилинна вправа для руки + найкращий друг твоєї руки + додаток вмик, штани вимк + стеж за околицями, аноне + будь ласка, будь ніжний, сенпаю~~ + поміняй партнера + день рук + цілься + точне попадання + сасіська + 21 палець + стриб-стриб :3 + ПОЗАДУ ТЕБЕ + БАКА НЕ ШЛЬОПАЙ МЕНЕ + Завантажуємо Збоченця + Татко.... + і вгору, і вниз… + ОСЬ ЦЕ КОВБАСА + Ти серйозно дрочиш один\? + Запроси друзів ;3 + Попроси дівчину- а, стоп… + Мила дівчинка, стоп ЦЕ Ж ТРАП + Стріляй далеко + Еректус + мила паличка, аноне + милий, час крутити ковбаску + Ковбаска *БРРРРРРР* + Стрибаючі диньки + Солено, але смачно + Є дірка – є і спосіб + входь у мене повністю + а\? це що, член\? + Фууу, прибери це + тей ще додаточек + використовуй мастило, брате + https://youtu.be/dQw4w9WgXcQ + книги мої книги + Стоп, не манґа\? + Диявольськи спокусливо + Я думав, ми будемо їсти молюсків на пару + Сподіваюся, ви приготувалися до незабутнього обіду + + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..1f06bb9c4c --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,857 @@ + + + + Ласкаво просимо + + Так + + Ні + ОК + Вмик. + Вимк. + Вибрати все + Зняти виділення + Перейти до сторінки… + Підтвердити + Будь ласка, зачекайте… + + книга + книги + книг + + + сторінка + сторінки + сторінок + + + файл + файла + файлів + + Групи + Призупинити + Зупинити + Відновити + Скасувати + Скасувати все + Завершено + Сервіс вже запущений + Відкрити папку + за замовчуванням + порожньо + Ніяк + + + Перехід на API 29 + Бібліотека + Черга + Налаштування + Про Hentoid + Код-пароль + Розширений пошук + + + Hentoid v12\nОновлення керування файлами + Hentoid оновлює вашу колекцію.\nЦе може зайняти кілька хвилин. + Розташування папки Hentoid\n%s + Папка Hentoid + Зовнішня папка + Вибрати папку Hentoid + Вибрати зовнішню папку + Сканую папки сайтів + Сканую бібліотеку… + Сканування бібліотеки (%1$d/%2$d) + Імпорт черги… + Будь ласка, виберіть існуючу папку Hentoid. Її розташування показано на екрані. + Будь ласка, підтвердіть дозвіл на запис + + + Пошук + Показати обране + Обране + Нова група + Пересортувати + Перейменувати + Підтвердити редагування + Скасувати редагування + Нове користувальницьке сортування замінить попереднє. + Сортувати книги + Видалити + Перемістити до групи… + Відкрити батьківську папку + Завантажити знову + Завантажити з нуля + Передавати потоком + Завантажити + Вибрати все + Поділитися + Поділитися лінком на книгу та її назвою через сторонній додаток + В архів + Заархівувати книгу в ZIP + Лінк на книгу + Назва, художник, теґ чи ID книги + Поділитися через: + Налаштування + Налаштування додатка + Налаштування книги + Дозволи на роботу з файлами були втрачені + Закінчується пам\'ять на пристрої! + Виправити + Переглянути чергу + + + Завантажувач + В процесі + Помилки + + Скасувати вибраний елемент\? + Скасувати вибрані елементи\? + Скасувати вибрані елементи\? + + Інвертувати порядок + Завантажити все знову + Скасувати всі помилки + В початок + В кінець + Черга призупинена + Підготовка завантаження + Завантажується: %s + Статистика помилок + Показати докладну статистику про помилки + Книга ще не існує + Скасувати все у черзі\? + + Скасувати %d книгу з помилками\? + Скасувати %d книги з помилками\? + Скасувати %d книг із помилками\? + + + Завантажити %d елемент знову\? + Завантажити %d елементи знову\? + Завантажити %d елементів знову\? + + Завантаження неможливе — Немає мережі + Завантаження неможливе — Немає підключення до Wi-Fi + Завантаження неможливе — Пам\'ять майже заповнена (залишилося %s). Звільніть місце або змініть папку для завантаження + Завантаження неможливе — Немає папки завантажень. Виберіть її в налаштуваннях додатка + Завантаження неможливе — Папки завантажень не знайдено. Виберіть її в налаштуваннях додатка + Завантаження неможливе — Немає доступу до папки завантажень. Виберіть її знову в налаштуваннях додатка + Завантаження неможливе — Застаріли Cookie або авторизація. Оновіть їх через повідомлення + Ініціалізація + Підготовка зображень + Завантаження зображень + Підготовка папки завантажень + Підготовка ланцюжка завантажень + Збереження черги + Очищення у процесі… + Розпочато ланцюжок завантажень! + Ви можете скасовувати завантаження свайпом вліво + Виникла проблема при повторному завантаженні — скасовано + + %1$d елемент із %2$d буде завантажено знову + %1$d елементи з %2$d будуть завантажені знову + %1$d елементів із %2$d будуть завантажені знову + + Додати в початок + Додати в кінець + Користувальницькі дії + Необхідна дія користувача + Натисніть, щоб відновити завантаження + Будь ласка, не закривайте це вікно, поки ваше завантаження відновлюється + JSON черги успішно збережено + Не вдалося зберегти JSON очереди + приблизно %.1f МБ + %1$s/%2$d оброблено + + (%d помилка) + (%d помилки) + (%d помилок) + + [спроба %1$d/%2$d] + %d Кб/с + %1$d%2$s МБ, %3$d Кб/c + Завантаження не вдалося + Неможливо завантажити %1$s: не вдалося створити папку %2$s. Будь ласка, перевірте папку Hentoid та повторіть завантаження за допомогою кнопки \'!\'. + + + Сеттинг: %s + Художник: %s + + (%d пропущено) + (%d пропущено) + (%d пропущено) + + Сторінок: %1$s%2$s + + %1$d сторінка / %2$.1f МБ + %1$d сторінки / %2$.1f МБ + %1$d сторінок / %2$.1f МБ + + %.0f МБ + Вибрані книги завантажуються знову + Обробка + Мережа + Обмін даними + Капча + Обробка зображення + Досягнуто ліміт завантажень або пропускної спроможності + Немає акаунта або недостатньо дозволів + Не виявлено локального файлу після імпорту + Книга пропущена через обмеження на розмір завантажень по Wi-Fi + Книга позначена заблокованим теґом + Невідомо + + + Оновити бібліотеку + Автоматично оновлювати бібліотеку, скануючи файли і папки знову. + Опції очищення + Бібліотека Hentoid + Зовнішня бібліотека + Перейменувати всі папки відповідно до поточних параметрів назв і максимальної довжини + Видалити папки, в яких… + Немає JSON-файлу (рекомендується) + Немає зображень + або + + + Видалити все, окрім обраних + %1$d/%2$d %3$s + + %1$d книга з %2$d + %1$d книги з %2$d + %1$d книг із %2$d + + + %1$d група з %2$d + %1$d групи з %2$d + %1$d груп із %2$d + + + + Імпорт метаданих + Додати імпортовані елементи + Замінити імпортованими елементами + Вибір файлу для імпорту + Неможливо обробити файл %s + + Імпорт бібліотеки (%d книга) + Імпорт бібліотеки (%d книги) + Імпорт бібліотеки (%d книг) + + + Імпорт черги (%d книга) + Імпорт черги (%d книги) + Імпорт черги (%d книг) + + + Імпорт груп користувача (%d група) + Імпорт груп користувача (%d групи) + Імпорт груп користувача (%d груп) + + + Імпорт закладок (%d елемент) + Імпорт закладок (%d елементи) + Імпорт закладок (%d елементів) + + Якщо папок із зображеннями не виявиться, імпортовані книги будуть відображені як порожні завантаження. Вам доведеться завантажити їх в Черга > Помилки. + ЗАПУСТИТИ ІМПОРТ + + %d книга успішно імпортована + %d книги успішно імпортовані + %d книг успішно імпортовано + + + %d закладка успішно імпортована + %d закладки успішно імпортовані + %d закладок успішно імпортовано + + Виберіть папку + + + Експорт метаданих + + Експорт бібліотеки (%d книга) + Експорт бібліотеки (%d книги) + Експорт бібліотеки (%d книг) + + Експортувати тільки обране + Експортувати групи користувача + + Експорт черги (%d книга) + Експорт черги (%d книги) + Експорт черги (%d книг) + + + Експорт закладок (%d елемент) + Експорт закладок (%d елементи) + Експорт закладок (%d елементів) + + Експорт метаданих створить список ваших книг у форматі JSON.\nЦе не спосіб переміщення вашої колекції на компʼютер чи SD-карту.\nНатисніть тут, щоб дізнатися, як це зробити. + Книги експортуються без зображень.\nВам доведеться завантажити їх знову після відновлення, якщо папка зображень відсутня. + ЗАПУСТИТИ ЕКСПОРТ + Імпорт налаштувань + Успішно імпортовано + + + Ви маєте надати дозволи перед тим, як продовжити + Ви маєте вручну надати дозволи перед тим, як продовжити + Імпорт скасовано + Неможливо використувати цю папку — будь ласка, виберіть іншу + Ви не можете вибрати папку додатка як зовнішню — будь ласка, виберіть іншу + Ви не маєте вибирати папку з усіма завантаженнями цього пристрою — будь ласка, створіть чи виберіть іншу + Не вдалося створити папку Hentoid — будь ласка, виберіть інше розташування + Імпорт зупинено + Імпорт був несподівано зупинено + Завдання завершено — дивіться логи для подробиць + Помилка — контент не виявлено + Hentoid виявив існуючу бібліотеку.\nЧи хочете ви імпортувати її\? + Імпорт бібліотеки + Імпорт починається… + Імпорт бібліотеки + Імпорт завершено + + Успішно імпортовано: %s + Успішно імпортовано: %s + Успішно імпортовано: %s + + + Не вдалося імпортувати: %s + Не вдалося імпортувати: %s + Не вдалося імпортувати: %s + + + + Використання памʼяті + Всього: %s + Вільно: %s + Hentoid (основне сховище): %s + Hentoid (зовнішнє сховище): %s + База даних: %1$s (заповнено на %2$.1f%%) + Подробиці (основне сховище) + Джерело + Книги + Розмір + + + Останні логи + Відкрити лог + Читати лог + Поділитися логом + Немає помилок + Оновлення/імпорт зовнішнеї бібліотеки + Оновлення/імпорт основной бібліотеки + Очищення основної бібліотеки + Міграція бібліотеки з Hentoid 1.11 + + + У вас немає прав на запис у цій папці, спробуйте іншу. + Помилка створення папки. + Помилка відкриття файлу: не знайдено відповідного додатка + Помилка відкриття лінку: браузер не виявлено + + + Спробувати завантажити знову\? + Всього зображень у завантаженні: %1$d\nЗображень без помилок: %2$d\nЗображень з помилками: %3$d + Перша помилка: %s + ЗАВАНТАЖИТИ ЗНОВУ + Відкрити лог помилок + Поділитися логами помилок + Генерую файл логов… + + + Відновити читання\? + Відкрити меню + Закрити меню + + Видалити %d вибраний елемент\? + Видалити %d вибраних елементи\? + Видалити %d вибраних елементів\? + + + Заархівувати %d вибраний елемент\? + Заархівувати %d вибраних елементи\? + Заархівувати %d вибраних елементів\? + + Назва нової групи\? + Редагувати назву групи + Редагувати назву книги + Видаляю книги + Знищаю книги + Скасовую елементи черги + + %d книга + %d книги + %d книг + + + %d група + %d групи + %d груп + + успішно видалено + Архівую книги + Готово: %s + Архівування завершено + + %d книга успішно заархівована + %d книги успішно заархівовані + %d книг успішно заархівовані + + Архівація не вдалася + Не вдалося архівувати принаймні одну книгу + + %d книга заархівована в Завантаженнях + %d книги заархівовані в Завантаженнях + %d книг заархівовані в Завантаженнях + + Потокову передачу скасовано — книга не доступна в мережі + Завантаження скасовано — книга не доступна в мережі + Натисніть \'Назад\' двічі, щоб вийти. + Чому в мені порожньо\? + Помилок при завантаженні не виявлено + Ну давай, скачай щось. + Завантажую~ + Немає результатів :( + Тут такого немає, йди, скачай це. + Пошук \'%s\' в… + ID: \'%s\' + + %d елемент + %d елементи + %d елементів + + + %1$d результат із %2$d + %1$d результати з %2$d + %1$d результатів із %2$d + + Зміни будуть застосовані після перезапуску Hentoid + + Завантажити вибране знову\? + Завантажити вибране знову\? + Завантажити вибране знову\? + + + Завантажити вибране\? + Завантажити вибране\? + Завантажити вибране\? + + + Стріміти вибране\? + Стріміти вибране\? + Стріміти вибране\? + + + %d книга знаходиться у зовнішній бібліотеці й не може бути завантажена знову\nЦі книги будуть пропущені. + %d книги знаходяться у зовнішній бібліотеці й не можуть бути завантажени знову\nЦі книги будуть пропущені. + %d книги знаходяться у зовнішній бібліотеці й не можуть бути завантажени знову\nЦі книги будуть пропущені. + + + %d книга вже завантажена\nЦі книги будуть пропущені. + %d книги вже завантажені\nЦі книги будуть пропущені. + %d книг вже завантажено\nЦі книги будуть пропущені. + + + %d книга вже стріміться або знаходиться у зовнішній бібліотеці\nЦі книги будуть пропущені. + %d книги вже стрімляться або знаходяться у зовнішній бібліотеці\nЦі книги будуть пропущені. + %d книг вже стрімляться або знаходяться у зовнішній бібліотеці\nЦі книги будуть пропущені. + + Шукайте за чому завгодно: назвою, художнику, теґам\nі навіть ID книги! + Ви можете проводити пальцем за номерами сторінок + + Вибрано %d елемент + Вибрано %d елементи + Вибрано %d елементів + + Помилка — Батьківська папка недоступна + Помилка — Неправильний URL + Помилка — Сайт не підтримується + + %d книга з зовнішньої бібліотеки не буде видалена + %d книги з зовнішньої бібліотеки не будуть видалені + %d книг із зовнішньої бібліотеки не будуть видалені + + Встановити як обкладинку групи + Встановити обкладинку цієї книги як обкладинку групи\? + Перемістити до групи користувача + Існуюча група + Створити нову + Назва нової групи користувача + Відʼєднати від цієї групи + Група з такою назвою вже існує. + Заборонено перейменовувати цю групу! + Назва групи не може бути порожньою! + Не вибрана група! + + Видалити лише виділене + Видалити лише виділене + Видалити лише виділене + + + Видалити %d книгу + Видалити %d книги + Видалити %d книг + + + Видалити групи та книги + Видалити групи та книги + Видалити групи та книги + + Нема чого видаляти — увімкніть опцію \"Дозволити видаляти зовнішній контент\" + + + Поєднати + Поєднаю книги… + Розділяю книгу… + Розділити + Нова назва + Видалити ці книги після поєднання + Натисніть і утримуйте для вибору\nКожна вибрана глава перетвориться на нову книгу + Поки не створено глав :( + Створити глави + Успішно поєднано + Успішно розділено + + + Всі книги + За художниками + За датою завантаження + Свої групи + [без художника] + + + За зростаючою + За спадною + Пересортувати + За назвами + За художниками + За сторінками + За датою початку завантаження + За датою завершення завантаження + За датою читання + За прочитанням + За розміром + За прогресом + Випадково + Своє + -невірно- + За кількістю книг + За датою початку завантаження + Фільтри + Відображення обраних книг + Відображати тільки завершене + Відображати не тільки завершене + + + Показати групи + Показати + Художники + Групи + + + Перемістити елемент + Знаходиться у зовнішній бібліотеці + Книга передається потоком + Переглянути джерело у браузері + Додати/видалити з обраного + + + У вас вже є ця книга. Шукати в інших джерелах\? + + + Оновлення + Перевірка оновлень + Доступно оновлення! + Натисніть для завантаження + Не вдалося завантажити оновлення + Натисніть, щоб повторити спробу + Завантажується оновлення + Оновлення готово! + Натисніть для інсталяції + + + Зверніть увагу, що завантаження можуть не працювати на цьому сайті. + Кнопка завантаження наразі не працює на цьому сайті. + Цей сайт зараз працює нестабільно. Додаток не може нічого з цим зробити. Будь ласка, спробуйте пізніше. + Цей сайт зараз недоступний. Додаток не може нічого з цим зробити. Будь ласка, спробуйте пізніше. + Команда розробників Hentoid працює над виправленням. Ми повідомимо вас, коли нова версія додатка буде доступна. + Виправлення доступне в останній версії програми. Будь ласка, інсталюйте оновлення. + + + Введіть код-пароль + Блокування увімкнено + Блокування вимкнено + Змінити код-пароль + Код-пароль змінено + Введіть поточний код-пароль + Введіть новий код-пароль + Підтвердіть новий код-пароль + Блокування кодом-паролем запобігає використанню цього додатка без попередньої авторизації за допомогою 4-значного PIN-коду + Блокувати при\nповторному відкритті + + + Hentoid — це додаток для завантаження додзінсі і хентай-манґи. Hentoid спочатку був створений Сесаром Арасакі. + ЗАЯВА ПРО КОНФІДЕНЦІЙНІСТЬ ПЕРСОНАЛЬНИХ ДАНИХ\n\nHentoid — це безкоштовне програмне забезпечення з відкритим кодом, розроблене спільнотою Hentoid (приєднуйтесь до нас у Discord за лінком нижче) + Hentoid збирає наступні персональні дані і ділиться ними з Google для обробки за допомогою бібліотеки Firebase:\n - Ідентифікатор сесії\n - IP-адреса + Вищезазначені дані видаляються як з діючої, так і з запасної систем Firebase протягом 180 днів (подробиці доступні на https://firebase.google.com/support/privacy/)\n\nНіякі інші персональні дані не збираються і не передаються третім особам. + Показати ліцензії + Версія Hentoid: %1$s (%2$d) + Версія Chrome: %1$d + + + Назад до останньої сторінки галереї + Назад до галереї + Назад + Уперед + Закладки + Оновити/зупинити + Скопіювати адресу + Завантажити + Цей контент не може бути завантажений. Соррі~ + URL скопійовано в буфер обміну + Ви зайшли на сторінку книги. Саме час завантажити її за допомогою кнопки внизу! + Знайдено заблокований теґ (%s)\nЗавантаження скасовано + Знайдено заблокований теґ (%s)\nДодано в чергу як помилка + Додати в закладки + Видалити з закладок + Скопіювати адресу закладки + Зробити адресу закладки домашньою сторінкою + У цієї книги є наступні заблоковані теґи: %s + Домашня сторінка + Не вдалося почати стрімінг: %s + Відсутній дозвіл на запис — неможливо використовувати завантажувач + + Будь ласка, створіть або використовуйте існуючий акаунт exHentai. + Неправильні дані, будь ласка, повторіть спробу входу + Авторизація не завершена, будь ласка, повторіть спробу входу. Якщо проблема зберігається, спробуйте відвідати сайт з іншої країни або створіть новий акаунт. + + + Будь-яке + Виключити + Шукаємо %s + Шукати %s + + Художник (%1$d) + Теґ (%1$d) + Персонаж (%1$d) + Сеттинг (%1$d) + Мова (%1$d) + Джерело (%1$d) + + художника + теґ + персонажа + сеттинг + мову + джерело + завантажувача + коло + категорію + перекладача + видавця + + Знайдено %s результат + Знайдено %s результати + Знайдено %s результатів + + Виберіть фільтр + + + Неможливо завантажити книгу: Не знайдено жодного зображення + Завантаження: %1$d из %2$d + Завантаження зображення: %d%% + %1$d x %2$d (масштаб %3$.0f%%) - %4$s + Виберіть напрямок читання + Зліва направо + Справа наліво + Зверху вниз + Слайдшоу + Режим слайдшоу + Перемішати сторінки + Змінити порядок сторінок + Перемішування сторінок + Показувати лише обрані сторінки + Показувати всі сторінки + Показування обраних сторінок + Встановити як обкладинку + Видалити книгу + Скопійовано в Завантаження + Не вдалося скопіювати в Завантаження + Видалити цю сторінку\? + Інформація + Попередня книга + Наступна книга + Галерея книг + Додати/видалити сторінку з обраних + Скопіювати сторінку в Завантаження + Поділитися сторінкою зі стороннім додатком + Видалити сторінку + + Запитати мене знову наступного разу + Більше не питати для цієї книги + Більше не питати на час цієї сесії + + Встановити цю сторінку як обкладинку книги\? + Зображення не знайдено + Стрімлю зображення… + Налаштування книги + Налаштування додатка + Використовувати налаштування додатка (%s) + Сторінка додана до обраного + Сторінка видалена з обраного + Книга додана до обраного + Книга видалена з обраного + Виправити помилку завантаження + Перезавантажити сторінку + Передодати книгу + Редагувати глави + Галерея + Глава + Щоб створити або видалити глави, натисніть на першу сторінку + Очистити всі глави + Додати та видалити глави + %1$sСторінка %2$d%3$s%4$s + Гл. %d + Немає глав + Перейменовую файли… + Не вдалося видалити контент + Не вдалося видалити сторінку + Не вдалося видалити файл + Відсутній дозвіл на запис — неможливо почати перегляд + Неможливо завантажити список книг + Не вдалося відкрити главу на сторінці %d + Не вдалося видалити глави + Не вдалося перемістити главу + Початок слайдшоу (затримка в %.1f с) + Слайдшоу зупинено + Видалити всі глави? + + + Що нового\? + Що нового\?* + Що нового\? + Доступна нова версія (%1$s) + Оновлення завершено! + Завантаження почато + + + Меню редагування + + + Пошук дублікатів + Назва + Обкладинка + Художник + Лише тей же мовою + Ігнорувати глави та сіквели + Чутливість + + Слабка + Звичайна + Сильна + + Розпочати пошук + Індексую обкладинки + Визначаю дублікати + + %d дублікат + %d дублікати + %d дублікатів + + Назва: %.0f%% + Назва: немає даних + Обкладинка: %.0f%% + Обкладинка: немає даних + Художник: %.0f%% + Художник: немає даних + Застосувати + Починаю визначення дублікатів + Визначення дублікатів завершено + + %d дублікат успішно визначено + %d дублікати успішно визначено + %d дублікатів успішно визначено + + Нема чого показувати, почніть новий пошук + Йде визначення дублікатів + + %d дублікат + %d дублікати + %d дублікатів + + Дублікатів не визначено + Залишити + Видалити + + + Увага: дублікат! + Книга, яку ви збираєтеся завантажити, може бути дублікатом книги, яка вже є в вас. + Книга, сторінки якої ви маєте намір завантажити, може бути не тією, яка є в вас. + Завжди завантажувати і більше не питати + Ніколи більше не додавати сторінки з потенційних дублікатів + Схожість: %.0f%% + Завантажити книгу + Завантажити додаткові сторінки + Зачекайте, будь ласка — йде індексування + + + Архівування контенту + Видалення контенту + Видалення завершено + + Видалено: %d + Видалено: %d + Видалено: %d + + Видалення не вдалося + Не вдалося видалити принаймні одну книгу + + + Запуск + Запускається… + Запуск завершено + + + Сьогодні + На тижні + За цей місяць + За останні 60 днів + За останній рік + Давно + Без групи + + + Світла + Темно-червона + Темна + + + англійська + китайська + японська + корейська + іспанська + російська + португальска + тайська + французька + вʼєтнамська + італійська + німецька + польська + індонезійська + угорська + турецька + чеська + + + Виберіть ваш файловий менеджер + б. + КБ + МБ + ГБ + ТБ + ПБ + ЕБ + diff --git a/app/src/main/res/values-uk/strings_download_service.xml b/app/src/main/res/values-uk/strings_download_service.xml new file mode 100644 index 0000000000..618f4a3a8b --- /dev/null +++ b/app/src/main/res/values-uk/strings_download_service.xml @@ -0,0 +1,18 @@ + + + + %d завантаження завершено + %d завантаження завершено + %d завантаження завершено + + Завантаження завершено + Завантаження скасовано + Завантаження завершено, але з помилками + + %1$d елемент із %2$d додано до черги + %1$d елементи з %2$d додано до черги + %1$d елементів із %2$d додано до черги + + Вже завантажено + Вже у черзі + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings_settings.xml b/app/src/main/res/values-uk/strings_settings.xml new file mode 100644 index 0000000000..43506d3b2c --- /dev/null +++ b/app/src/main/res/values-uk/strings_settings.xml @@ -0,0 +1,189 @@ + + + + + Інтерфейс та навігація + Бібліотека + Керування джерелами + Вмикайте, вимикайте та реорганізовуйте джерела, що відображаються в лівому меню + Спосіб відображення + Вид відображення книг та груп\nПоточний: %s + Нескінченна прокрутка + Включає нескінченне прокручування у списку завантажень\nВимикає поділ на сторінки + Прокручуйте, скільки вашій душі завгодно + Кількість елементів на сторінці + Визначає, скільки елементів відображається на одній сторінці у списку завантажень\nЗараз: %s елементів + Кнопка переходу наверх + Вимкнена + Увімкнена + Розширений пошук + Спосіб сортування метаданих + Відображати кількість доступних книг + Кількість доступних книг відображається поруч із кожною характеристикою + Кількість доступних книг не відображається + Тема додатка + Схема кольорів + Поточна тема: %s + + + Сховище + Бібліотека Hentoid + Вибір папки завантажень + Виберіть, де Hentoid буде зберігати ваші завантаження + Додавати порожні книги в чергу як невдалі завантаження + Порожні книги будуть додані в чергу як помилки завантаження + Порожні книги будуть додані до бібліотеки + Назви папок + Спосіб найменування папок:\n%s + Імпортування вже запущено + Вимкнути зовнішню бібліотеку\? + Зовнішня бібліотека вимкнена + Скорочувати назви папок + Урізання назв папок\nЗараз: %s + Використання пам\'яті + Вільне місце: %.2f%% (натисніть для подробиць) + Попереджати при майже повному заповненні пам\'яті + Зовнішня бібліотека + Зовнішня бібліотека + Зовнішні бібліотеки не встановлені + Дозволяти видалення зовнішнього контенту + Hentoid може видаляти книги та сторінки у зовнішній бібліотеці + Hentoid не може видаляти книги та сторінки у зовнішній бібліотеці + Вимкнути зовнішню бібліотеку + Не використовувати зовнішню бібліотеку в Hentoid + Додатково + Оновити бібліотеку + Вручну оновити вміст бібліотеки + Видалити всі книги, крім обраних + Очистити бібліотеку і залишити лише обрані книги\nУвага: не відноситься до зовнішньої бібліотеки + + Видалити %d книгу\? + Видалити %d книги\? + Видалити %d книг\? + + + + Браузер + Поведінка + Відновлювати останню сторінку + Сайти відкриватимуться зі своїх головних сторінок + Сайти відкриватимуться з останньої сторінки, яка була відкрита на них + Завантаження + Швидке завантаження + Не відкриваючи книгу, натисніть на неї, щоб завантажити + Заблоковані на книги nhentai відображаються… + Відображаються з розмиттям + Не відображаються + Поріг довгого натискання для швидкого завантаження + Не відпускайте палець протягом цього часу після натискання, щоб почати швидке завантаження\nЗараз: %s + Перевірка на дублікати + Ні: завжди завантажувати + Так: запитувати перед завантаженням + Намагатися знайти додаткові сторінки на потенційних дублікатах\? + Шукати додаткові сторінки лише на оригіналі + Шукати додаткові сторінки на потенційних дублікатах + Помічати завантажені книги + Вже завантажені книги не виділяються + Вже завантажені книги виділяються\nУвага: працює не на всіх сайтах + Зовнішній вигляд + Поліпшення якості життя + Використовується стандартна поведінка + Реклама видаляється, а обробка відбувається швидше + Вимкнути огляд сторінки + Внизу ви можете виставити бажаний рівень масштабу за замовчуванням + Скористайтеся налаштуванням внизу для визначення бажаного рівня масштабу за замовчуванням\nПримітка: Працює лише на hitomi та e-hentai + Масштаб за замовчуванням + Рівень масштабу під час завантаження сторінки\nЗараз: %s%% + Сторонній DNS + DNS через HTTPS + + + Завантажувач + Автоматично починати чергу завантаження + Черга завантаження розпочнеться як тільки завантаження завершиться + Черга завантаження запуститься лише вручну + Завантажувати тільки через Wi-Fi + Черга завантаження розпочнеться лише якщо є підключення до Wi-Fi + Черга завантаження розпочнеться незалежно від типу мережі + Розташування нових завантажень + Пропускати великі завантаження на мобільній мережі + Великі завантаження будуть пропущені, якщо відсутнє підключення до Wi-Fi + Усі завантаження будуть оброблені + Обмеження на розмір завантажень + Обмеження на кількість сторінок у завантаженнях + Автоматично намагатись повторно завантажувати + При невдалих спробах завантаження воно буде здійснено знову + Невдалі завантаження відбуватимуться повторно + Невдалі завантаження будуть позначені як помилки + Кількість повторних спроб + Обмеження використання пам\'яті + Блокування теґів + Заблоковані теґи + Книги з певними теґами не завантажуватимуться\nПідказка: теґи розділяються комами + Погані теґи має… + Кількість паралельних завантажень + Кількість зображень, що завантажуються одночасно\nЗараз: %s + + + Конфіденційність + Перегляд у списку нещодавніх додатків + У списку останніх додатків не відображається, що відкрито в додатку\nУвімкніть, якщо хочете робити скріншоти додатка + Вміст екрана не ховається у списку нещодавніх додатків + Дозволити скріншоти + Перегляд у списку недавніх додатків та скріншоти в додатку вимкнено + Скріншоти дозволені + Аналітика Firebase + Збір аналітики вимкнено.\nУвімкніть його, щоб ми могли покращувати додаток, відстежуючи збої та помилки в ньому + Збір аналітики увімкнено.\nДякуємо, що допомагаєте нам покращити Hentoid! + + + Оновлення + Перевірити оновлення вручну + Зробити перевірку прямо зараз + Перевіряю оновлення… + Оновлень не знайдено — Ви вже використовуєте останню версію + Не вдалося перевірити оновлення. Перевірте з\'єднання або спробуйте пізніше + Автоматичні оновлення + Ви також можете увімкнути автоматичну перевірку оновлень по мобільній мережі + Це не вимикає можливість ручної перевірки оновлень + + + Перегляд зображень + Управління + Режим огляду + Режим перегортання сторінок + Свайп + Натискання на край + Подвійне збільшення зони натискання + Кнопки гучності + Стрілки вліво/вправо на клавіатурі + Перегортати кілька сторінок одним свайпом + Інвертувати кнопки гучності + Збільшення + Тимчасово збільшувати при утриманні пальця + Обмежити зум за подвійним або довгим натисканням + Зовнішній вигляд + Не вимикати екран + Завжди показувати номер сторінки + Увімкнути анімацію зміни сторінок + Увімкнути анімацію зуму + Смуги, що розділяють у вертикальному режимі + Режим відображення для горизонтального перегляду + Режим відмальовування + Плавне відмальовування не доступно на Android 5 + Автоматично повертати зображення + Зображення не повертаються + Зображення повертаються так, щоб зайняти якнайбільше місця на екрані + Число стовпців у галереї + Навігація та поведінка + Відновлювати читання при повторному відкритті книг + Книги завжди відкриватимуться з першої сторінки + Раніше відкриті книги відкриватимуться з останньої переглянутої сторінки, крім випадків, коли книга була дочитана + Завжди відкривати у режимі галереї + Безперервне читання + Остання сторінка книги справді остання + З останньої сторінки книги можна піти на наступну книгу + Вважати книгу прочитаною після N сторінок + Слайдшоу / Час показу кожної сторінки + Деякі сайти погано працюють з DNS через HTTPS. Скористайтеся стороннім додатком для DNS, якщо ви маєте проблеми при навігації (наприклад: порожні сторінки або помилки під час авторизації) + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings_slides.xml b/app/src/main/res/values-uk/strings_slides.xml new file mode 100644 index 0000000000..e8ed05e0e8 --- /dev/null +++ b/app/src/main/res/values-uk/strings_slides.xml @@ -0,0 +1,22 @@ + + + Ласкаво просимо до Hentoid! + Hentoid — це додаток для завантаження\nдодзінсі та хентай-манґи. + Дозволи + Нам потрібен ваш дозвіл, щоб зберігати файли на пристрої. + Бібліотека + Hentoid потрібна окрема папка для зберігання файлів. + + Пропустити + + Пропустити\? + Якщо ви не пропустите цей крок, ви не зможете нічого завантажити.\nДля завантаження вам потрібно вибрати папку завантажень у налаштуваннях додатка. + Тема + Виберіть тему. + Джерела + + Виберіть джерела для завантажень. + Ви можете змінити цей вибір пізніше в будь-який момент, натиснувши на \'Змінити\' у меню зліва. + От і все! + Все готово!\nНасолоджуйтесь додатком. + diff --git a/app/src/main/res/values-uk/strings_tools.xml b/app/src/main/res/values-uk/strings_tools.xml new file mode 100644 index 0000000000..2608c244af --- /dev/null +++ b/app/src/main/res/values-uk/strings_tools.xml @@ -0,0 +1,47 @@ + + + + Інструменти + + Імпорт та експорт + + + Експортувати метадані + + Експортувати книги з Hentoid в JSON + + Імпортувати метадані + + Відновити книги в Hentoid з JSON + + Налаштування + + Експортувати налаштування + + Експортувати налаштування в JSON + + Імпортувати налаштування + + Відновити налаштування з JSON + + + Пошук дублікатів + + Пошук та видалення ідентичних копій книг + + + Керування кешем + + Очистити кеш браузера + + Кеш WebView успішно очищено + + Очистити кеш додатка + + Кеш додатка успішно очищено + + + Логи додатка + + Переглянути останні логи додатка + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 14f6f420c0..0ccbd3df67 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -19,4 +19,5 @@ + diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 28a23fb271..259a4908e6 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -355,6 +355,7 @@ 0 true 20 + true 1 %s true diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 59cd78a770..a8e66ceb11 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,6 +26,7 @@ file files + Groups Pause Stop Resume @@ -36,6 +37,7 @@ Open folder default empty + None API 29 migration @@ -64,11 +66,11 @@ Search Toggle favourite filter - Toggle completed filter Toggle favourite New group - Edit order - Edit name + Reorder + Rename + Confirm editing Cancel editing New custom ordering will overwrite the previous one. Order books @@ -91,7 +93,7 @@ App settings Book settings File permissions have been lost - Your device is low on memory! + Your device is low on storage! Fix View queue @@ -294,8 +296,8 @@ %s failed - - Memory usage + + Storage usage Total: %s Free: %s Hentoid (primary): %s @@ -336,9 +338,6 @@ Resume reading from where you left\? Open drawer Close drawer - How would you like to filter completed books\? - Show completed only - Show not completed only Delete the selected item\? Delete %d selected items\? @@ -470,18 +469,22 @@ Content merged Content split - + All books By Artist - By Download date - Custom + By Download date (processed) + Custom groups [no artist] - + + Ascending + Descending + Reshuffle Title Artist Pages - Download date + Download date (processed) + Download date (completed) Read date Reads Size @@ -490,6 +493,17 @@ Custom -invalid- Books + Upload date + Filters + Toggle display of favourite books + Show completed books only + Show not completed books only + + + Display groups + Display + Artists + Groups Drag item @@ -499,9 +513,6 @@ Mark/unmark as favourite - Show artists - Show groups - Show artists & groups You already have this book. Search other sources\? @@ -620,7 +631,7 @@ Toggle page shuffling Only show favourite pages Show all pages - Toggle display of favourite page + Toggle display of favourite pages Set as cover Delete book Copied to Downloads folder diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 71e67b735a..62c036e7fe 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -12,6 +12,9 @@ Scroll away to your heart\'s content Quantity Per Page Number of items per page in download list\nCurrently: %s items + Go to Top button + Button disabled + Button enabled Advanced search Sorting order of displayed metadata Count available books @@ -36,9 +39,9 @@ External library detached Truncate folder name Book folder name truncation\nCurrently: %s - Memory usage + Storage usage Current free space: %.2f%% (tap for details) - Alert on low memory + Alert on low storage External library External library No external library is set diff --git a/app/src/main/res/values/strings_slides.xml b/app/src/main/res/values/strings_slides.xml index a78711c04d..aa0970d9f0 100644 --- a/app/src/main/res/values/strings_slides.xml +++ b/app/src/main/res/values/strings_slides.xml @@ -1,12 +1,9 @@ - Welcome to Hentoid! Hentoid is a Doujinshi\n& H-Manga archiving app. - Permissions We need to ask for your permission to store files on your device. - Library Hentoid requires a dedicated folder for storing your media. @@ -14,15 +11,12 @@ Skip\? You won\'t be able to download if you skip this step.\nIn order to download, you\'ll have to set the download folder through the app settings. - Theme Choose a display theme. - Sources Select sources to download from. You can change this selection later at any time using the edit button on the left menu. - All set up! We are set!\nEnjoy the app. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 904c4d9605..274d956684 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,4 +1,4 @@ - + + + + +