├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable
│ │ │ ├── broken.png
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_settings.png
│ │ │ ├── ic_hops_grey600_48dp.png
│ │ │ ├── ic_info_grey600_24dp.png
│ │ │ ├── ic_book_open_white_24dp.png
│ │ │ ├── ic_aspect_ratio_white_24dp.png
│ │ │ ├── ic_filter_list_white_24dp.png
│ │ │ ├── ic_my_library_books_grey600_24dp.png
│ │ │ ├── nav_item.xml
│ │ │ ├── reader_nav_bg.xml
│ │ │ ├── reader_nav_thumb.xml
│ │ │ ├── reader_nav_progress.xml
│ │ │ └── reader_nav_progress_inverse.xml
│ │ ├── drawable-hdpi
│ │ │ ├── broken.png
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_settings.png
│ │ │ ├── ic_hops_grey600_48dp.png
│ │ │ ├── ic_info_grey600_24dp.png
│ │ │ ├── ic_book_open_white_24dp.png
│ │ │ ├── ic_aspect_ratio_white_24dp.png
│ │ │ ├── ic_filter_list_white_24dp.png
│ │ │ └── ic_my_library_books_grey600_24dp.png
│ │ ├── drawable-mdpi
│ │ │ ├── broken.png
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_settings.png
│ │ │ ├── ic_hops_grey600_48dp.png
│ │ │ ├── ic_info_grey600_24dp.png
│ │ │ ├── ic_book_open_white_24dp.png
│ │ │ ├── ic_aspect_ratio_white_24dp.png
│ │ │ ├── ic_filter_list_white_24dp.png
│ │ │ └── ic_my_library_books_grey600_24dp.png
│ │ ├── drawable-xhdpi
│ │ │ ├── broken.png
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_settings.png
│ │ │ ├── ic_hops_grey600_48dp.png
│ │ │ ├── ic_info_grey600_24dp.png
│ │ │ ├── ic_book_open_white_24dp.png
│ │ │ ├── ic_filter_list_white_24dp.png
│ │ │ ├── ic_aspect_ratio_white_24dp.png
│ │ │ └── ic_my_library_books_grey600_24dp.png
│ │ ├── drawable-xxhdpi
│ │ │ ├── broken.png
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_settings.png
│ │ │ ├── ic_hops_grey600_48dp.png
│ │ │ ├── ic_info_grey600_24dp.png
│ │ │ ├── ic_book_open_white_24dp.png
│ │ │ ├── ic_aspect_ratio_white_24dp.png
│ │ │ ├── ic_filter_list_white_24dp.png
│ │ │ └── ic_my_library_books_grey600_24dp.png
│ │ ├── drawable-xxxhdpi
│ │ │ ├── broken.png
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_settings.png
│ │ │ ├── ic_hops_grey600_48dp.png
│ │ │ ├── ic_info_grey600_24dp.png
│ │ │ ├── ic_book_open_white_24dp.png
│ │ │ ├── ic_aspect_ratio_white_24dp.png
│ │ │ ├── ic_filter_list_white_24dp.png
│ │ │ └── ic_my_library_books_grey600_24dp.png
│ │ ├── layout
│ │ │ ├── layout_series.xml
│ │ │ ├── fragment_reader.xml
│ │ │ ├── fragment_series_header.xml
│ │ │ ├── breadcrumb.xml
│ │ │ ├── layout_header.xml
│ │ │ ├── fragment_series.xml
│ │ │ ├── fragment_reader_page.xml
│ │ │ ├── fragment_library.xml
│ │ │ ├── layout_main.xml
│ │ │ ├── card_cover.xml
│ │ │ ├── layout_reader.xml
│ │ │ ├── fragment_about.xml
│ │ │ └── card_deps.xml
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── dimen_aboutlibraries.xml
│ │ │ ├── styles.xml
│ │ │ ├── integers.xml
│ │ │ ├── styles_reader.xml
│ │ │ └── strings.xml
│ │ ├── menu
│ │ │ ├── drawer.xml
│ │ │ ├── browser.xml
│ │ │ └── reader.xml
│ │ ├── xml
│ │ │ └── app_preferences.xml
│ │ ├── values-de
│ │ │ └── strings.xml
│ │ ├── values-nl
│ │ │ └── strings.xml
│ │ ├── values-es
│ │ │ └── strings.xml
│ │ ├── values-ro
│ │ │ └── strings.xml
│ │ └── values-fr
│ │ │ └── strings.xml
│ │ ├── java
│ │ └── be
│ │ │ └── nosuid
│ │ │ └── bubble
│ │ │ ├── komga
│ │ │ ├── User.java
│ │ │ ├── ReadProgress.java
│ │ │ ├── Media.java
│ │ │ ├── Series.java
│ │ │ ├── Books.java
│ │ │ └── KomgaApi.java
│ │ │ ├── MainApplication.java
│ │ │ ├── model
│ │ │ ├── Series.java
│ │ │ └── Comic.java
│ │ │ ├── view
│ │ │ ├── CoverImageView.java
│ │ │ ├── CoverViewHolder.java
│ │ │ ├── ComicSeekBar.java
│ │ │ ├── SwipeOutViewPager.java
│ │ │ └── PageImageView.java
│ │ │ ├── activity
│ │ │ ├── ReaderActivity.java
│ │ │ └── MainActivity.java
│ │ │ ├── managers
│ │ │ ├── Utils.java
│ │ │ ├── SharedResourcesManager.java
│ │ │ ├── SettingsManager.java
│ │ │ └── LibraryManager.java
│ │ │ └── fragment
│ │ │ ├── SettingsFragment.java
│ │ │ ├── AboutFragment.java
│ │ │ ├── LibraryFragment.java
│ │ │ ├── SeriesFragment.java
│ │ │ └── ReaderFragment.java
│ │ └── AndroidManifest.xml
└── build.gradle
├── settings.gradle
├── .gitignore
├── gradle.properties
├── LICENSE
├── README.md
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "bubble-komga"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | local.properties
4 | .idea/
5 | build/
6 | gradle/
--------------------------------------------------------------------------------
/app/src/main/res/drawable/broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable/broken.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-hdpi/broken.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-mdpi/broken.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xhdpi/broken.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxhdpi/broken.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxxhdpi/broken.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-hdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-mdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xhdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxhdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxxhdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_hops_grey600_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable/ic_hops_grey600_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_info_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable/ic_info_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_book_open_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable/ic_book_open_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_hops_grey600_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-hdpi/ic_hops_grey600_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_info_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-hdpi/ic_info_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_hops_grey600_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-mdpi/ic_hops_grey600_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_info_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-mdpi/ic_info_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_hops_grey600_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xhdpi/ic_hops_grey600_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_info_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xhdpi/ic_info_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_aspect_ratio_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable/ic_aspect_ratio_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_filter_list_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable/ic_filter_list_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_book_open_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-hdpi/ic_book_open_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_book_open_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-mdpi/ic_book_open_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_book_open_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xhdpi/ic_book_open_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_hops_grey600_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxhdpi/ic_hops_grey600_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_info_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxhdpi/ic_info_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_hops_grey600_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxxhdpi/ic_hops_grey600_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_info_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxxhdpi/ic_info_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_aspect_ratio_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-hdpi/ic_aspect_ratio_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_filter_list_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-hdpi/ic_filter_list_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_aspect_ratio_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-mdpi/ic_aspect_ratio_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_filter_list_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-mdpi/ic_filter_list_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_book_open_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxhdpi/ic_book_open_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_book_open_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxxhdpi/ic_book_open_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_aspect_ratio_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xhdpi/ic_aspect_ratio_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_aspect_ratio_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxhdpi/ic_aspect_ratio_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_aspect_ratio_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxxhdpi/ic_aspect_ratio_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_my_library_books_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable/ic_my_library_books_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_my_library_books_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-hdpi/ic_my_library_books_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_my_library_books_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-mdpi/ic_my_library_books_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_my_library_books_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xhdpi/ic_my_library_books_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_my_library_books_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxhdpi/ic_my_library_books_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_my_library_books_grey600_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PichetGoulu/bubble-komga/HEAD/app/src/main/res/drawable-xxxhdpi/ic_my_library_books_grey600_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/nav_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/reader_nav_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/reader_nav_thumb.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536m
2 | # AndroidX package structure to make it clearer which packages are bundled with the
3 | # Android operating system, and which are packaged with your app's APK
4 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
5 | #android.useAndroidX=true
6 | # Automatically convert third-party libraries to use AndroidX
7 | #android.enableJetifier=true
8 | android.useAndroidX=true
9 | android.enableJetifier=true
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_series.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #DB5162
4 | #B35159
5 | #ff466e87
6 |
7 | #222222
8 | #ce222222
9 | #333333
10 |
11 | #ffffff
12 | #dddddd
13 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/komga/User.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.komga;
2 |
3 | import com.google.gson.annotations.Expose;
4 | import com.google.gson.annotations.SerializedName;
5 |
6 | public class User {
7 | @SerializedName("id")
8 | @Expose
9 | private String id = null;
10 | @SerializedName("email")
11 | @Expose
12 | private String email;
13 |
14 | public String getId() {
15 | return id;
16 | }
17 |
18 | public String getEmail() {
19 | return email;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_reader.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/reader_nav_progress.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 | -
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimen_aboutlibraries.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 12dp
4 |
5 | 72dp
6 |
7 | 20sp
8 | 14sp
9 |
10 |
11 |
12 | 16dp
13 | 16dp
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/reader_nav_progress_inverse.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 | -
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_series_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/MainApplication.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble;
2 |
3 | import android.app.Application;
4 |
5 | import be.nosuid.bubble.managers.SharedResourcesManager;
6 |
7 |
8 | public class MainApplication extends Application {
9 | private SharedResourcesManager mSharedResourcesManager;
10 |
11 | @Override
12 | public void onCreate() {
13 | super.onCreate();
14 | mSharedResourcesManager = new SharedResourcesManager(this);
15 | }
16 |
17 | public SharedResourcesManager getSharedResourcesManager() {
18 | return mSharedResourcesManager;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/breadcrumb.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/komga/ReadProgress.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.komga;
2 |
3 |
4 | import com.google.gson.annotations.Expose;
5 | import com.google.gson.annotations.SerializedName;
6 |
7 | public class ReadProgress {
8 |
9 | @SerializedName("page")
10 | @Expose
11 | private Integer page;
12 | @SerializedName("completed")
13 | @Expose
14 | private Boolean completed;
15 |
16 | public Integer getPage() {
17 | return page;
18 | }
19 |
20 | public void setPage(Integer page) {
21 | this.page = page;
22 | }
23 |
24 | public Boolean getCompleted() {
25 | return completed;
26 | }
27 |
28 | public void setCompleted(Boolean completed) {
29 | this.completed = completed;
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/app/src/main/res/menu/drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_series.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/model/Series.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.model;
2 |
3 | import java.io.Serializable;
4 |
5 | public class Series implements Serializable {
6 | private String mId;
7 | private String mName;
8 | private int mBookLastRead;
9 | private int mBooksCount;
10 |
11 | public Series(String id, String name, int bookLastRead, int booksCount) {
12 | mId = id;
13 | mName = name;
14 | mBookLastRead = bookLastRead;
15 | mBooksCount = booksCount;
16 | }
17 |
18 | public String getId() {
19 | return mId;
20 | }
21 |
22 | public String getName() {
23 | return mName;
24 | }
25 |
26 | public int getBooksLastRead() {
27 | return mBookLastRead;
28 | }
29 |
30 | public int getBooksCount() {
31 | return mBooksCount;
32 | }
33 |
34 | @Override
35 | public boolean equals(Object o) {
36 | return (o instanceof Series) && getId().equals(((Series) o).getId());
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/komga/Media.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.komga;
2 |
3 | import com.google.gson.annotations.Expose;
4 | import com.google.gson.annotations.SerializedName;
5 |
6 | public class Media {
7 |
8 | @SerializedName("status")
9 | @Expose
10 | private String status;
11 | @SerializedName("mediaType")
12 | @Expose
13 | private String mediaType;
14 | @SerializedName("pagesCount")
15 | @Expose
16 | private Integer pagesCount;
17 |
18 | public String getStatus() {
19 | return status;
20 | }
21 |
22 | public void setStatus(String status) {
23 | this.status = status;
24 | }
25 |
26 | public String getMediaType() {
27 | return mediaType;
28 | }
29 |
30 | public void setMediaType(String mediaType) {
31 | this.mediaType = mediaType;
32 | }
33 |
34 | public Integer getPagesCount() {
35 | return pagesCount;
36 | }
37 |
38 | public void setPagesCount(Integer pagesCount) {
39 | this.pagesCount = pagesCount;
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/integers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 200
4 | 7
5 | 200
6 | 7
7 |
8 | 5dp
9 | 10dp
10 |
11 | 20dp
12 | 10dp
13 |
14 | 8dp
15 | 4dp
16 |
17 | 48dp
18 | 24sp
19 | 14sp
20 |
21 | 18sp
22 | 14sp
23 | 8dp
24 |
25 | 72dp
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Nazar Kanaev (nkanaev@live.com)
2 | Copyright (c) 2020 PichetGoulu (pichet@nosuid.be)
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles_reader.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
15 |
16 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/browser.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_reader_page.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
16 |
17 |
23 |
24 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/view/CoverImageView.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.view;
2 |
3 | import android.content.Context;
4 | import android.graphics.drawable.Drawable;
5 | import android.util.AttributeSet;
6 |
7 | import androidx.appcompat.widget.AppCompatImageView;
8 |
9 | public class CoverImageView extends AppCompatImageView {
10 |
11 | public CoverImageView(Context context) {
12 | super(context);
13 | }
14 |
15 | public CoverImageView(Context context, AttributeSet attributeSet) {
16 | super(context, attributeSet);
17 | }
18 |
19 | @Override
20 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
21 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
22 | int width = getMeasuredWidth();
23 | setMeasuredDimension(width, width * 4 / 3);
24 | }
25 |
26 | @Override
27 | public void setImageDrawable(Drawable drawable) {
28 | super.setImageDrawable(drawable);
29 |
30 | if (drawable != null) {
31 | int width = drawable.getIntrinsicWidth();
32 | int height = drawable.getIntrinsicHeight();
33 | double ratio = (double) height / (double) width;
34 | if (1.2 <= ratio && ratio <= 1.6) {
35 | setScaleType(ScaleType.CENTER_CROP);
36 | } else {
37 | setScaleType(ScaleType.FIT_CENTER);
38 | }
39 | }
40 |
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/view/CoverViewHolder.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.view;
2 |
3 | import android.net.Uri;
4 | import android.view.View;
5 | import android.widget.ImageView;
6 | import android.widget.TextView;
7 |
8 | import androidx.recyclerview.widget.RecyclerView;
9 |
10 | import com.squareup.picasso.Picasso;
11 |
12 | import be.nosuid.bubble.R;
13 |
14 | public abstract class CoverViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
15 | private ImageView mCoverView;
16 | private TextView mTitleTextView;
17 | private TextView mProgressTextView;
18 |
19 | public CoverViewHolder(View itemView) {
20 | super(itemView);
21 | mCoverView = itemView.findViewById(R.id.coverImageView);
22 | mTitleTextView = itemView.findViewById(R.id.coverTitleTextView);
23 | mProgressTextView = itemView.findViewById(R.id.coverProgressTextView);
24 |
25 | itemView.setClickable(true);
26 | itemView.setOnClickListener(this);
27 | }
28 |
29 | protected void setupText(String title, int progress, int maxProgress) {
30 | mTitleTextView.setText(title);
31 | mProgressTextView.setText(String.format("%d/%d", progress, maxProgress));
32 | }
33 |
34 | protected void setupCover(Picasso picasso, Uri thumbnail, Object tag) {
35 | picasso.load(thumbnail)
36 | .tag(tag)
37 | .into(mCoverView);
38 | }
39 |
40 | @Override
41 | public abstract void onClick(View v);
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/app_preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
17 |
18 |
23 |
24 |
29 |
30 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/view/ComicSeekBar.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.view;
2 |
3 | import android.content.Context;
4 | import android.graphics.Canvas;
5 | import android.util.AttributeSet;
6 |
7 | import androidx.appcompat.widget.AppCompatSeekBar;
8 |
9 | public class ComicSeekBar extends AppCompatSeekBar {
10 | private boolean mProgressToRight = false;
11 |
12 | public ComicSeekBar(Context context) {
13 | super(context);
14 | }
15 |
16 | public ComicSeekBar(Context context, AttributeSet attrs) {
17 | super(context, attrs);
18 | }
19 |
20 | public ComicSeekBar(Context context, AttributeSet attrs, int defStyle) {
21 | super(context, attrs, defStyle);
22 | }
23 |
24 | public void setProgressToRight(boolean toRight) {
25 | mProgressToRight = toRight;
26 |
27 | // Force redraw the progress bar
28 | onSizeChanged(getWidth(), getHeight(), 0, 0);
29 | }
30 |
31 | public void setMaxPageCount(int pageCount) {
32 | // Progress is from 0 to (pageCount -1)
33 | setMax(pageCount - 1);
34 | }
35 |
36 | public void setPageProgress(int pageNum) {
37 | // Progress is from 0 to (pageCount -1)
38 | setProgress(pageNum - 1);
39 | }
40 |
41 | @Override
42 | protected void onDraw(Canvas canvas) {
43 | if (mProgressToRight) {
44 | float px = this.getWidth() / 2.0f;
45 | float py = this.getHeight() / 2.0f;
46 |
47 | canvas.scale(-1, 1, px, py);
48 | }
49 | super.onDraw(canvas);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 29
5 |
6 | defaultConfig {
7 | applicationId "be.nosuid.bubble"
8 | versionName "1.0"
9 | minSdkVersion 16
10 | targetSdkVersion 29
11 | versionCode 1
12 | }
13 |
14 | buildTypes {
15 | release {
16 | // NPE if enabled
17 | minifyEnabled false
18 | }
19 | debug {
20 | applicationIdSuffix ".debug"
21 | debuggable true
22 | }
23 |
24 | }
25 |
26 | compileOptions {
27 | sourceCompatibility = 1.8
28 | targetCompatibility = 1.8
29 | }
30 | }
31 |
32 | dependencies {
33 | implementation 'androidx.appcompat:appcompat:1.2.0'
34 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
35 | implementation 'androidx.cardview:cardview:1.0.0'
36 | implementation 'androidx.recyclerview:recyclerview:1.1.0'
37 | implementation 'com.google.android.material:material:1.2.1'
38 | implementation 'androidx.preference:preference:1.1.1'
39 |
40 | implementation 'com.squareup.picasso:picasso:2.8'
41 |
42 | implementation 'com.squareup.retrofit2:retrofit:2.7.1'
43 | implementation 'com.squareup.retrofit2:converter-gson:2.7.1'
44 | implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1'
45 |
46 | implementation 'io.reactivex.rxjava2:rxjava:2.2.16'
47 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
48 |
49 | // >3.12 need minSdkVersion 21
50 | implementation('com.squareup.okhttp3:okhttp') { version { strictly '3.12.12' } }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/reader.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 | -
9 |
10 |
11 |
13 |
15 |
16 |
17 |
18 | -
22 |
23 |
24 |
26 |
28 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
33 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_library.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
17 |
18 |
19 |
25 |
26 |
31 |
32 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/view/SwipeOutViewPager.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.view;
2 |
3 |
4 | import android.content.Context;
5 | import android.util.AttributeSet;
6 | import android.view.MotionEvent;
7 |
8 | import androidx.viewpager.widget.ViewPager;
9 |
10 | public class SwipeOutViewPager extends ViewPager {
11 | private float mStartX = 0;
12 | private OnSwipeOutListener mSwipeOutListener;
13 |
14 | public interface OnSwipeOutListener {
15 | void onSwipeOutAtStart();
16 |
17 | void onSwipeOutAtEnd();
18 | }
19 |
20 | public SwipeOutViewPager(Context context, AttributeSet attrs) {
21 | super(context, attrs);
22 | }
23 |
24 | public SwipeOutViewPager(Context context) {
25 | super(context);
26 | }
27 |
28 | public void setOnSwipeOutListener(OnSwipeOutListener listener) {
29 | mSwipeOutListener = listener;
30 | }
31 |
32 | @Override
33 | public boolean onTouchEvent(MotionEvent ev) {
34 | if (ev.getAction() == MotionEvent.ACTION_UP) {
35 | float diff = ev.getX() - mStartX;
36 |
37 | if (diff > 0 && getCurrentItem() == 0) {
38 | if (mSwipeOutListener != null)
39 | mSwipeOutListener.onSwipeOutAtStart();
40 | } else if (diff < 0 && getCurrentItem() == (getAdapter().getCount() - 1)) {
41 | if (mSwipeOutListener != null)
42 | mSwipeOutListener.onSwipeOutAtEnd();
43 | }
44 | }
45 | return super.onTouchEvent(ev);
46 | }
47 |
48 | @Override
49 | public boolean onInterceptTouchEvent(MotionEvent ev) {
50 | if (ev.getAction() == MotionEvent.ACTION_DOWN) {
51 | mStartX = ev.getX();
52 | }
53 |
54 | return super.onInterceptTouchEvent(ev);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
23 |
24 |
29 |
30 |
31 |
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/card_cover.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
18 |
19 |
23 |
24 |
33 |
34 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/model/Comic.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.model;
2 |
3 | import java.io.Serializable;
4 | import java.util.Comparator;
5 |
6 | public class Comic implements Serializable {
7 | private String mId;
8 | private String mName;
9 | private String mSeriesId;
10 | private int mChapterNum;
11 | private int mPageLastRead;
12 | private int mPagesCount;
13 |
14 | public Comic(String id, String name, String seriesId, int chapterNum, int pageLastRead, int pagesCount) {
15 | mId = id;
16 | mName = name;
17 | mSeriesId = seriesId;
18 | mChapterNum = chapterNum;
19 | mPageLastRead = pageLastRead;
20 | mPagesCount = pagesCount;
21 | }
22 |
23 | public String getId() {
24 | return mId;
25 | }
26 |
27 | public String getName() {
28 | return mName;
29 | }
30 |
31 | public String getSeriesId() {
32 | return mSeriesId;
33 | }
34 |
35 | public int getChapterNum() {
36 | return mChapterNum;
37 | }
38 |
39 | public int getPageLastRead() {
40 | return mPageLastRead;
41 | }
42 |
43 | public void setPageLastRead(int pageNum) {
44 | mPageLastRead = pageNum;
45 | }
46 |
47 | public int getPagesCount() {
48 | return mPagesCount;
49 | }
50 |
51 |
52 | @Override
53 | public boolean equals(Object o) {
54 | return (o instanceof Comic) && getId().equals(((Comic) o).getId());
55 | }
56 |
57 | public static class ChapterOrderComparator implements Comparator {
58 | @Override
59 | public int compare(Comic c1, Comic c2) {
60 | return c1.getChapterNum() - c2.getChapterNum();
61 | }
62 | }
63 |
64 | public static class ChapterReverseOrderComparator implements Comparator {
65 | @Override
66 | public int compare(Comic c1, Comic c2) {
67 | return c2.getChapterNum() - c1.getChapterNum();
68 | }
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/activity/ReaderActivity.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.activity;
2 |
3 | import android.os.Bundle;
4 | import android.view.MenuItem;
5 |
6 | import androidx.appcompat.app.ActionBar;
7 | import androidx.appcompat.app.AppCompatActivity;
8 | import androidx.appcompat.widget.Toolbar;
9 | import androidx.fragment.app.Fragment;
10 |
11 | import be.nosuid.bubble.R;
12 | import be.nosuid.bubble.fragment.ReaderFragment;
13 | import be.nosuid.bubble.model.Comic;
14 |
15 | public class ReaderActivity extends AppCompatActivity {
16 |
17 | @Override
18 | public void onCreate(Bundle savedInstanceState) {
19 | super.onCreate(savedInstanceState);
20 |
21 | setContentView(R.layout.layout_reader);
22 |
23 | Toolbar toolbar = findViewById(R.id.toolbar_reader);
24 | setSupportActionBar(toolbar);
25 |
26 | if (savedInstanceState == null) {
27 | Bundle extras = getIntent().getExtras();
28 | Comic comic = (Comic) extras.getSerializable(ReaderFragment.PARAM_COMIC);
29 | ReaderFragment fragment = ReaderFragment.create(comic);
30 | setFragment(fragment);
31 | }
32 |
33 | ActionBar actionBar = getSupportActionBar();
34 | if (actionBar != null) {
35 | actionBar.setDisplayHomeAsUpEnabled(true);
36 | }
37 | }
38 |
39 | public void setFragment(Fragment fragment) {
40 | getSupportFragmentManager()
41 | .beginTransaction()
42 | .replace(R.id.content_frame_reader, fragment)
43 | .commit();
44 |
45 | }
46 |
47 | @Override
48 | public boolean onOptionsItemSelected(MenuItem item) {
49 | switch (item.getItemId()) {
50 | case android.R.id.home:
51 | finish();
52 | return true;
53 | }
54 | return super.onOptionsItemSelected(item);
55 | }
56 |
57 | @Override
58 | public void onBackPressed() {
59 | super.onBackPressed();
60 | finish();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bubble-Komga
2 |
3 | A fork of [Bubble](https://github.com/nkanaev/bubble) that support [Komga](https://github.com/gotson/komga) as comics source
4 |
5 | ## Supported Komga features
6 |
7 | - Browse the series and thumbnails of one library (no multi-library support)
8 | - Browse the books, thumbnails and read status of a selected series
9 | - View the pages of a selected book
10 | - Go to the next/previous book from the last/first page of a book
11 | - Update the page read progress of a book
12 |
13 | ## Notable changes from [Bubble](https://github.com/nkanaev/bubble)
14 |
15 | - Drop support for local files and archives
16 | - Add support for [Komga](https://github.com/gotson/komga) server using [Retrofit2](https://square.github.io/retrofit/), [RxJava](https://github.com/ReactiveX/RxJava) and [Picasso](https://square.github.io/picasso/)
17 | - Use the read progress feature of Komga (see [Komga issue #25](https://github.com/gotson/komga/issues/25)) instead of a local `sqlite` database
18 | - Convert to AndroidX, update Gradle
19 | - Keep `minSdkVersion 16`
20 | - Refactor all `Fragment`
21 |
22 | # Development
23 |
24 | ## Plan for future version
25 |
26 | This code is provided as-is.
27 |
28 | My plan for this repo is to provide fix for bugs should some be reported and maintain compatibility with the API of future version of Komga if needed, but I do not plan to add any additional feature for now.
29 |
30 | My goal with this fork is:
31 |
32 | - To support my own personnal use of Komga
33 | - To provide ideas or a possible baseline for a Komga-specific Android App (see [Komga issue #36](https://github.com/gotson/komga/issues/36))
34 |
35 | Feel free to fork it for your own development!
36 |
37 | ## Build/Install the debug APK
38 |
39 | - Build using `./gradlew assembleDebug`
40 | - Push the debug APK to your device using `adb install -r -t app/build/outputs/apk/debug/app-debug.apk`
41 |
42 | ## Build the release APK
43 |
44 | - Build using `./gradlew assembleRelease`
45 |
46 | ## TODO
47 |
48 | - SettingsFragment is ugly but works
49 | - PageImageView is mostly untouched and may need some additional cleanup
50 |
51 | # License
52 |
53 | Source code is available under the MIT license. See the LICENSE for more info.
54 |
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bubble-Komga
4 | Suche
5 | Nein
6 | Ja
7 | Möchten Sie zum nächsten Comic wechseln?
8 | Möchten Sie zum vorhergehenden Comic wechseln?
9 | Lesemodus
10 | Comics
11 | Manga
12 | Neu Laden
13 | Einstellungen
14 | Ansicht
15 | Ausfüllen
16 | Einpassen
17 | Breite ausfüllen
18 | Comic-Reader für Android
19 | Bitte einen Suchbegriff eingeben
20 | Abbrechen
21 | Auswählen
22 | Bibliotheksverzeichnis auswählen
23 | Navigationspanel ausblenden
24 | Navigationspanel öffnen
25 | Alle
26 | Zuletzt gelesen
27 | Über
28 | Filter
29 | Alle
30 | Gelesen
31 | Gerade am Lesen
32 | Angefangen
33 | Ungelesen
34 | Bibliothek
35 | Neu laden
36 |
--------------------------------------------------------------------------------
/app/src/main/res/values-nl/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bubble-Komga
4 | Android-striplezer
5 |
6 | Verversen
7 | Instellingen
8 |
9 | Weergavemodus
10 | Volledig opvullen
11 | Passend maken
12 | Breedte opvullen
13 |
14 | Leesmodus
15 | Strips
16 | Manga
17 |
18 | Filter
19 | Alles
20 | Gelezen
21 | Momenteel aan het lezen
22 | Ongelezen
23 | Niet-afgerond
24 |
25 | Navigatiemenu openen
26 | Navigatiemenu sluiten
27 |
28 | Bibliotheek
29 | Over
30 |
31 | Recent
32 | Alles
33 |
34 | Stel een bibliotheekmap in
35 | Selecteren
36 | Annuleren
37 |
38 | Zoeken
39 |
40 | Voer een zoekopdracht in
41 |
42 | Herladen
43 |
44 | Ja
45 | Nee
46 | Wil je overschakelen naar de volgende strip?
47 | Wil je overschakelen naar de vorige strip?
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/komga/Series.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.komga;
2 |
3 | import com.google.gson.annotations.Expose;
4 | import com.google.gson.annotations.SerializedName;
5 |
6 | import java.util.List;
7 |
8 | public class Series {
9 |
10 | @SerializedName("content")
11 | @Expose
12 | private List content = null;
13 | @SerializedName("last")
14 | @Expose
15 | private Boolean last;
16 |
17 | public List getContent() {
18 | return content;
19 | }
20 |
21 | public void setContent(List content) {
22 | this.content = content;
23 | }
24 |
25 | public Boolean isLast() {
26 | return last;
27 | }
28 |
29 | public void setLast(Boolean last) {
30 | this.last = last;
31 | }
32 |
33 | public static class Content {
34 | @SerializedName("booksCount")
35 | @Expose
36 | private Integer booksCount;
37 | @SerializedName("booksReadCount")
38 | @Expose
39 | private Integer booksReadCount;
40 | @SerializedName("id")
41 | @Expose
42 | private String id;
43 | @SerializedName("libraryId")
44 | @Expose
45 | private String libraryId;
46 | @SerializedName("name")
47 | @Expose
48 | private String name;
49 |
50 | public Integer getBooksCount() {
51 | return booksCount;
52 | }
53 |
54 | public void setBooksCount(Integer booksCount) {
55 | this.booksCount = booksCount;
56 | }
57 |
58 | public Integer getBooksReadCount() {
59 | return booksReadCount;
60 | }
61 |
62 | public void setBooksReadCount(Integer booksReadCount) {
63 | this.booksReadCount = booksReadCount;
64 | }
65 |
66 | public String getId() {
67 | return id;
68 | }
69 |
70 | public void setId(String id) {
71 | this.id = id;
72 | }
73 |
74 | public String getLibraryId() {
75 | return libraryId;
76 | }
77 |
78 | public void setLibraryId(String libraryId) {
79 | this.libraryId = libraryId;
80 | }
81 |
82 | public String getName() {
83 | return name;
84 | }
85 |
86 | public void setName(String name) {
87 | this.name = name;
88 | }
89 | }
90 |
91 | }
92 |
93 |
--------------------------------------------------------------------------------
/app/src/main/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bubble-Komga
4 | Lector de cómics para android
5 |
6 | Actualizar
7 | Configuración
8 |
9 | Modo de visualización
10 | Llenar aspecto
11 | Ajuste de aspecto
12 | Ajuste de ancho
13 |
14 | Modo de lectura
15 | Cómics
16 | Manga
17 |
18 | Filtro
19 | Todos
20 | Leer
21 | Actualmente leyendo
22 | Sin leer
23 | Incompleto
24 |
25 | Abrir panel de navegación
26 | Cerrar el panel de navegación
27 |
28 | Biblioteca
29 | Acerca de
30 |
31 | Reciente
32 | Todos
33 |
34 | Establecer un directorio de la Biblioteca
35 | Seleccionar
36 | Cancelar
37 |
38 | Buscar
39 |
40 | Por favor introduzca un término de búsqueda
41 |
42 | Recargar
43 |
44 | Si
45 | No
46 | ¿Le gustaría pasar al siguiente cómic?
47 | ¿Le gustaría cambiar al cómic anterior?
48 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ro/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bubble-Komga
4 | Cititor de benzi desenate pentru Android
5 |
6 | Reactualizează
7 | Setări
8 |
9 | Modul de vizualizare
10 | Aspect umplere
11 | Aspect potrivire
12 | Potrivește pe lățime
13 |
14 | Modul de citire
15 | Benzi desenate
16 | Manga
17 |
18 | Filtrează
19 | Toate
20 | Citește
21 | În curs de citire
22 | Necitite
23 | Neterminate
24 |
25 | Deschide meniul de navigare
26 | Închide meniul de navigare
27 |
28 | Bibliotecă
29 | Despre
30 |
31 | Recente
32 | Toate
33 |
34 | Setează directorul bibliotecii
35 | Selectează
36 | Anulează
37 |
38 | Caută
39 |
40 | Vă rugăm introduceți un termen de căutare
41 |
42 | Reîncarcă
43 |
44 | Da
45 | Nu
46 | Doriți să treceți la următoarele benzi desenate?
47 | Doriți să treceți la benzile desenate anterioare?
48 |
49 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 | Bubble-Komga
13 | Lecteur de BDs pour Android
14 |
15 | Rafraichir
16 | Paramètres
17 |
18 | Vue
19 | Remplissage
20 | Adaptée
21 | Largeur adaptée
22 |
23 |
24 |
25 |
26 |
27 | Filtre
28 | Tout
29 | Lu
30 | En lecture
31 | Non lu
32 | Inachevé
33 |
34 | Ouvrir le volet de navigation
35 | Fermer le volet de navigation
36 |
37 | Bibliothèque
38 | À propos
39 |
40 | Définissez le répertoire de vos BDs
41 | Selectionner
42 | Annuler
43 |
44 | Rechercher
45 |
46 | Veuillez entrer un terme de recherche
47 |
48 | Recharger
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_reader.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
18 |
19 |
25 |
26 |
40 |
41 |
46 |
47 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/managers/Utils.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.managers;
2 |
3 | import android.content.Context;
4 | import android.os.Build;
5 | import android.util.DisplayMetrics;
6 | import android.util.Log;
7 |
8 | import io.reactivex.CompletableObserver;
9 | import io.reactivex.MaybeObserver;
10 | import io.reactivex.SingleObserver;
11 | import io.reactivex.disposables.Disposable;
12 | import io.reactivex.internal.disposables.DisposableContainer;
13 |
14 |
15 | public final class Utils {
16 | public static boolean isKitKatOrLater() {
17 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
18 | }
19 |
20 | public static boolean isLollipopOrLater() {
21 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
22 | }
23 |
24 | public static int getScreenWidth(Context context) {
25 | DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
26 | return Math.round(displayMetrics.widthPixels / displayMetrics.density);
27 | }
28 |
29 | public static int getScreenHeight(Context context) {
30 | DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
31 | return Math.round(displayMetrics.heightPixels / displayMetrics.density);
32 | }
33 |
34 | public static abstract class DefaultObserver {
35 | protected String mTag;
36 | protected DisposableContainer mDisposableContainer;
37 |
38 | public DefaultObserver(String tag, DisposableContainer disposableContainer) {
39 | mTag = tag;
40 | mDisposableContainer = disposableContainer;
41 | }
42 |
43 | public void onSubscribe(Disposable d) {
44 | Log.d(mTag, "onSubscribe");
45 | mDisposableContainer.add(d);
46 | }
47 |
48 | public void onError(Throwable e) {
49 | Log.d(mTag, "onError");
50 | Log.e(mTag, e.toString());
51 | }
52 | }
53 |
54 | public static abstract class DefaultSingleObserver extends DefaultObserver implements SingleObserver {
55 | public DefaultSingleObserver(String tag, DisposableContainer disposableContainer) {
56 | super(tag, disposableContainer);
57 | }
58 |
59 | abstract public void onSuccess(T t);
60 | }
61 |
62 | public static abstract class DefaultMaybeObserver extends DefaultObserver implements MaybeObserver {
63 | public DefaultMaybeObserver(String tag, DisposableContainer disposableContainer) {
64 | super(tag, disposableContainer);
65 | }
66 |
67 | abstract public void onSuccess(T t);
68 |
69 | abstract public void onComplete();
70 | }
71 |
72 | public static abstract class DefaultCompletableObserver extends DefaultObserver implements CompletableObserver {
73 | public DefaultCompletableObserver(String tag, DisposableContainer disposableContainer) {
74 | super(tag, disposableContainer);
75 | }
76 |
77 | abstract public void onComplete();
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/komga/Books.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.komga;
2 |
3 | import com.google.gson.annotations.Expose;
4 | import com.google.gson.annotations.SerializedName;
5 |
6 | import java.util.List;
7 |
8 | public class Books {
9 |
10 | @SerializedName("content")
11 | @Expose
12 | private List content = null;
13 | @SerializedName("last")
14 | @Expose
15 | private Boolean last;
16 |
17 | public List getContent() {
18 | return content;
19 | }
20 |
21 | public void setContent(List content) {
22 | this.content = content;
23 | }
24 |
25 | public Boolean isLast() {
26 | return last;
27 | }
28 |
29 | public void setLast(Boolean last) {
30 | this.last = last;
31 | }
32 |
33 | public static class Content {
34 |
35 | @SerializedName("id")
36 | @Expose
37 | private String id;
38 | @SerializedName("seriesId")
39 | @Expose
40 | private String seriesId;
41 | @SerializedName("name")
42 | @Expose
43 | private String name;
44 | @SerializedName("number")
45 | @Expose
46 | private Integer number;
47 | @SerializedName("media")
48 | @Expose
49 | private Media media;
50 | @SerializedName("readProgress")
51 | @Expose
52 | private ReadProgress readProgress;
53 |
54 | public String getId() {
55 | return id;
56 | }
57 |
58 | public void setId(String id) {
59 | this.id = id;
60 | }
61 |
62 | public String getSeriesId() {
63 | return seriesId;
64 | }
65 |
66 | public void setSeriesId(String seriesId) {
67 | this.seriesId = seriesId;
68 | }
69 |
70 | public String getName() {
71 | return name;
72 | }
73 |
74 | public void setName(String name) {
75 | this.name = name;
76 | }
77 |
78 | public Integer getNumber() {
79 | return number;
80 | }
81 |
82 | public void setNumber(Integer number) {
83 | this.number = number;
84 | }
85 |
86 | public Media getMedia() {
87 | return media;
88 | }
89 |
90 | public void setMedia(Media media) {
91 | this.media = media;
92 | }
93 |
94 | public ReadProgress getReadProgress() {
95 | if (this.readProgress != null) {
96 | return readProgress;
97 | }
98 |
99 | // No status for this book, create an empty one
100 | ReadProgress readProgress = new ReadProgress();
101 | readProgress.setCompleted(false);
102 | readProgress.setPage(0);
103 | return readProgress;
104 | }
105 |
106 | public void setReadProgress(ReadProgress readProgress) {
107 | this.readProgress = readProgress;
108 | }
109 |
110 | }
111 |
112 | }
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/fragment/SettingsFragment.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.fragment;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.os.Bundle;
6 | import android.widget.Toast;
7 |
8 | import androidx.preference.PreferenceFragmentCompat;
9 | import androidx.preference.PreferenceManager;
10 |
11 | import be.nosuid.bubble.MainApplication;
12 | import be.nosuid.bubble.R;
13 | import be.nosuid.bubble.managers.SharedResourcesManager;
14 | import be.nosuid.bubble.managers.Utils;
15 | import io.reactivex.disposables.CompositeDisposable;
16 |
17 |
18 | public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
19 |
20 | private SharedResourcesManager mSharedRes;
21 | private CompositeDisposable mCompositeDisposable;
22 |
23 | @Override
24 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
25 | MainApplication app = (MainApplication) getActivity().getApplication();
26 | mSharedRes = app.getSharedResourcesManager();
27 | mCompositeDisposable = new CompositeDisposable();
28 |
29 | PreferenceManager preferenceManager = getPreferenceManager();
30 | preferenceManager.setSharedPreferencesName(app.getString(R.string.setting_key_preferences_file));
31 | preferenceManager.setSharedPreferencesMode(Context.MODE_PRIVATE);
32 | preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
33 |
34 | addPreferencesFromResource(R.xml.app_preferences);
35 |
36 | findPreference(getString(R.string.setting_key_komga_test_button)).setOnPreferenceClickListener(preference -> {
37 | mSharedRes.getLibraryManager()
38 | .ping()
39 | .subscribe(new Utils.DefaultMaybeObserver("testKomgaSettings", mCompositeDisposable) {
40 | @Override
41 | public void onSuccess(Boolean aBoolean) {
42 | Toast.makeText(app, R.string.komga_test_api_ok, Toast.LENGTH_SHORT)
43 | .show();
44 | }
45 |
46 | @Override
47 | public void onComplete() {
48 | Toast.makeText(app, R.string.komga_test_api_error, Toast.LENGTH_SHORT)
49 | .show();
50 | }
51 | });
52 | return true;
53 | });
54 | }
55 |
56 | @Override
57 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
58 | mSharedRes.onSettingsChange();
59 | }
60 |
61 | @Override
62 | public void onResume() {
63 | super.onResume();
64 | getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
65 | }
66 |
67 | @Override
68 | public void onPause() {
69 | super.onPause();
70 | getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/komga/KomgaApi.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.komga;
2 |
3 | import java.util.List;
4 |
5 | import io.reactivex.Completable;
6 | import io.reactivex.Maybe;
7 | import io.reactivex.Single;
8 | import retrofit2.http.Body;
9 | import retrofit2.http.GET;
10 | import retrofit2.http.PATCH;
11 | import retrofit2.http.Path;
12 | import retrofit2.http.Query;
13 |
14 | public interface KomgaApi {
15 | enum SortingMethod {
16 | NAME_ASC("name,asc"),
17 | NAME_DESC("name,desc"),
18 | NUMBER_ASC("number,asc"),
19 | NUMBER_DESC("number,desc"),
20 |
21 | // From metadata
22 | // Only usable for Series
23 | METADATA_TITLE_ASC("metadata.titleSort,asc"),
24 | // Only usable for Books
25 | METADATA_NUMBER_ASC("metadata.numberSort,asc");
26 |
27 |
28 | public final String sortingMethod;
29 |
30 | SortingMethod(String sortingMethod) {
31 | this.sortingMethod = sortingMethod;
32 | }
33 |
34 | @Override
35 | public String toString() {
36 | return this.sortingMethod;
37 | }
38 | }
39 |
40 | enum ReadStatus {
41 | UNREAD("UNREAD"),
42 | READ("READ"),
43 | IN_PROGRESS("IN_PROGRESS");
44 |
45 | public final String readStatus;
46 |
47 | ReadStatus(String readStatus) {
48 | this.readStatus = readStatus;
49 | }
50 |
51 | @Override
52 | public String toString() {
53 | return this.readStatus;
54 | }
55 | }
56 |
57 | @GET("series/{seriesId}/books")
58 | Single getSeriesBooksForPage(@Path("seriesId") String seriesId,
59 | @Query("page") int page,
60 | @Query("size") int pageSize,
61 | @Query("sort") SortingMethod sortingMethod);
62 |
63 | @GET("series/{seriesId}/books")
64 | Single getSeriesBooksForPage(@Path("seriesId") String seriesId,
65 | @Query("page") int page,
66 | @Query("size") int pageSize,
67 | @Query("sort") SortingMethod sortingMethod,
68 | @Query("read_status") List readStatus);
69 |
70 | @GET("series")
71 | Single getSeriesForPage(@Query("library_id") String libraryId,
72 | @Query("page") int page,
73 | @Query("size") int pageSize,
74 | @Query("sort") SortingMethod sortingMethod);
75 |
76 | @GET("books/{bookId}/next")
77 | Maybe getBookSiblingNext(@Path("bookId") String bookId);
78 |
79 | @GET("books/{bookId}/previous")
80 | Maybe getBookSiblingPrevious(@Path("bookId") String bookId);
81 |
82 | @PATCH("books/{bookId}/read-progress")
83 | Completable setBookReadProgress(@Path("bookId") String bookId, @Body ReadProgress readProgress);
84 |
85 | @GET("users/me")
86 | Single getUserMe();
87 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_about.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
16 |
17 |
23 |
24 |
30 |
31 |
39 |
40 |
49 |
50 |
56 |
57 |
68 |
69 |
70 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bubble-Komga
4 | Android comic reader
5 |
6 | Refresh
7 | Settings
8 |
9 | View Mode
10 | Aspect Fill
11 | Aspect Fit
12 | Fit Width
13 |
14 | Reading Mode
15 | Comics
16 | Manga
17 |
18 | Filter
19 | All
20 | Read
21 | Currently reading
22 | Unread
23 | Unfinished
24 |
25 | Open navigation drawer
26 | Close navigation drawer
27 |
28 | Library
29 | Settings
30 | About
31 |
32 | Recent
33 | All
34 |
35 | Set a library directory
36 | Select
37 | Cancel
38 |
39 | Search
40 |
41 | Please enter a search term
42 |
43 | Reload
44 |
45 | Yes
46 | No
47 | Ok
48 | Would you like to switch to the next comic?
49 | Would you like to switch to the previous comic?
50 | This comic is the last one
51 | This comic is the first one
52 | Switching to the next comic
53 | Switching to the previous comic
54 |
55 | Your collection seems empty.\n Check your connection with Komga
56 |
57 | Ok
58 | Could not connect to the Komga server
59 |
60 | Komga Server
61 | Server URL
62 | Username
63 | Password
64 | Library Id
65 | Test connection
66 | Click me to test the connection
67 |
68 | setting_key_preferences
69 | setting_key_komga_url
70 | setting_key_komga_username
71 | setting_key_komga_password
72 | setting_key_komga_library_id
73 | setting_key_komga_test_button
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/card_deps.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
17 |
18 |
25 |
26 |
37 |
38 |
50 |
51 |
52 |
58 |
59 |
69 |
70 |
76 |
77 |
86 |
87 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/managers/SharedResourcesManager.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.managers;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.util.Log;
6 |
7 | import com.squareup.picasso.OkHttp3Downloader;
8 | import com.squareup.picasso.Picasso;
9 |
10 | import be.nosuid.bubble.R;
11 | import be.nosuid.bubble.komga.KomgaApi;
12 | import okhttp3.Credentials;
13 | import okhttp3.OkHttpClient;
14 | import okhttp3.Request;
15 | import retrofit2.Retrofit;
16 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
17 | import retrofit2.converter.gson.GsonConverterFactory;
18 |
19 | public class SharedResourcesManager implements SettingsManager.OnSettingsChangeListener {
20 |
21 | private Context mAppContext;
22 | private SettingsManager mSettingsManager;
23 | private OkHttpClient mOkHttpClient;
24 | private Picasso mPicasso;
25 | private KomgaApi mKomgaApi;
26 | private LibraryManager mLibraryManager;
27 |
28 | public SharedResourcesManager(Context appContext) {
29 | mAppContext = appContext;
30 | SharedPreferences sharedPrefs = mAppContext.getSharedPreferences(
31 | appContext.getString(R.string.setting_key_preferences_file), Context.MODE_PRIVATE);
32 |
33 | mSettingsManager = new SettingsManager(sharedPrefs, mAppContext.getResources(), this);
34 |
35 | onSettingsChange();
36 | }
37 |
38 | @Override
39 | public void onSettingsChange() {
40 | Log.d("SharedResourcesManager", "onSettingsChange");
41 | mOkHttpClient = createHttpClient();
42 | if (mPicasso != null) {
43 | mPicasso.shutdown();
44 | }
45 | mPicasso = createPicasso();
46 | mKomgaApi = createKomgaApi();
47 |
48 | mLibraryManager = createLibraryManager();
49 | }
50 |
51 | private OkHttpClient createHttpClient() {
52 | String authToken = Credentials.basic(
53 | mSettingsManager.getKomgaUsername(),
54 | mSettingsManager.getKomgaPassword());
55 |
56 | //TODO: Check if bad user/pass : java.net.ProtocolException: Too many follow-up requests: 21
57 |
58 | return new OkHttpClient.Builder()
59 | .authenticator((route, response) -> response.request().newBuilder()
60 | .header("Authorization", authToken)
61 | .build())
62 | .addInterceptor(chain -> {
63 | Request request = chain.request();
64 | Log.d("OkHttpClient", String.format("requesting: %s", request.url()));
65 | return chain.proceed(request);
66 | })
67 | .build();
68 | }
69 |
70 | private Picasso createPicasso() {
71 | if (getOkHttpClient() == null) {
72 | return null;
73 | }
74 |
75 | return new Picasso.Builder(mAppContext)
76 | .downloader(new OkHttp3Downloader(getOkHttpClient()))
77 | .build();
78 |
79 | }
80 |
81 | private KomgaApi createKomgaApi() {
82 | if (getOkHttpClient() == null) {
83 | return null;
84 | }
85 |
86 | return new Retrofit.Builder()
87 | .baseUrl(mSettingsManager.getKomgaApiUrl())
88 | .client(getOkHttpClient())
89 | .addConverterFactory(GsonConverterFactory.create())
90 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
91 | .build()
92 | .create(KomgaApi.class);
93 | }
94 |
95 | private LibraryManager createLibraryManager() {
96 | if (getKomgaApi() == null) {
97 | return null;
98 | }
99 |
100 | return new LibraryManager(this);
101 | }
102 |
103 |
104 | private OkHttpClient getOkHttpClient() {
105 | return mOkHttpClient;
106 | }
107 |
108 | public SettingsManager getSettingsManager() {
109 | return mSettingsManager;
110 | }
111 |
112 | public Picasso getPicasso() {
113 | return mPicasso;
114 | }
115 |
116 | public KomgaApi getKomgaApi() {
117 | return mKomgaApi;
118 | }
119 |
120 | public LibraryManager getLibraryManager() {
121 | return mLibraryManager;
122 | }
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/managers/SettingsManager.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.managers;
2 |
3 | import android.content.SharedPreferences;
4 | import android.content.res.Resources;
5 |
6 | import java.util.HashMap;
7 |
8 | import be.nosuid.bubble.R;
9 |
10 | import static java.lang.Integer.parseInt;
11 |
12 | public class SettingsManager {
13 | public enum PageViewMode {
14 | ASPECT_FILL,
15 | ASPECT_FIT,
16 | FIT_WIDTH
17 | }
18 |
19 | public static final String SETTINGS_PAGE_VIEW_MODE = "SETTINGS_PAGE_VIEW_MODE";
20 | public static final String SETTINGS_READING_LEFT_TO_RIGHT = "SETTINGS_READING_LEFT_TO_RIGHT";
21 | public static final int MAX_LAST_READ_COUNT = 5;
22 |
23 | private SharedPreferences mSharedPreferences;
24 | private Resources mResources;
25 | private OnSettingsChangeListener mOnSettingsChangeListener;
26 |
27 | private final static HashMap RESOURCE_VIEW_MODE;
28 |
29 | static {
30 | RESOURCE_VIEW_MODE = new HashMap();
31 | RESOURCE_VIEW_MODE.put(R.id.view_mode_aspect_fill, PageViewMode.ASPECT_FILL);
32 | RESOURCE_VIEW_MODE.put(R.id.view_mode_aspect_fit, PageViewMode.ASPECT_FIT);
33 | RESOURCE_VIEW_MODE.put(R.id.view_mode_fit_width, PageViewMode.FIT_WIDTH);
34 | }
35 |
36 | public interface OnSettingsChangeListener {
37 | void onSettingsChange();
38 | }
39 |
40 | public SettingsManager(SharedPreferences sharedPrefs, Resources res,
41 | OnSettingsChangeListener oscl) {
42 | mSharedPreferences = sharedPrefs;
43 | mResources = res;
44 | mOnSettingsChangeListener = oscl;
45 | }
46 |
47 | public String getKomgaApiUrl() {
48 | String url = mSharedPreferences.getString(
49 | mResources.getString(R.string.setting_key_komga_url),
50 | "http://example/");
51 |
52 | if (!url.endsWith("/api/v1/")) {
53 | url += "/api/v1/";
54 | }
55 |
56 | return url;
57 | }
58 |
59 | public String getKomgaUsername() {
60 | return mSharedPreferences.getString(
61 | mResources.getString(R.string.setting_key_komga_username),
62 | "username");
63 | }
64 |
65 | public String getKomgaPassword() {
66 | return mSharedPreferences.getString(
67 | mResources.getString(R.string.setting_key_komga_password),
68 | "password");
69 | }
70 |
71 | public String getKomgaLibraryId() {
72 | return mSharedPreferences.getString(
73 | mResources.getString(R.string.setting_key_komga_library_id),
74 | "1");
75 | }
76 |
77 | public void setIsReadingLeftToRight(boolean isReadingLeftToRight) {
78 | mSharedPreferences.edit()
79 | .putBoolean(SETTINGS_READING_LEFT_TO_RIGHT, isReadingLeftToRight)
80 | .apply();
81 | }
82 |
83 | public boolean getIsReadingLeftToRight() {
84 | return mSharedPreferences.getBoolean(SETTINGS_READING_LEFT_TO_RIGHT, true);
85 | }
86 |
87 | public void setPageViewMode(PageViewMode pv) {
88 | mSharedPreferences.edit()
89 | .putInt(SETTINGS_PAGE_VIEW_MODE, pv.ordinal())
90 | .apply();
91 | }
92 |
93 | public PageViewMode getPageViewMode() {
94 | return PageViewMode.values()[mSharedPreferences.getInt(
95 | SETTINGS_PAGE_VIEW_MODE,
96 | PageViewMode.ASPECT_FIT.ordinal())];
97 | }
98 |
99 | public int getLibraryColumnsCount(int deviceWidth) {
100 | int minColumnWidth = mResources.getInteger(R.integer.cover_grid_library_min_column_width);
101 | int maxColumnCount = mResources.getInteger(R.integer.cover_grid_library_max_column_count);
102 |
103 | int columnsCount = Math.round((float) deviceWidth / minColumnWidth);
104 | if (columnsCount <= maxColumnCount) {
105 | return columnsCount;
106 | } else {
107 | return maxColumnCount;
108 | }
109 | }
110 |
111 | public int getSeriesColumnsCount(int deviceWidth) {
112 | int minColumnWidth = mResources.getInteger(R.integer.cover_grid_series_min_column_width);
113 | int maxColumnCount = mResources.getInteger(R.integer.cover_grid_series_max_column_count);
114 |
115 | int columnsCount = Math.round((float) deviceWidth / minColumnWidth);
116 | if (columnsCount <= maxColumnCount) {
117 | return columnsCount;
118 | } else {
119 | return maxColumnCount;
120 | }
121 | }
122 |
123 | public boolean getIsSwitchComicConfirmation() {
124 | return false;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/fragment/AboutFragment.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.fragment;
2 |
3 | import android.content.Intent;
4 | import android.content.pm.PackageInfo;
5 | import android.content.pm.PackageManager;
6 | import android.net.Uri;
7 | import android.os.Bundle;
8 | import android.view.LayoutInflater;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 | import android.widget.LinearLayout;
12 | import android.widget.TextView;
13 |
14 | import androidx.fragment.app.Fragment;
15 |
16 | import be.nosuid.bubble.R;
17 |
18 | public class AboutFragment extends Fragment implements View.OnClickListener {
19 | private class LibraryDescription {
20 | public final String name;
21 | public final String description;
22 | public final String license;
23 | public final String owner;
24 | public final String link;
25 |
26 | LibraryDescription(String name, String description, String license, String owner, String link) {
27 | this.name = name;
28 | this.description = description;
29 | this.license = license;
30 | this.owner = owner;
31 | this.link = link;
32 | }
33 | }
34 |
35 | private LibraryDescription[] mDescriptions = new LibraryDescription[]{
36 | new LibraryDescription(
37 | "Bubble",
38 | "Simple and beautiful app for all your offline comic books",
39 | "MIT",
40 | "nkanaev",
41 | "https://github.com/nkanaev/bubble"
42 | ),
43 | new LibraryDescription(
44 | "Komga",
45 | "Komga is a free and open source comics/mangas server.",
46 | "MIT",
47 | "gotson",
48 | "https://github.com/gotson/komga"
49 | ),
50 | new LibraryDescription(
51 | "Picasso",
52 | "A powerful image downloading and caching library for Android",
53 | "Apache Version 2.0",
54 | "Square",
55 | "https://square.github.io/picasso/"
56 | ),
57 | new LibraryDescription(
58 | "Retrofit2",
59 | "A type-safe HTTP client for Android and Java",
60 | "Apache Version 2.0",
61 | "Square",
62 | "https://square.github.io/retrofit/"
63 | ),
64 | new LibraryDescription(
65 | "RxJava",
66 | "A library for composing asynchronous and event-based programs using observable sequences for the Java VM",
67 | "Apache Version 2.0",
68 | "RxJava Contributors",
69 | "https://github.com/ReactiveX/RxJava"
70 | ),
71 | };
72 |
73 | @Override
74 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
75 | final View view = inflater.inflate(R.layout.fragment_about, container, false);
76 |
77 | LinearLayout libsLayout = view.findViewById(R.id.about_libraries);
78 |
79 | ((TextView) view.findViewById(R.id.aboutVersion)).setText(getVersionString());
80 |
81 | for (int i = 0; i < mDescriptions.length; i++) {
82 | View cardView = inflater.inflate(R.layout.card_deps, libsLayout, false);
83 |
84 | ((TextView) cardView.findViewById(R.id.libraryName)).setText(mDescriptions[i].name);
85 | ((TextView) cardView.findViewById(R.id.libraryCreator)).setText(mDescriptions[i].owner);
86 | ((TextView) cardView.findViewById(R.id.libraryDescription)).setText(mDescriptions[i].description);
87 | ((TextView) cardView.findViewById(R.id.libraryLicense)).setText(mDescriptions[i].license);
88 |
89 | cardView.setTag(mDescriptions[i].link);
90 | cardView.setOnClickListener(this);
91 | libsLayout.addView(cardView);
92 | }
93 |
94 | return view;
95 | }
96 |
97 | private String getVersionString() {
98 | try {
99 | PackageInfo pi = getActivity()
100 | .getPackageManager()
101 | .getPackageInfo(getActivity().getPackageName(), 0);
102 | return "Version " + pi.versionName + " (" + Integer.toString(pi.versionCode) + ")";
103 | } catch (PackageManager.NameNotFoundException e) {
104 | return "";
105 | }
106 | }
107 |
108 | @Override
109 | public void onClick(View v) {
110 | String link = (String) v.getTag();
111 | Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
112 | startActivity(browserIntent);
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/activity/MainActivity.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.activity;
2 |
3 | import android.content.res.Configuration;
4 | import android.os.Bundle;
5 | import android.view.MenuItem;
6 |
7 | import androidx.appcompat.app.ActionBar;
8 | import androidx.appcompat.app.ActionBarDrawerToggle;
9 | import androidx.appcompat.app.AppCompatActivity;
10 | import androidx.appcompat.widget.Toolbar;
11 | import androidx.core.view.GravityCompat;
12 | import androidx.drawerlayout.widget.DrawerLayout;
13 | import androidx.fragment.app.Fragment;
14 | import androidx.fragment.app.FragmentManager;
15 |
16 | import com.google.android.material.navigation.NavigationView;
17 |
18 | import be.nosuid.bubble.R;
19 | import be.nosuid.bubble.fragment.AboutFragment;
20 | import be.nosuid.bubble.fragment.LibraryFragment;
21 | import be.nosuid.bubble.fragment.SettingsFragment;
22 | import be.nosuid.bubble.managers.Utils;
23 |
24 |
25 | public class MainActivity extends AppCompatActivity implements FragmentManager.OnBackStackChangedListener {
26 | private final static String STATE_CURRENT_MENU_ITEM = "STATE_CURRENT_MENU_ITEM";
27 |
28 | private DrawerLayout mDrawerLayout;
29 | private ActionBarDrawerToggle mDrawerToggle;
30 | private int mCurrentNavItem;
31 |
32 | @Override
33 | public void onCreate(Bundle savedInstanceState) {
34 | super.onCreate(savedInstanceState);
35 | setContentView(R.layout.layout_main);
36 |
37 | Toolbar toolbar = findViewById(R.id.toolbar);
38 | setSupportActionBar(toolbar);
39 | getSupportFragmentManager().addOnBackStackChangedListener(this);
40 |
41 | if (Utils.isLollipopOrLater()) {
42 | toolbar.setElevation(8);
43 | }
44 |
45 | ActionBar actionBar = getSupportActionBar();
46 | if (actionBar != null) {
47 | actionBar.setHomeButtonEnabled(true);
48 | actionBar.setDisplayHomeAsUpEnabled(true);
49 | }
50 |
51 | NavigationView navigationView = findViewById(R.id.navigation_view);
52 | setupNavigationView(navigationView);
53 |
54 | mDrawerLayout = findViewById(R.id.drawer_layout);
55 | mDrawerToggle = new ActionBarDrawerToggle(
56 | this, mDrawerLayout,
57 | R.string.drawer_open, R.string.drawer_close);
58 | mDrawerLayout.setDrawerListener(mDrawerToggle);
59 |
60 |
61 | if (savedInstanceState == null) {
62 | setFragment(new LibraryFragment());
63 | mCurrentNavItem = R.id.drawer_menu_library;
64 | navigationView.getMenu().findItem(mCurrentNavItem).setChecked(true);
65 | } else {
66 | onBackStackChanged(); // force-call method to ensure indicator is shown properly
67 | mCurrentNavItem = savedInstanceState.getInt(STATE_CURRENT_MENU_ITEM);
68 | navigationView.getMenu().findItem(mCurrentNavItem).setChecked(true);
69 | }
70 | }
71 |
72 | @Override
73 | protected void onPostCreate(Bundle savedInstanceState) {
74 | super.onPostCreate(savedInstanceState);
75 | mDrawerToggle.syncState();
76 | }
77 |
78 | @Override
79 | protected void onSaveInstanceState(Bundle outState) {
80 | outState.putInt(STATE_CURRENT_MENU_ITEM, mCurrentNavItem);
81 | super.onSaveInstanceState(outState);
82 | }
83 |
84 | @Override
85 | public void onConfigurationChanged(Configuration newConfig) {
86 | super.onConfigurationChanged(newConfig);
87 | mDrawerToggle.onConfigurationChanged(newConfig);
88 | }
89 |
90 | private void setFragment(Fragment fragment) {
91 | FragmentManager fragmentManager = getSupportFragmentManager();
92 | if (fragmentManager.getBackStackEntryCount() >= 1) {
93 | fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
94 | }
95 |
96 | fragmentManager
97 | .beginTransaction()
98 | .replace(R.id.content_frame, fragment)
99 | .commit();
100 | }
101 |
102 | public void pushFragment(Fragment fragment) {
103 | getSupportFragmentManager()
104 | .beginTransaction()
105 | .replace(R.id.content_frame, fragment)
106 | .addToBackStack(null)
107 | .commit();
108 | }
109 |
110 | private boolean popFragment() {
111 | FragmentManager fragmentManager = getSupportFragmentManager();
112 | if (fragmentManager.getBackStackEntryCount() > 0) {
113 | fragmentManager.popBackStack();
114 | return true;
115 | }
116 | return false;
117 | }
118 |
119 | private void setupNavigationView(NavigationView view) {
120 | view.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
121 | @Override
122 | public boolean onNavigationItemSelected(MenuItem menuItem) {
123 | if (mCurrentNavItem == menuItem.getItemId()) {
124 | mDrawerLayout.closeDrawers();
125 | return true;
126 | }
127 |
128 | switch (menuItem.getItemId()) {
129 | case R.id.drawer_menu_library:
130 | setFragment(new LibraryFragment());
131 | break;
132 | case R.id.drawer_menu_settings:
133 | setTitle(R.string.menu_settings);
134 | setFragment(new SettingsFragment());
135 | break;
136 | case R.id.drawer_menu_about:
137 | setTitle(R.string.menu_about);
138 | setFragment(new AboutFragment());
139 | break;
140 | }
141 |
142 | mCurrentNavItem = menuItem.getItemId();
143 | menuItem.setChecked(true);
144 | mDrawerLayout.closeDrawers();
145 | return true;
146 | }
147 | });
148 | }
149 |
150 | @Override
151 | public void onBackStackChanged() {
152 | mDrawerToggle.setDrawerIndicatorEnabled(getSupportFragmentManager().getBackStackEntryCount() == 0);
153 | }
154 |
155 | @Override
156 | public void onBackPressed() {
157 | if (!popFragment()) {
158 | finish();
159 | }
160 | }
161 |
162 | @Override
163 | public boolean onSupportNavigateUp() {
164 | if (!popFragment()) {
165 | if (mDrawerLayout.isDrawerOpen(GravityCompat.START))
166 | mDrawerLayout.closeDrawers();
167 | else
168 | mDrawerLayout.openDrawer(GravityCompat.START);
169 | }
170 | return super.onSupportNavigateUp();
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/fragment/LibraryFragment.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.fragment;
2 |
3 | import android.content.Context;
4 | import android.graphics.Rect;
5 | import android.os.Bundle;
6 | import android.util.Log;
7 | import android.view.LayoutInflater;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 |
11 | import androidx.fragment.app.Fragment;
12 | import androidx.recyclerview.widget.GridLayoutManager;
13 | import androidx.recyclerview.widget.RecyclerView;
14 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
15 |
16 | import java.util.ArrayList;
17 | import java.util.List;
18 |
19 | import be.nosuid.bubble.MainApplication;
20 | import be.nosuid.bubble.R;
21 | import be.nosuid.bubble.activity.MainActivity;
22 | import be.nosuid.bubble.managers.SharedResourcesManager;
23 | import be.nosuid.bubble.managers.Utils;
24 | import be.nosuid.bubble.model.Series;
25 | import be.nosuid.bubble.view.CoverViewHolder;
26 | import io.reactivex.disposables.CompositeDisposable;
27 | import io.reactivex.disposables.Disposable;
28 |
29 |
30 | public class LibraryFragment extends Fragment {
31 |
32 | private SwipeRefreshLayout mRefreshLayout;
33 | private View mEmptyView;
34 | private RecyclerView mSeriesGridView;
35 |
36 | private SharedResourcesManager mSharedRes;
37 |
38 | private CompositeDisposable mCompositeDisposable;
39 | private List mSeries;
40 |
41 | @Override
42 | public void onCreate(Bundle savedInstanceState) {
43 | super.onCreate(savedInstanceState);
44 |
45 | mSharedRes = ((MainApplication) getActivity().getApplication()).getSharedResourcesManager();
46 |
47 | mCompositeDisposable = new CompositeDisposable();
48 | mSeries = new ArrayList<>();
49 |
50 | setHasOptionsMenu(true);
51 | }
52 |
53 | @Override
54 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
55 | final ViewGroup view = (ViewGroup) inflater.inflate(R.layout.fragment_library, container, false);
56 |
57 | final int numColumns = mSharedRes.getSettingsManager()
58 | .getLibraryColumnsCount(Utils.getScreenWidth(getActivity()));
59 | final int spacing = (int) getResources().getDimension(R.dimen.cover_grid_margin);
60 |
61 | mRefreshLayout = view.findViewById(R.id.fragmentLibraryLayout);
62 | mRefreshLayout.setColorSchemeColors(getResources().getColor(R.color.primary));
63 | mRefreshLayout.setOnRefreshListener(() -> {
64 | refreshSeries();
65 | });
66 | mRefreshLayout.setEnabled(true);
67 |
68 | GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), numColumns);
69 | layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
70 | @Override
71 | public int getSpanSize(int position) {
72 | return 1;
73 | }
74 | });
75 |
76 | mSeriesGridView = view.findViewById(R.id.seriesGridView);
77 | mSeriesGridView.setHasFixedSize(true);
78 | mSeriesGridView.setLayoutManager(layoutManager);
79 | mSeriesGridView.setAdapter(new SeriesGridAdapter());
80 | mSeriesGridView.addItemDecoration(new SeriesGridSpacingItemDecoration(numColumns, spacing));
81 |
82 | mEmptyView = view.findViewById(R.id.fragmentLibraryEmptyLayout);
83 |
84 | getActivity().setTitle(R.string.menu_library);
85 |
86 | showEmptyMessage(false);
87 |
88 | return view;
89 | }
90 |
91 | @Override
92 | public void onDestroy() {
93 | mSharedRes.getPicasso().cancelTag(getActivity());
94 | if (!mCompositeDisposable.isDisposed()) {
95 | mCompositeDisposable.dispose();
96 | }
97 | super.onDestroy();
98 | }
99 |
100 |
101 | @Override
102 | public void onResume() {
103 | super.onResume();
104 | refreshSeries();
105 | }
106 |
107 |
108 | private void refreshSeries() {
109 | mSharedRes.getLibraryManager()
110 | .getSeries()
111 | .subscribe(new Utils.DefaultSingleObserver>("refreshSeries", mCompositeDisposable) {
112 | @Override
113 | public void onSuccess(List series) {
114 | Log.d(mTag, "onSuccess");
115 | mSeries.clear();
116 | mSeries.addAll(series);
117 | mSeriesGridView.getAdapter().notifyDataSetChanged();
118 | setLoading(false);
119 | }
120 |
121 | @Override
122 | public void onSubscribe(Disposable d) {
123 | super.onSubscribe(d);
124 | setLoading(true);
125 | }
126 |
127 | @Override
128 | public void onError(Throwable e) {
129 | super.onError(e);
130 | setLoading(false);
131 | }
132 | });
133 | }
134 |
135 | @Override
136 | public void onSaveInstanceState(Bundle outState) {
137 | super.onSaveInstanceState(outState);
138 | }
139 |
140 | private void setLoading(boolean isLoading) {
141 | mRefreshLayout.setRefreshing(isLoading);
142 | showEmptyMessage(!isLoading && mSeries.size() == 0);
143 | }
144 |
145 | private void openSeries(Series series) {
146 | SeriesFragment fragment = SeriesFragment.create(series);
147 | ((MainActivity) getActivity()).pushFragment(fragment);
148 | }
149 |
150 | private void showEmptyMessage(boolean show) {
151 | mEmptyView.setVisibility(show ? View.VISIBLE : View.GONE);
152 | mRefreshLayout.setEnabled(!show);
153 | }
154 |
155 | private Series getSeriesAtPosition(int position) {
156 | return mSeries.get(position);
157 | }
158 |
159 | private final class SeriesGridSpacingItemDecoration extends RecyclerView.ItemDecoration {
160 | private int mSpanCount;
161 | private int mSpacing;
162 |
163 | public SeriesGridSpacingItemDecoration(int spanCount, int spacing) {
164 | mSpanCount = spanCount;
165 | mSpacing = spacing;
166 | }
167 |
168 | @Override
169 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
170 | int position = parent.getChildAdapterPosition(view);
171 | int column = position % mSpanCount;
172 |
173 | outRect.left = mSpacing - column * mSpacing / mSpanCount;
174 | outRect.right = (column + 1) * mSpacing / mSpanCount;
175 |
176 | if (position < mSpanCount) {
177 | outRect.top = mSpacing;
178 | }
179 | outRect.bottom = mSpacing;
180 | }
181 | }
182 |
183 | private final class SeriesGridAdapter extends RecyclerView.Adapter {
184 | @Override
185 | public int getItemCount() {
186 | return mSeries.size();
187 | }
188 |
189 | @Override
190 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
191 | Context ctx = viewGroup.getContext();
192 | View view = LayoutInflater.from(ctx)
193 | .inflate(R.layout.card_cover, viewGroup, false);
194 | return new SeriesViewHolder(view);
195 | }
196 |
197 | @Override
198 | public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
199 | Series series = getSeriesAtPosition(i);
200 | SeriesViewHolder holder = (SeriesViewHolder) viewHolder;
201 | holder.setupSeries(series);
202 | }
203 | }
204 |
205 | private class SeriesViewHolder extends CoverViewHolder {
206 | public SeriesViewHolder(View view) {
207 | super(view);
208 | }
209 |
210 | private void setupSeries(Series series) {
211 | setupText(series.getName(), series.getBooksLastRead(), series.getBooksCount());
212 | setupCover(mSharedRes.getPicasso(),
213 | mSharedRes.getLibraryManager().getSerieThumbnailUri(series.getId()),
214 | getActivity());
215 | }
216 |
217 | @Override
218 | public void onClick(View v) {
219 | Series series = getSeriesAtPosition(getAdapterPosition());
220 | openSeries(series);
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/managers/LibraryManager.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.managers;
2 |
3 | import android.net.Uri;
4 | import android.util.Log;
5 |
6 | import java.io.IOException;
7 | import java.io.InterruptedIOException;
8 | import java.net.SocketException;
9 | import java.util.ArrayList;
10 | import java.util.Arrays;
11 | import java.util.List;
12 |
13 | import be.nosuid.bubble.komga.Books;
14 | import be.nosuid.bubble.komga.KomgaApi;
15 | import be.nosuid.bubble.komga.ReadProgress;
16 | import be.nosuid.bubble.model.Comic;
17 | import be.nosuid.bubble.model.Series;
18 | import io.reactivex.Completable;
19 | import io.reactivex.Maybe;
20 | import io.reactivex.Observable;
21 | import io.reactivex.Single;
22 | import io.reactivex.SingleSource;
23 | import io.reactivex.android.schedulers.AndroidSchedulers;
24 | import io.reactivex.exceptions.UndeliverableException;
25 | import io.reactivex.plugins.RxJavaPlugins;
26 | import io.reactivex.schedulers.Schedulers;
27 |
28 | public class LibraryManager {
29 | private SharedResourcesManager mSharedRes;
30 |
31 | public LibraryManager(SharedResourcesManager srm) {
32 | mSharedRes = srm;
33 |
34 | // https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
35 | // TODO: Got some InterruptedIOException ...
36 | RxJavaPlugins.setErrorHandler(e -> {
37 | if (e instanceof UndeliverableException) {
38 | e = e.getCause();
39 | }
40 | if ((e instanceof IOException) || (e instanceof SocketException)) {
41 | // fine, irrelevant network problem or API that throws on cancellation
42 | return;
43 | }
44 | if ((e instanceof InterruptedException) || (e instanceof InterruptedIOException)) {
45 | // fine, some blocking code was interrupted by a dispose call
46 | return;
47 | }
48 | if ((e instanceof NullPointerException) || (e instanceof IllegalArgumentException)) {
49 | // that's likely a bug in the application
50 | Thread.currentThread()
51 | .getUncaughtExceptionHandler()
52 | .uncaughtException(Thread.currentThread(), e);
53 | return;
54 | }
55 | if (e instanceof IllegalStateException) {
56 | // that's a bug in RxJava or in a custom operator
57 | Thread.currentThread()
58 | .getUncaughtExceptionHandler()
59 | .uncaughtException(Thread.currentThread(), e);
60 | return;
61 | }
62 | Log.w("LibraryManager", "Undeliverable exception received, not sure what to do", e);
63 | });
64 | }
65 |
66 | private int mPageSize = 20;
67 |
68 | public enum ComicsStatusFilter {
69 | ALL,
70 | READ, // Where pageLastRead == pagesCount
71 | UNREAD, // Where pageLastRead == 0
72 | UNFINISHED, // Where pageLastRead != pagesCount
73 | READING // Where UNREAD || UNFINISHED
74 | }
75 |
76 | private Series toSeriesMapper(be.nosuid.bubble.komga.Series.Content series) {
77 | // TODO: Last read book ~= number of books read
78 | return new Series(series.getId(), series.getName(),
79 | series.getBooksReadCount(), series.getBooksCount());
80 | }
81 |
82 | private List toSeriesListMapper(be.nosuid.bubble.komga.Series series) {
83 | List al = new ArrayList<>();
84 | for (be.nosuid.bubble.komga.Series.Content c : series.getContent()) {
85 | al.add(toSeriesMapper(c));
86 | }
87 | return al;
88 | }
89 |
90 | private List toSeriesListMapper(List seriesList) {
91 | List al = new ArrayList<>();
92 | for (be.nosuid.bubble.komga.Series s : seriesList) {
93 | al.addAll(toSeriesListMapper(s));
94 | }
95 | return al;
96 | }
97 |
98 | private Comic toComicMapper(Books.Content book) {
99 | return new Comic(book.getId(), book.getName(), book.getSeriesId(),
100 | book.getNumber(), book.getReadProgress().getPage(), book.getMedia().getPagesCount());
101 | }
102 |
103 | private List toComicListMapper(Books books) {
104 | List os = new ArrayList<>();
105 | for (Books.Content c : books.getContent()) {
106 | os.add(toComicMapper(c));
107 | }
108 | return os;
109 | }
110 |
111 | private List toComicListMapper(List booksList) {
112 | List al = new ArrayList<>();
113 | for (Books b : booksList) {
114 | al.addAll(toComicListMapper(b));
115 | }
116 | return al;
117 | }
118 |
119 |
120 | private static class RxJava2Sched {
121 | // http://reactivex.io/documentation/scheduler.html
122 | // Calling these will :
123 | // - set the initial running thread to Schedulers.io()
124 | // - change the running thread to AndroidSchedulers.mainThread() (the UI thread)
125 | // It should be call as late as possible to process everything in the
126 | // Schedulers.io() thread and not freeze the UI running in AndroidSchedulers.mainThread().
127 |
128 | private static SingleSource applySchedulers(Single upstream) {
129 | return upstream
130 | .subscribeOn(Schedulers.io())
131 | .observeOn(AndroidSchedulers.mainThread());
132 | }
133 |
134 | private static Maybe applySchedulers(Maybe upstream) {
135 | return upstream
136 | .subscribeOn(Schedulers.io())
137 | .observeOn(AndroidSchedulers.mainThread());
138 | }
139 |
140 | private static Completable applySchedulers(Completable upstream) {
141 | return upstream
142 | .subscribeOn(Schedulers.io())
143 | .observeOn(AndroidSchedulers.mainThread());
144 | }
145 | }
146 |
147 | private Single> getSeriesUnpaged(KomgaApi.SortingMethod sortingMethod) {
148 | String libraryId = mSharedRes.getSettingsManager().getKomgaLibraryId();
149 |
150 | // TODO: handle paging in Adapters
151 | Observable stream = Observable.create(subscriber -> {
152 | int pageNum = 0;
153 | be.nosuid.bubble.komga.Series page;
154 |
155 | do {
156 | page = mSharedRes.getKomgaApi()
157 | .getSeriesForPage(libraryId, pageNum, mPageSize, sortingMethod)
158 | .blockingGet();
159 | subscriber.onNext(page);
160 | pageNum += 1;
161 | } while (!page.isLast());
162 |
163 | subscriber.onComplete();
164 | });
165 |
166 | return stream
167 | .toList()
168 | .map(this::toSeriesListMapper);
169 | }
170 |
171 | private Single> getComicsUnpaged(String seriesId, KomgaApi.SortingMethod sortingMethod) {
172 | return getComicsUnpaged(seriesId, sortingMethod, null);
173 | }
174 |
175 | private Single> getComicsUnpaged(String seriesId, KomgaApi.SortingMethod sortingMethod, List readStatus) {
176 | // TODO: handle paging in Adapters
177 | Observable stream = Observable.create(subscriber -> {
178 | int pageNum = 0;
179 | Books page;
180 |
181 | do {
182 | if (readStatus != null) {
183 | page = mSharedRes.getKomgaApi()
184 | .getSeriesBooksForPage(seriesId, pageNum, mPageSize, sortingMethod, readStatus)
185 | .blockingGet();
186 | } else {
187 | page = mSharedRes.getKomgaApi()
188 | .getSeriesBooksForPage(seriesId, pageNum, mPageSize, sortingMethod)
189 | .blockingGet();
190 | }
191 |
192 | if (subscriber.isDisposed()) {
193 | return;
194 | }
195 |
196 | subscriber.onNext(page);
197 | pageNum += 1;
198 | } while (!page.isLast());
199 |
200 | subscriber.onComplete();
201 | });
202 |
203 | return stream
204 | .toList()
205 | .map(this::toComicListMapper);
206 | }
207 |
208 | public Single> getSeries() {
209 | return getSeriesUnpaged(KomgaApi.SortingMethod.METADATA_TITLE_ASC)
210 | .compose(RxJava2Sched::applySchedulers);
211 | }
212 |
213 | public Single> getComics(String seriesId) {
214 | return getComicsUnpaged(seriesId, KomgaApi.SortingMethod.METADATA_NUMBER_ASC)
215 | .compose(RxJava2Sched::applySchedulers);
216 | }
217 |
218 | public Single> getComicsWithStatus(String seriesId, ComicsStatusFilter statusFilter) {
219 | switch (statusFilter) {
220 | case READ:
221 | return getComicsUnpaged(seriesId, KomgaApi.SortingMethod.METADATA_NUMBER_ASC,
222 | Arrays.asList(KomgaApi.ReadStatus.READ))
223 | .compose(RxJava2Sched::applySchedulers);
224 | case UNREAD:
225 | return getComicsUnpaged(seriesId, KomgaApi.SortingMethod.METADATA_NUMBER_ASC,
226 | Arrays.asList(KomgaApi.ReadStatus.UNREAD))
227 | .compose(RxJava2Sched::applySchedulers);
228 | case UNFINISHED:
229 | return getComicsUnpaged(seriesId, KomgaApi.SortingMethod.METADATA_NUMBER_ASC,
230 | Arrays.asList(KomgaApi.ReadStatus.IN_PROGRESS))
231 | .compose(RxJava2Sched::applySchedulers);
232 | case READING:
233 | return getComicsUnpaged(seriesId, KomgaApi.SortingMethod.METADATA_NUMBER_ASC,
234 | Arrays.asList(KomgaApi.ReadStatus.UNREAD, KomgaApi.ReadStatus.IN_PROGRESS))
235 | .compose(RxJava2Sched::applySchedulers);
236 | case ALL:
237 | default:
238 | return getComicsUnpaged(seriesId, KomgaApi.SortingMethod.METADATA_NUMBER_ASC)
239 | .compose(RxJava2Sched::applySchedulers);
240 | }
241 | }
242 |
243 | public Single> getSeriesLastReadComics(String seriesId, int historySize) {
244 | return getComicsUnpaged(seriesId, KomgaApi.SortingMethod.METADATA_NUMBER_ASC,
245 | Arrays.asList(KomgaApi.ReadStatus.READ, KomgaApi.ReadStatus.IN_PROGRESS))
246 | .toObservable()
247 | .flatMapIterable(comics -> comics)
248 | .takeLast(historySize)
249 | .toSortedList(new Comic.ChapterReverseOrderComparator())
250 | .compose(RxJava2Sched::applySchedulers);
251 | }
252 |
253 | public Maybe getSerieNextComic(String seriesId, String previousComicId) {
254 | return mSharedRes.getKomgaApi()
255 | .getBookSiblingNext(previousComicId)
256 | //TODO: Error (404 ?) -> Maybe.Empty()
257 | .onErrorComplete()
258 | .map(this::toComicMapper)
259 | .compose(RxJava2Sched::applySchedulers);
260 | }
261 |
262 | public Maybe getSeriePreviousComic(String seriesId, String nextComicId) {
263 | return mSharedRes.getKomgaApi()
264 | .getBookSiblingPrevious(nextComicId)
265 | //TODO: Error (404 ?) -> Maybe.Empty()
266 | .onErrorComplete()
267 | .map(this::toComicMapper)
268 | .compose(RxJava2Sched::applySchedulers);
269 | }
270 |
271 | public Completable setComicLastReadPage(String comidId, int pageNum) {
272 | ReadProgress readProgress = new ReadProgress();
273 | readProgress.setPage(pageNum);
274 |
275 | return mSharedRes.getKomgaApi()
276 | .setBookReadProgress(comidId, readProgress)
277 | .onErrorComplete()
278 | .compose(RxJava2Sched::applySchedulers);
279 | }
280 |
281 |
282 | /*
283 | * Uri Builder for Picasso
284 | */
285 |
286 | public Uri getSerieThumbnailUri(String seriesId) {
287 | String baseUrl = mSharedRes.getSettingsManager().getKomgaApiUrl();
288 | return Uri.parse(String.format("%sseries/%s/thumbnail", baseUrl, seriesId));
289 | }
290 |
291 | public Uri getComicThumbnailUri(String comicId) {
292 | String baseUrl = mSharedRes.getSettingsManager().getKomgaApiUrl();
293 | return Uri.parse(String.format("%sbooks/%s/thumbnail", baseUrl, comicId));
294 | }
295 |
296 | public Uri getComicPageUri(String comicId, int pageNum) {
297 | assert pageNum > 0;
298 |
299 | String baseUrl = mSharedRes.getSettingsManager().getKomgaApiUrl();
300 | return Uri.parse(String.format("%sbooks/%s/pages/%d", baseUrl, comicId, pageNum));
301 | }
302 |
303 | public Maybe ping() {
304 | // Test authenticated access to the API,
305 | return mSharedRes.getKomgaApi()
306 | .getUserMe()
307 | .toMaybe()
308 | .onErrorComplete()
309 | .map(user -> true)
310 | .compose(RxJava2Sched::applySchedulers);
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/view/PageImageView.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.view;
2 |
3 | import android.content.Context;
4 | import android.graphics.Matrix;
5 | import android.graphics.Point;
6 | import android.graphics.RectF;
7 | import android.graphics.drawable.Drawable;
8 | import android.os.Build;
9 | import android.util.AttributeSet;
10 | import android.view.GestureDetector;
11 | import android.view.GestureDetector.SimpleOnGestureListener;
12 | import android.view.MotionEvent;
13 | import android.view.ScaleGestureDetector;
14 | import android.view.View;
15 | import android.view.ViewConfiguration;
16 | import android.view.animation.AccelerateDecelerateInterpolator;
17 | import android.view.animation.Interpolator;
18 | import android.widget.OverScroller;
19 |
20 | import androidx.appcompat.widget.AppCompatImageView;
21 | import androidx.core.view.ViewCompat;
22 |
23 | import be.nosuid.bubble.managers.SettingsManager;
24 |
25 | public class PageImageView extends AppCompatImageView {
26 | private SettingsManager.PageViewMode mViewMode;
27 | private boolean mHaveFrame = false;
28 | private boolean mSkipScaling = false;
29 | private boolean mTranslateRightEdge = false;
30 | private OnTouchListener mOuterTouchListener;
31 | private ScaleGestureDetector mScaleGestureDetector;
32 | private GestureDetector mDragGestureDetector;
33 | private OverScroller mScroller;
34 | private float mMinScale, mMaxScale;
35 | private float mOriginalScale;
36 | private float[] m = new float[9];
37 | private Matrix mMatrix;
38 |
39 | public PageImageView(Context context) {
40 | super(context);
41 | init();
42 | }
43 |
44 | public PageImageView(Context context, AttributeSet attributeSet) {
45 | super(context, attributeSet);
46 | init();
47 | }
48 |
49 | public void setViewMode(SettingsManager.PageViewMode viewMode, boolean isLeftToRight) {
50 | mViewMode = viewMode;
51 | mTranslateRightEdge = !isLeftToRight
52 | && (mViewMode == SettingsManager.PageViewMode.ASPECT_FILL);
53 | mSkipScaling = false;
54 | requestLayout();
55 | invalidate();
56 |
57 | }
58 |
59 | @Override
60 | protected boolean setFrame(int l, int t, int r, int b) {
61 | boolean changed = super.setFrame(l, t, r, b);
62 | mHaveFrame = true;
63 | scale();
64 | return changed;
65 | }
66 |
67 | @Override
68 | public void setImageDrawable(Drawable drawable) {
69 | super.setImageDrawable(drawable);
70 | mSkipScaling = false;
71 | scale();
72 | }
73 |
74 | private void init() {
75 | mMatrix = new Matrix();
76 | setScaleType(ScaleType.MATRIX);
77 | setImageMatrix(mMatrix);
78 |
79 | mScaleGestureDetector = new ScaleGestureDetector(getContext(), new PrivateScaleDetector());
80 | mDragGestureDetector = new GestureDetector(getContext(), new PrivateDragListener());
81 | super.setOnTouchListener(new OnTouchListener() {
82 | @Override
83 | public boolean onTouch(View v, MotionEvent event) {
84 | mScaleGestureDetector.onTouchEvent(event);
85 | mDragGestureDetector.onTouchEvent(event);
86 | if (mOuterTouchListener != null) mOuterTouchListener.onTouch(v, event);
87 | return true;
88 | }
89 | });
90 |
91 | mScroller = new OverScroller(getContext());
92 | mScroller.setFriction(ViewConfiguration.getScrollFriction() * 2);
93 | mViewMode = SettingsManager.PageViewMode.ASPECT_FIT;
94 | }
95 |
96 | @Override
97 | public void setOnTouchListener(OnTouchListener l) {
98 | mOuterTouchListener = l;
99 | }
100 |
101 | private void scale() {
102 | Drawable drawable = getDrawable();
103 | if (drawable == null || !mHaveFrame || mSkipScaling) return;
104 |
105 | int dwidth = drawable.getIntrinsicWidth();
106 | int dheight = drawable.getIntrinsicHeight();
107 |
108 | int vwidth = getWidth();
109 | int vheight = getHeight();
110 |
111 | if (mViewMode == SettingsManager.PageViewMode.ASPECT_FILL) {
112 | float scale;
113 | float dx = 0;
114 |
115 | if (dwidth * vheight > vwidth * dheight) {
116 | scale = (float) vheight / (float) dheight;
117 | if (mTranslateRightEdge)
118 | dx = vwidth - dwidth * scale;
119 | } else {
120 | scale = (float) vwidth / (float) dwidth;
121 | }
122 |
123 | mMatrix.setScale(scale, scale);
124 | mMatrix.postTranslate((int) (dx + 0.5f), 0);
125 | } else if (mViewMode == SettingsManager.PageViewMode.ASPECT_FIT) {
126 | RectF mTempSrc = new RectF(0, 0, dwidth, dheight);
127 | RectF mTempDst = new RectF(0, 0, vwidth, vheight);
128 |
129 | mMatrix.setRectToRect(mTempSrc, mTempDst, Matrix.ScaleToFit.CENTER);
130 | } else if (mViewMode == SettingsManager.PageViewMode.FIT_WIDTH) {
131 | float widthScale = (float) getWidth() / drawable.getIntrinsicWidth();
132 | mMatrix.setScale(widthScale, widthScale);
133 | mMatrix.postTranslate(0, 0);
134 | }
135 |
136 | // calculate min/max scale
137 | float heightRatio = (float) vheight / dheight;
138 | float w = dwidth * heightRatio;
139 | if (w < vwidth) {
140 | mMinScale = vheight * 0.75f / dheight;
141 | mMaxScale = Math.max(dwidth, vwidth) * 1.5f / dwidth;
142 | } else {
143 | mMinScale = vwidth * 0.75f / dwidth;
144 | mMaxScale = Math.max(dheight, vheight) * 1.5f / dheight;
145 | }
146 | setImageMatrix(mMatrix);
147 | mOriginalScale = getCurrentScale();
148 | mSkipScaling = true;
149 | }
150 |
151 | private class PrivateScaleDetector extends ScaleGestureDetector.SimpleOnScaleGestureListener {
152 | @Override
153 | public boolean onScale(ScaleGestureDetector detector) {
154 | mMatrix.getValues(m);
155 |
156 | float scale = m[Matrix.MSCALE_X];
157 | float scaleFactor = detector.getScaleFactor();
158 | float scaleNew = scale * scaleFactor;
159 | boolean scalable = true;
160 |
161 | if (scaleFactor > 1 && mMaxScale - scaleNew < 0) {
162 | scaleFactor = mMaxScale / scale;
163 | scalable = false;
164 | } else if (scaleFactor < 1 && mMinScale - scaleNew > 0) {
165 | scaleFactor = mMinScale / scale;
166 | scalable = false;
167 | }
168 |
169 | mMatrix.postScale(
170 | scaleFactor, scaleFactor,
171 | detector.getFocusX(), detector.getFocusY());
172 | setImageMatrix(mMatrix);
173 |
174 | return scalable;
175 | }
176 | }
177 |
178 | private class PrivateDragListener extends SimpleOnGestureListener {
179 | @Override
180 | public boolean onDown(MotionEvent e) {
181 | mScroller.forceFinished(true);
182 | return true;
183 | }
184 |
185 | @Override
186 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
187 | mMatrix.postTranslate(-distanceX, -distanceY);
188 | setImageMatrix(mMatrix);
189 | return true;
190 | }
191 |
192 | @Override
193 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
194 | Point imageSize = computeCurrentImageSize();
195 | Point offset = computeCurrentOffset();
196 |
197 | int minX = -imageSize.x - PageImageView.this.getWidth();
198 | int minY = -imageSize.y - PageImageView.this.getHeight();
199 | int maxX = 0;
200 | int maxY = 0;
201 |
202 | if (offset.x > 0) {
203 | minX = offset.x;
204 | maxX = offset.x;
205 | }
206 | if (offset.y > 0) {
207 | minY = offset.y;
208 | maxY = offset.y;
209 | }
210 |
211 | mScroller.fling(
212 | offset.x, offset.y,
213 | (int) velocityX, (int) velocityY,
214 | minX, maxX, minY, maxY);
215 | ViewCompat.postInvalidateOnAnimation(PageImageView.this);
216 | return true;
217 | }
218 |
219 | @Override
220 | public boolean onDoubleTapEvent(MotionEvent e) {
221 | if (e.getAction() == MotionEvent.ACTION_UP) {
222 | float scale = (mOriginalScale == getCurrentScale()) ? mMaxScale : mOriginalScale;
223 | zoomAnimated(e, scale);
224 | }
225 | return true;
226 | }
227 | }
228 |
229 | private void zoomAnimated(MotionEvent e, float scale) {
230 | post(new ZoomAnimation(e.getX(), e.getY(), scale));
231 | }
232 |
233 | @Override
234 | public void computeScroll() {
235 | if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
236 | int curX = mScroller.getCurrX();
237 | int curY = mScroller.getCurrY();
238 |
239 | mMatrix.getValues(m);
240 | m[Matrix.MTRANS_X] = curX;
241 | m[Matrix.MTRANS_Y] = curY;
242 | mMatrix.setValues(m);
243 | setImageMatrix(mMatrix);
244 | ViewCompat.postInvalidateOnAnimation(this);
245 | }
246 | super.computeScroll();
247 | }
248 |
249 | private float getCurrentScale() {
250 | mMatrix.getValues(m);
251 | return m[Matrix.MSCALE_X];
252 | }
253 |
254 | private Point computeCurrentImageSize() {
255 | final Point size = new Point();
256 | Drawable d = getDrawable();
257 | if (d != null) {
258 | mMatrix.getValues(m);
259 |
260 | float scale = m[Matrix.MSCALE_X];
261 | float width = d.getIntrinsicWidth() * scale;
262 | float height = d.getIntrinsicHeight() * scale;
263 |
264 | size.set((int) width, (int) height);
265 |
266 | return size;
267 | }
268 |
269 | size.set(0, 0);
270 | return size;
271 | }
272 |
273 | private Point computeCurrentOffset() {
274 | final Point offset = new Point();
275 |
276 | mMatrix.getValues(m);
277 | float transX = m[Matrix.MTRANS_X];
278 | float transY = m[Matrix.MTRANS_Y];
279 |
280 | offset.set((int) transX, (int) transY);
281 |
282 | return offset;
283 | }
284 |
285 | @Override
286 | public void setImageMatrix(Matrix matrix) {
287 | super.setImageMatrix(fixMatrix(matrix));
288 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
289 | postInvalidate();
290 | }
291 | }
292 |
293 | private Matrix fixMatrix(Matrix matrix) {
294 | if (getDrawable() == null)
295 | return matrix;
296 |
297 | matrix.getValues(m);
298 |
299 | Point imageSize = computeCurrentImageSize();
300 |
301 | int imageWidth = imageSize.x;
302 | int imageHeight = imageSize.y;
303 | int maxTransX = imageWidth - getWidth();
304 | int maxTransY = imageHeight - getHeight();
305 |
306 | if (imageWidth > getWidth())
307 | m[Matrix.MTRANS_X] = Math.min(0, Math.max(m[Matrix.MTRANS_X], -maxTransX));
308 | else
309 | m[Matrix.MTRANS_X] = getWidth() / 2 - imageWidth / 2;
310 | if (imageHeight > getHeight())
311 | m[Matrix.MTRANS_Y] = Math.min(0, Math.max(m[Matrix.MTRANS_Y], -maxTransY));
312 | else
313 | m[Matrix.MTRANS_Y] = getHeight() / 2 - imageHeight / 2;
314 |
315 | matrix.setValues(m);
316 | return matrix;
317 | }
318 |
319 | @Override
320 | public boolean canScrollHorizontally(int direction) {
321 | if (getDrawable() == null)
322 | return false;
323 |
324 | float imageWidth = computeCurrentImageSize().x;
325 | float offsetX = computeCurrentOffset().x;
326 |
327 | if (offsetX >= 0 && direction < 0) {
328 | return false;
329 | } else if (Math.abs(offsetX) + getWidth() >= imageWidth && direction > 0) {
330 | return false;
331 | }
332 | return true;
333 | }
334 |
335 | private class ZoomAnimation implements Runnable {
336 | public final static int ZOOM_DURATION = 200;
337 | float mX;
338 | float mY;
339 | float mScale;
340 | Interpolator mInterpolator;
341 | float mStartScale;
342 | long mStartTime;
343 |
344 | ZoomAnimation(float x, float y, float scale) {
345 | mMatrix.getValues(m);
346 | mX = x;
347 | mY = y;
348 | mScale = scale;
349 |
350 | mInterpolator = new AccelerateDecelerateInterpolator();
351 | mStartScale = getCurrentScale();
352 | mStartTime = System.currentTimeMillis();
353 | }
354 |
355 | @Override
356 | public void run() {
357 | float t = (float) (System.currentTimeMillis() - mStartTime) / ZOOM_DURATION;
358 | float interpolateRatio = mInterpolator.getInterpolation(t);
359 | t = (t > 1f) ? 1f : t;
360 |
361 | mMatrix.getValues(m);
362 | float newScale = mStartScale + interpolateRatio * (mScale - mStartScale);
363 | float newScaleFactor = newScale / m[Matrix.MSCALE_X];
364 |
365 | mMatrix.postScale(newScaleFactor, newScaleFactor, mX, mY);
366 | setImageMatrix(mMatrix);
367 |
368 | if (t < 1f) {
369 | post(this);
370 | } else {
371 | // set exact scale
372 | mMatrix.getValues(m);
373 | mMatrix.setScale(mScale, mScale);
374 | mMatrix.postTranslate(m[Matrix.MTRANS_X], m[Matrix.MTRANS_Y]);
375 | setImageMatrix(mMatrix);
376 | }
377 | }
378 | }
379 | }
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/fragment/SeriesFragment.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.fragment;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.graphics.Rect;
6 | import android.os.Bundle;
7 | import android.util.Log;
8 | import android.view.LayoutInflater;
9 | import android.view.Menu;
10 | import android.view.MenuInflater;
11 | import android.view.MenuItem;
12 | import android.view.View;
13 | import android.view.ViewGroup;
14 | import android.widget.TextView;
15 |
16 | import androidx.fragment.app.Fragment;
17 | import androidx.recyclerview.widget.GridLayoutManager;
18 | import androidx.recyclerview.widget.RecyclerView;
19 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
20 |
21 | import java.util.ArrayList;
22 | import java.util.List;
23 |
24 | import be.nosuid.bubble.MainApplication;
25 | import be.nosuid.bubble.R;
26 | import be.nosuid.bubble.activity.ReaderActivity;
27 | import be.nosuid.bubble.managers.LibraryManager.ComicsStatusFilter;
28 | import be.nosuid.bubble.managers.SettingsManager;
29 | import be.nosuid.bubble.managers.SharedResourcesManager;
30 | import be.nosuid.bubble.managers.Utils;
31 | import be.nosuid.bubble.model.Comic;
32 | import be.nosuid.bubble.model.Series;
33 | import be.nosuid.bubble.view.CoverViewHolder;
34 | import io.reactivex.disposables.CompositeDisposable;
35 | import io.reactivex.disposables.Disposable;
36 |
37 | public class SeriesFragment extends Fragment {
38 | private static final String PARAM_SERIES = "PARAM_SERIES";
39 |
40 | private final int ITEM_VIEW_TYPE_COMIC = 1;
41 | private final int ITEM_VIEW_TYPE_HEADER_LAST_READ = 2;
42 | private final int ITEM_VIEW_TYPE_HEADER_ALL = 3;
43 |
44 | private final int NUM_HEADERS = 2;
45 |
46 | private SharedResourcesManager mSharedRes;
47 |
48 | private SwipeRefreshLayout mRefreshLayout;
49 |
50 | private Series mSeries;
51 | private List mFilteredComics;
52 | private List mLastReadComics;
53 |
54 | private CompositeDisposable mCompositeDisposable;
55 |
56 | private ComicsStatusFilter mComicsStatusFilter = ComicsStatusFilter.ALL;
57 |
58 | private RecyclerView mComicsGridView;
59 |
60 | public static SeriesFragment create(Series series) {
61 | SeriesFragment fragment = new SeriesFragment();
62 | Bundle args = new Bundle();
63 | args.putSerializable(PARAM_SERIES, series);
64 | fragment.setArguments(args);
65 | return fragment;
66 | }
67 |
68 | @Override
69 | public void onCreate(Bundle savedInstanceState) {
70 | super.onCreate(savedInstanceState);
71 |
72 | mSharedRes = ((MainApplication) getActivity().getApplication()).getSharedResourcesManager();
73 |
74 | mSeries = (Series) getArguments().getSerializable(PARAM_SERIES);
75 | mFilteredComics = new ArrayList<>();
76 | mLastReadComics = new ArrayList<>();
77 |
78 | mCompositeDisposable = new CompositeDisposable();
79 |
80 | setHasOptionsMenu(true);
81 | }
82 |
83 | @Override
84 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
85 | final View view = inflater.inflate(R.layout.fragment_series, container, false);
86 |
87 | final int numColumns = mSharedRes.getSettingsManager()
88 | .getSeriesColumnsCount(Utils.getScreenWidth(getActivity()));
89 | final int spacing = (int) getResources().getDimension(R.dimen.cover_grid_margin);
90 |
91 | mRefreshLayout = view.findViewById(R.id.fragmentSeriesLayout);
92 | mRefreshLayout.setColorSchemeColors(getResources().getColor(R.color.primary));
93 | mRefreshLayout.setOnRefreshListener(() -> {
94 | refreshLastReadComics();
95 | refreshFilteredComics();
96 | });
97 | mRefreshLayout.setEnabled(true);
98 |
99 | GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), numColumns);
100 | layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
101 | @Override
102 | public int getSpanSize(int position) {
103 | if (getItemViewTypeAtPosition(position) == ITEM_VIEW_TYPE_COMIC) {
104 | return 1;
105 | } else {
106 | return numColumns;
107 | }
108 | }
109 | });
110 |
111 | mComicsGridView = view.findViewById(R.id.comicsGridView);
112 | mComicsGridView.setHasFixedSize(true);
113 | mComicsGridView.setLayoutManager(layoutManager);
114 | mComicsGridView.setAdapter(new ComicGridAdapter());
115 | mComicsGridView.addItemDecoration(new ComicsGridSpacingItemDecoration(numColumns, spacing));
116 |
117 | getActivity().setTitle(mSeries.getName());
118 |
119 | return view;
120 | }
121 |
122 | @Override
123 | public void onDestroy() {
124 | mSharedRes.getPicasso().cancelTag(getActivity());
125 | if (!mCompositeDisposable.isDisposed()) {
126 | mCompositeDisposable.dispose();
127 | }
128 | super.onDestroy();
129 | }
130 |
131 | @Override
132 | public void onResume() {
133 | super.onResume();
134 | refreshFilteredComics();
135 | refreshLastReadComics();
136 | }
137 |
138 | private void refreshFilteredComics() {
139 | mSharedRes.getLibraryManager()
140 | .getComicsWithStatus(mSeries.getId(), mComicsStatusFilter)
141 | .subscribe(new Utils.DefaultSingleObserver>("refreshFilteredComics", mCompositeDisposable) {
142 | @Override
143 | public void onSuccess(List comics) {
144 | Log.d(mTag, "onSuccess");
145 | mFilteredComics.clear();
146 | mFilteredComics.addAll(comics);
147 | mComicsGridView.getAdapter().notifyDataSetChanged();
148 | setLoading(false);
149 | }
150 |
151 | @Override
152 | public void onSubscribe(Disposable d) {
153 | super.onSubscribe(d);
154 | setLoading(true);
155 | }
156 |
157 | @Override
158 | public void onError(Throwable e) {
159 | super.onError(e);
160 | setLoading(false);
161 | }
162 | });
163 | }
164 |
165 | private void refreshLastReadComics() {
166 | mSharedRes.getLibraryManager()
167 | .getSeriesLastReadComics(mSeries.getId(), SettingsManager.MAX_LAST_READ_COUNT)
168 | .subscribe(new Utils.DefaultSingleObserver>("refreshLastReadComics", mCompositeDisposable) {
169 | @Override
170 | public void onSuccess(List comics) {
171 | Log.d(mTag, "onSuccess");
172 | mLastReadComics.clear();
173 | mLastReadComics.addAll(comics);
174 | mComicsGridView.getAdapter().notifyDataSetChanged();
175 | setLoading(false);
176 | }
177 |
178 | @Override
179 | public void onSubscribe(Disposable d) {
180 | super.onSubscribe(d);
181 | setLoading(true);
182 | }
183 |
184 | @Override
185 | public void onError(Throwable e) {
186 | super.onError(e);
187 | setLoading(false);
188 | }
189 | });
190 | }
191 |
192 | @Override
193 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
194 | menu.clear();
195 | inflater.inflate(R.menu.browser, menu);
196 | super.onCreateOptionsMenu(menu, inflater);
197 | }
198 |
199 | @Override
200 | public boolean onOptionsItemSelected(MenuItem item) {
201 | switch (item.getItemId()) {
202 | case R.id.menu_browser_filter_all:
203 | return changeComicsStatusFilter(item, ComicsStatusFilter.ALL);
204 | case R.id.menu_browser_filter_read:
205 | return changeComicsStatusFilter(item, ComicsStatusFilter.READ);
206 | case R.id.menu_browser_filter_unread:
207 | return changeComicsStatusFilter(item, ComicsStatusFilter.UNREAD);
208 | case R.id.menu_browser_filter_unfinished:
209 | return changeComicsStatusFilter(item, ComicsStatusFilter.UNFINISHED);
210 | case R.id.menu_browser_filter_reading:
211 | return changeComicsStatusFilter(item, ComicsStatusFilter.READING);
212 | }
213 |
214 | return super.onOptionsItemSelected(item);
215 | }
216 |
217 | private boolean changeComicsStatusFilter(MenuItem item, ComicsStatusFilter filter) {
218 | mComicsStatusFilter = filter;
219 | item.setChecked(true);
220 | refreshFilteredComics();
221 | return true;
222 | }
223 |
224 | private void setLoading(boolean isLoading) {
225 | mRefreshLayout.setRefreshing(isLoading);
226 | }
227 |
228 | private void openComic(Comic comic) {
229 | Intent intent = new Intent(getActivity(), ReaderActivity.class);
230 | intent.putExtra(ReaderFragment.PARAM_COMIC, comic);
231 | startActivity(intent);
232 | }
233 |
234 | private Comic getComicAtPosition(int position) {
235 | if (hasLastRead()) {
236 | // mLastReadComics are displayed on top of all mFilteredComics
237 | // First position is the "Last Read" header
238 | // Position "mLastReadComics.size() + 1" is the "All" header
239 | if (position > 0 && position < mLastReadComics.size() + 1)
240 | return mLastReadComics.get(position - 1);
241 | else
242 | return mFilteredComics.get(position - mLastReadComics.size() - NUM_HEADERS);
243 | } else {
244 | return mFilteredComics.get(position);
245 | }
246 | }
247 |
248 | private int getItemViewTypeAtPosition(int position) {
249 | if (hasLastRead()) {
250 | if (position == 0)
251 | return ITEM_VIEW_TYPE_HEADER_LAST_READ;
252 | else if (position == mLastReadComics.size() + 1)
253 | return ITEM_VIEW_TYPE_HEADER_ALL;
254 | }
255 | return ITEM_VIEW_TYPE_COMIC;
256 | }
257 |
258 | private boolean hasLastRead() {
259 | return mComicsStatusFilter == ComicsStatusFilter.ALL
260 | && mLastReadComics.size() > 0;
261 | }
262 |
263 | private final class ComicsGridSpacingItemDecoration extends RecyclerView.ItemDecoration {
264 | private int mSpanCount;
265 | private int mSpacing;
266 |
267 | public ComicsGridSpacingItemDecoration(int spanCount, int spacing) {
268 | mSpanCount = spanCount;
269 | mSpacing = spacing;
270 | }
271 |
272 | @Override
273 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
274 | int position = parent.getChildAdapterPosition(view);
275 |
276 | if (hasLastRead()) {
277 | // those are headers
278 | if (position == 0 || position == mLastReadComics.size() + 1)
279 | return;
280 |
281 | if (position > 0 && position < mLastReadComics.size() + 1) {
282 | position -= 1;
283 | } else {
284 | position -= (NUM_HEADERS + mLastReadComics.size());
285 | }
286 | }
287 |
288 | int column = position % mSpanCount;
289 |
290 | outRect.left = mSpacing - column * mSpacing / mSpanCount;
291 | outRect.right = (column + 1) * mSpacing / mSpanCount;
292 |
293 | if (position < mSpanCount) {
294 | outRect.top = mSpacing;
295 | }
296 | outRect.bottom = mSpacing;
297 | }
298 | }
299 |
300 | private final class ComicGridAdapter extends RecyclerView.Adapter {
301 | @Override
302 | public int getItemCount() {
303 | if (hasLastRead()) {
304 | return mFilteredComics.size() + mLastReadComics.size() + NUM_HEADERS;
305 | }
306 | return mFilteredComics.size();
307 | }
308 |
309 | @Override
310 | public int getItemViewType(int position) {
311 | return getItemViewTypeAtPosition(position);
312 | }
313 |
314 | @Override
315 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
316 | Context ctx = viewGroup.getContext();
317 |
318 | if (i == ITEM_VIEW_TYPE_HEADER_LAST_READ) {
319 | TextView view = (TextView) LayoutInflater.from(ctx)
320 | .inflate(R.layout.fragment_series_header, viewGroup, false);
321 | view.setText(R.string.library_header_recent);
322 |
323 | int spacing = (int) getResources().getDimension(R.dimen.cover_grid_margin);
324 | RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
325 | lp.setMargins(0, spacing, 0, 0);
326 |
327 | return new HeaderViewHolder(view);
328 | } else if (i == ITEM_VIEW_TYPE_HEADER_ALL) {
329 | TextView view = (TextView) LayoutInflater.from(ctx)
330 | .inflate(R.layout.fragment_series_header, viewGroup, false);
331 | view.setText(R.string.library_header_all);
332 |
333 | return new HeaderViewHolder(view);
334 | }
335 |
336 | View view = LayoutInflater.from(ctx)
337 | .inflate(R.layout.card_cover, viewGroup, false);
338 | return new ComicViewHolder(view);
339 | }
340 |
341 | @Override
342 | public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
343 | if (viewHolder.getItemViewType() == ITEM_VIEW_TYPE_COMIC) {
344 | Comic comic = getComicAtPosition(i);
345 | ComicViewHolder holder = (ComicViewHolder) viewHolder;
346 | holder.setupComic(comic);
347 | }
348 | }
349 | }
350 |
351 | private class HeaderViewHolder extends RecyclerView.ViewHolder {
352 | public HeaderViewHolder(TextView textView) {
353 | super(textView);
354 | }
355 | }
356 |
357 | private class ComicViewHolder extends CoverViewHolder {
358 | public ComicViewHolder(View view) {
359 | super(view);
360 | }
361 |
362 | private void setupComic(Comic comic) {
363 | setupText(comic.getName(), comic.getPageLastRead(), comic.getPagesCount());
364 | setupCover(mSharedRes.getPicasso(),
365 | mSharedRes.getLibraryManager().getComicThumbnailUri(comic.getId()),
366 | getActivity());
367 | }
368 |
369 | @Override
370 | public void onClick(View v) {
371 | Comic comic = getComicAtPosition(getAdapterPosition());
372 | openComic(comic);
373 | }
374 | }
375 | }
376 |
--------------------------------------------------------------------------------
/app/src/main/java/be/nosuid/bubble/fragment/ReaderFragment.java:
--------------------------------------------------------------------------------
1 | package be.nosuid.bubble.fragment;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.drawable.BitmapDrawable;
6 | import android.graphics.drawable.Drawable;
7 | import android.os.Bundle;
8 | import android.os.Handler;
9 | import android.util.Log;
10 | import android.util.SparseArray;
11 | import android.view.GestureDetector;
12 | import android.view.LayoutInflater;
13 | import android.view.Menu;
14 | import android.view.MenuInflater;
15 | import android.view.MenuItem;
16 | import android.view.MotionEvent;
17 | import android.view.View;
18 | import android.view.ViewGroup;
19 | import android.view.Window;
20 | import android.view.WindowManager;
21 | import android.widget.ImageButton;
22 | import android.widget.ImageView;
23 | import android.widget.LinearLayout;
24 | import android.widget.SeekBar;
25 | import android.widget.TextView;
26 | import android.widget.Toast;
27 |
28 | import androidx.appcompat.app.ActionBar;
29 | import androidx.appcompat.app.AlertDialog;
30 | import androidx.appcompat.app.AppCompatActivity;
31 | import androidx.fragment.app.Fragment;
32 | import androidx.viewpager.widget.PagerAdapter;
33 | import androidx.viewpager.widget.ViewPager;
34 |
35 | import com.squareup.picasso.MemoryPolicy;
36 | import com.squareup.picasso.Picasso;
37 | import com.squareup.picasso.Target;
38 |
39 | import be.nosuid.bubble.MainApplication;
40 | import be.nosuid.bubble.R;
41 | import be.nosuid.bubble.activity.ReaderActivity;
42 | import be.nosuid.bubble.managers.SettingsManager;
43 | import be.nosuid.bubble.managers.SharedResourcesManager;
44 | import be.nosuid.bubble.managers.Utils;
45 | import be.nosuid.bubble.model.Comic;
46 | import be.nosuid.bubble.view.ComicSeekBar;
47 | import be.nosuid.bubble.view.PageImageView;
48 | import be.nosuid.bubble.view.SwipeOutViewPager;
49 | import io.reactivex.disposables.CompositeDisposable;
50 |
51 |
52 | public class ReaderFragment extends Fragment {
53 | public static final String PARAM_COMIC = "PARAM_COMIC";
54 | private static final String STATE_FULLSCREEN = "STATE_FULLSCREEN";
55 |
56 | private SharedResourcesManager mSharedRes;
57 |
58 | private SwipeOutViewPager mViewPager;
59 | private ComicSeekBar mSeekBar;
60 | private ComicPagerAdapter mPagerAdapter;
61 |
62 | private LinearLayout mPageNavLayout;
63 | private TextView mPageNavTextView;
64 |
65 | private GestureDetector mGestureDetector;
66 |
67 | private Comic mComic;
68 | private int mCurrentPageNum;
69 |
70 | private boolean mIsFullscreen;
71 |
72 | private CompositeDisposable mCompositeDisposable;
73 |
74 | public static ReaderFragment create(Comic comic) {
75 | ReaderFragment fragment = new ReaderFragment();
76 | Bundle args = new Bundle();
77 | args.putSerializable(PARAM_COMIC, comic);
78 | fragment.setArguments(args);
79 | return fragment;
80 | }
81 |
82 | @Override
83 | public void onCreate(Bundle savedInstanceState) {
84 | super.onCreate(savedInstanceState);
85 |
86 | mSharedRes = ((MainApplication) getActivity().getApplication()).getSharedResourcesManager();
87 |
88 | Bundle bundle = getArguments();
89 | mComic = (Comic) bundle.getSerializable(PARAM_COMIC);
90 |
91 | mCompositeDisposable = new CompositeDisposable();
92 |
93 | mPagerAdapter = new ComicPagerAdapter(mComic.getPagesCount());
94 | mGestureDetector = new GestureDetector(getActivity(), new FingerTapListener());
95 |
96 | setHasOptionsMenu(true);
97 | }
98 |
99 | @Override
100 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
101 | final View view = inflater.inflate(R.layout.fragment_reader, container, false);
102 |
103 | mPageNavLayout = getActivity().findViewById(R.id.pageNavLayout);
104 |
105 | mSeekBar = mPageNavLayout.findViewById(R.id.pageSeekBar);
106 | mSeekBar.setMaxPageCount(mComic.getPagesCount());
107 | mSeekBar.setProgressToRight(!mSharedRes.getSettingsManager().getIsReadingLeftToRight());
108 | mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
109 | @Override
110 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
111 | if (fromUser) {
112 | setCurrentPage(linearIdxToPage(progress));
113 | }
114 | }
115 |
116 | @Override
117 | public void onStartTrackingTouch(SeekBar seekBar) {
118 | mSharedRes.getPicasso().pauseTag(getActivity());
119 | }
120 |
121 | @Override
122 | public void onStopTrackingTouch(SeekBar seekBar) {
123 | mSharedRes.getPicasso().resumeTag(getActivity());
124 | }
125 | });
126 |
127 | mPageNavTextView = mPageNavLayout.findViewById(R.id.pageNavTextView);
128 |
129 | mViewPager = view.findViewById(R.id.viewPager);
130 | mViewPager.setAdapter(mPagerAdapter);
131 | mViewPager.setOffscreenPageLimit(3);
132 | mViewPager.setOnTouchListener(new View.OnTouchListener() {
133 | @Override
134 | public boolean onTouch(View v, MotionEvent event) {
135 | return mGestureDetector.onTouchEvent(event);
136 | }
137 | });
138 | mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
139 | @Override
140 | public void onPageSelected(int position) {
141 | setCurrentPage(linearIdxToPage(position));
142 | }
143 | });
144 | mViewPager.setOnSwipeOutListener(new SwipeOutViewPager.OnSwipeOutListener() {
145 | @Override
146 | public void onSwipeOutAtStart() {
147 | if (mSharedRes.getSettingsManager().getIsReadingLeftToRight()) {
148 | toPreviousComic();
149 | } else {
150 | toNextComic();
151 | }
152 | }
153 |
154 | @Override
155 | public void onSwipeOutAtEnd() {
156 | if (mSharedRes.getSettingsManager().getIsReadingLeftToRight()) {
157 | toNextComic();
158 | } else {
159 | toPreviousComic();
160 | }
161 | }
162 | });
163 |
164 | mCurrentPageNum = mComic.getPageLastRead();
165 | if (mCurrentPageNum == 0) {
166 | // First time we open this comic
167 | mCurrentPageNum += 1;
168 | }
169 | setCurrentPage(mCurrentPageNum);
170 |
171 | if (savedInstanceState != null) {
172 | boolean fullscreen = savedInstanceState.getBoolean(STATE_FULLSCREEN);
173 | setFullscreen(fullscreen);
174 | } else {
175 | setFullscreen(true);
176 | }
177 | getActivity().setTitle(mComic.getName());
178 |
179 | return view;
180 | }
181 |
182 | @Override
183 | public void onSaveInstanceState(Bundle outState) {
184 | outState.putBoolean(STATE_FULLSCREEN, mIsFullscreen);
185 | super.onSaveInstanceState(outState);
186 | }
187 |
188 | @Override
189 | public void onDestroy() {
190 | mSharedRes.getPicasso().cancelTag(getActivity());
191 | if (!mCompositeDisposable.isDisposed()) {
192 | mCompositeDisposable.dispose();
193 | }
194 | super.onDestroy();
195 | }
196 |
197 | private int pageToLinearIdx(int pageNum) {
198 | // Views order in the adapters are always in the LeftToRight order
199 | if (mSharedRes.getSettingsManager().getIsReadingLeftToRight()) {
200 | return pageNum - 1;
201 | } else {
202 | return mComic.getPagesCount() - pageNum;
203 | }
204 | }
205 |
206 | private int linearIdxToPage(int position) {
207 | // Views order in the adapters are always in the LeftToRight order
208 | if (mSharedRes.getSettingsManager().getIsReadingLeftToRight()) {
209 | return position + 1;
210 | } else {
211 | return mComic.getPagesCount() - position;
212 | }
213 | }
214 |
215 | private void setCurrentPage(int pageNum) {
216 | setCurrentPage(pageNum, true);
217 | }
218 |
219 | private void setCurrentPage(int pageNum, boolean animated) {
220 | mCurrentPageNum = pageNum;
221 | mViewPager.setCurrentItem(pageToLinearIdx(pageNum), animated);
222 | mSeekBar.setPageProgress(pageNum);
223 | mPageNavTextView.setText(String.format("%d/%d", pageNum, mComic.getPagesCount()));
224 |
225 | setLastReadPage(mCurrentPageNum);
226 | }
227 |
228 | private void setLastReadPage(int pageNum) {
229 | mSharedRes.getLibraryManager()
230 | .setComicLastReadPage(mComic.getId(), pageNum)
231 | .subscribe(new Utils.DefaultCompletableObserver("setLastReadPage", mCompositeDisposable) {
232 | @Override
233 | public void onComplete() {
234 | Log.d(mTag, "onComplete");
235 | mComic.setPageLastRead(pageNum);
236 | }
237 | });
238 | }
239 |
240 | @Override
241 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
242 | inflater.inflate(R.menu.reader, menu);
243 |
244 | switch (mSharedRes.getSettingsManager().getPageViewMode()) {
245 | case ASPECT_FILL:
246 | menu.findItem(R.id.view_mode_aspect_fill).setChecked(true);
247 | break;
248 | case ASPECT_FIT:
249 | menu.findItem(R.id.view_mode_aspect_fit).setChecked(true);
250 | break;
251 | case FIT_WIDTH:
252 | menu.findItem(R.id.view_mode_fit_width).setChecked(true);
253 | break;
254 | }
255 |
256 | if (mSharedRes.getSettingsManager().getIsReadingLeftToRight()) {
257 | menu.findItem(R.id.reading_left_to_right).setChecked(true);
258 | } else {
259 | menu.findItem(R.id.reading_right_to_left).setChecked(true);
260 | }
261 | }
262 |
263 | @Override
264 | public boolean onOptionsItemSelected(MenuItem item) {
265 | switch (item.getItemId()) {
266 | case R.id.view_mode_aspect_fill:
267 | changePageViewMode(SettingsManager.PageViewMode.ASPECT_FILL, mViewPager);
268 | break;
269 | case R.id.view_mode_aspect_fit:
270 | changePageViewMode(SettingsManager.PageViewMode.ASPECT_FIT, mViewPager);
271 | break;
272 | case R.id.view_mode_fit_width:
273 | changePageViewMode(SettingsManager.PageViewMode.FIT_WIDTH, mViewPager);
274 | break;
275 |
276 | case R.id.reading_left_to_right:
277 | changeReadingDirection(true);
278 | break;
279 | case R.id.reading_right_to_left:
280 | changeReadingDirection(false);
281 | break;
282 | }
283 |
284 | item.setChecked(true);
285 | return super.onOptionsItemSelected(item);
286 | }
287 |
288 | private void changePageViewMode(SettingsManager.PageViewMode mode, ViewGroup parentView) {
289 | mSharedRes.getSettingsManager().setPageViewMode(mode);
290 |
291 | // Announce the PageViewMode change to the existing PageImageViews
292 | for (int i = 0; i < parentView.getChildCount(); i++) {
293 | final View child = parentView.getChildAt(i);
294 | if (child instanceof ViewGroup) {
295 | changePageViewMode(mode, (ViewGroup) child);
296 | } else if (child instanceof PageImageView) {
297 | PageImageView pageImageView = (PageImageView) child;
298 | pageImageView.setViewMode(
299 | mSharedRes.getSettingsManager().getPageViewMode(),
300 | mSharedRes.getSettingsManager().getIsReadingLeftToRight());
301 | }
302 | }
303 | }
304 |
305 | private void changeReadingDirection(boolean isLeftToRight) {
306 | mSharedRes.getSettingsManager().setIsReadingLeftToRight(isLeftToRight);
307 | mSeekBar.setProgressToRight(!isLeftToRight);
308 |
309 | setCurrentPage(mCurrentPageNum, false);
310 | }
311 |
312 | private class ComicPagerAdapter extends PagerAdapter {
313 | private SparseArray mPageViewTargets;
314 |
315 | public ComicPagerAdapter(int pageCount) {
316 | mPageViewTargets = new SparseArray<>(pageCount);
317 | }
318 |
319 | @Override
320 | public int getItemPosition(Object object) {
321 | return POSITION_NONE;
322 | }
323 |
324 | @Override
325 | public int getCount() {
326 | return mComic.getPagesCount();
327 | }
328 |
329 | @Override
330 | public boolean isViewFromObject(View view, Object o) {
331 | return view == o;
332 | }
333 |
334 | @Override
335 | public Object instantiateItem(ViewGroup container, int position) {
336 | final LayoutInflater inflater = (LayoutInflater) getActivity()
337 | .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
338 |
339 | View view = inflater.inflate(R.layout.fragment_reader_page, container, false);
340 |
341 | PageImageView pageImageView = view.findViewById(R.id.pageImageView);
342 | pageImageView.setOnTouchListener(new View.OnTouchListener() {
343 | @Override
344 | public boolean onTouch(View v, MotionEvent event) {
345 | return mGestureDetector.onTouchEvent(event);
346 | }
347 | });
348 | pageImageView.setViewMode(
349 | mSharedRes.getSettingsManager().getPageViewMode(),
350 | mSharedRes.getSettingsManager().getIsReadingLeftToRight());
351 |
352 | container.addView(view);
353 |
354 | ComicPageViewTarget t = new ComicPageViewTarget(view, position);
355 | loadPageImage(t, linearIdxToPage(position));
356 | mPageViewTargets.put(position, t);
357 |
358 | return view;
359 | }
360 |
361 | @Override
362 | public void destroyItem(ViewGroup container, int position, Object object) {
363 | mSharedRes.getPicasso().cancelRequest(mPageViewTargets.get(position));
364 |
365 | mPageViewTargets.delete(position);
366 |
367 | View view = (View) object;
368 | container.removeView(view);
369 |
370 | ImageView iv = view.findViewById(R.id.pageImageView);
371 | Drawable drawable = iv.getDrawable();
372 | if (drawable instanceof BitmapDrawable) {
373 | BitmapDrawable bd = (BitmapDrawable) drawable;
374 | Bitmap bm = bd.getBitmap();
375 | if (bm != null) {
376 | bm.recycle();
377 | }
378 | }
379 | }
380 | }
381 |
382 | private void loadPageImage(ComicPageViewTarget t, int pageNum) {
383 | mSharedRes.getPicasso()
384 | .load(mSharedRes.getLibraryManager().getComicPageUri(mComic.getId(), pageNum))
385 | .memoryPolicy(MemoryPolicy.NO_STORE)
386 | .tag(getActivity())
387 | .into(t);
388 | }
389 |
390 | private class ComicPageViewTarget implements Target {
391 | //TODO: From docs picasso.Target must have a proper Object#equals(Object) and Object#hashCode()
392 | private View mPageView;
393 | private int mPageNum;
394 |
395 | public ComicPageViewTarget(View pageView, int pageNum) {
396 | mPageView = pageView;
397 | mPageNum = pageNum;
398 | }
399 |
400 | private void setVisibility(int imageView, int progressBar, int reloadButton) {
401 | mPageView.findViewById(R.id.pageImageView).setVisibility(imageView);
402 | mPageView.findViewById(R.id.pageProgressBar).setVisibility(progressBar);
403 | mPageView.findViewById(R.id.reloadButton).setVisibility(reloadButton);
404 | }
405 |
406 | @Override
407 | public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
408 | setVisibility(View.VISIBLE, View.GONE, View.GONE);
409 | ImageView iv = mPageView.findViewById(R.id.pageImageView);
410 | iv.setImageBitmap(bitmap);
411 | }
412 |
413 | @Override
414 | public void onBitmapFailed(Exception e, Drawable errorDrawable) {
415 | setVisibility(View.GONE, View.GONE, View.VISIBLE);
416 | ImageButton reloadButton = mPageView.findViewById(R.id.reloadButton);
417 |
418 | reloadButton.setOnClickListener((View v) -> {
419 | setVisibility(View.GONE, View.VISIBLE, View.GONE);
420 | loadPageImage(this, mPageNum);
421 | });
422 | }
423 |
424 | @Override
425 | public void onPrepareLoad(Drawable placeHolderDrawable) {
426 |
427 | }
428 | }
429 |
430 | private class FingerTapListener extends GestureDetector.SimpleOnGestureListener {
431 | @Override
432 | public boolean onSingleTapConfirmed(MotionEvent e) {
433 | if (!mIsFullscreen) {
434 | setFullscreen(true);
435 | return true;
436 | }
437 |
438 | float x = e.getX();
439 | boolean isReadingLeftToRight = mSharedRes.getSettingsManager().getIsReadingLeftToRight();
440 |
441 | // Tap left edge
442 | if (x < (float) mViewPager.getWidth() / 3) {
443 | if (isReadingLeftToRight) {
444 | if (mCurrentPageNum == 1) {
445 | toPreviousComic();
446 | } else {
447 | setCurrentPage(mCurrentPageNum - 1);
448 | }
449 | } else {
450 | if (mCurrentPageNum == mComic.getPagesCount()) {
451 | toNextComic();
452 | } else {
453 | setCurrentPage(mCurrentPageNum + 1);
454 | }
455 | }
456 | }
457 | // Tap right edge
458 | else if (x > (float) mViewPager.getWidth() / 3 * 2) {
459 | if (isReadingLeftToRight) {
460 | if (mCurrentPageNum == mComic.getPagesCount()) {
461 | toNextComic();
462 | } else {
463 | setCurrentPage(mCurrentPageNum + 1);
464 | }
465 | } else {
466 | if (mCurrentPageNum == 1) {
467 | toPreviousComic();
468 | } else {
469 | setCurrentPage(mCurrentPageNum - 1);
470 | }
471 | }
472 | }
473 | // Tap center
474 | else {
475 | setFullscreen(false);
476 | }
477 |
478 | return true;
479 | }
480 | }
481 |
482 | private void setFullscreen(boolean fullscreen) {
483 | mIsFullscreen = fullscreen;
484 |
485 | ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
486 |
487 | if (mIsFullscreen) {
488 | if (actionBar != null) actionBar.hide();
489 |
490 | int flag = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
491 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
492 | | View.SYSTEM_UI_FLAG_FULLSCREEN;
493 | if (Utils.isKitKatOrLater()) {
494 | flag |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
495 | flag |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
496 | flag |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
497 | }
498 | mViewPager.setSystemUiVisibility(flag);
499 |
500 | mPageNavLayout.setVisibility(View.INVISIBLE);
501 | } else {
502 | if (actionBar != null) actionBar.show();
503 |
504 | int flag = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
505 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
506 | if (Utils.isKitKatOrLater()) {
507 | flag |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
508 | }
509 | mViewPager.setSystemUiVisibility(flag);
510 |
511 | mPageNavLayout.setVisibility(View.VISIBLE);
512 |
513 | // status bar & navigation bar background won't show in some cases
514 | if (Utils.isLollipopOrLater()) {
515 | new Handler().postDelayed(() -> {
516 | Window w = getActivity().getWindow();
517 | w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
518 | w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
519 | }, 300);
520 | }
521 | }
522 | }
523 |
524 | private void toPreviousComic() {
525 | mSharedRes.getLibraryManager()
526 | .getSeriePreviousComic(mComic.getSeriesId(), mComic.getId())
527 | .subscribe(new Utils.DefaultMaybeObserver("toPreviousComic", mCompositeDisposable) {
528 | @Override
529 | public void onSuccess(Comic comic) {
530 | Log.d(mTag, "onSuccess");
531 | if (mSharedRes.getSettingsManager().getIsSwitchComicConfirmation()) {
532 | confirmSwitchToComic(comic, R.string.switch_prev_confirm_comic);
533 | } else {
534 | switchToComic(comic, R.string.switch_prev_comic);
535 | }
536 | }
537 |
538 | @Override
539 | public void onComplete() {
540 | Log.d(mTag, "onComplete");
541 | noMoreComic(R.string.switch_previous_no_more_comic);
542 | }
543 | });
544 | }
545 |
546 | private void toNextComic() {
547 | mSharedRes.getLibraryManager()
548 | .getSerieNextComic(mComic.getSeriesId(), mComic.getId())
549 | .subscribe(new Utils.DefaultMaybeObserver("toNextComic.getSerieNextComic", mCompositeDisposable) {
550 | @Override
551 | public void onSuccess(Comic comic) {
552 | Log.d(mTag, "onSuccess");
553 | if (mSharedRes.getSettingsManager().getIsSwitchComicConfirmation()) {
554 | confirmSwitchToComic(comic, R.string.switch_next_confirm_comic);
555 | } else {
556 | switchToComic(comic, R.string.switch_next_comic);
557 | }
558 | }
559 |
560 | @Override
561 | public void onComplete() {
562 | Log.d(mTag, "onComplete");
563 | noMoreComic(R.string.switch_next_no_more_comic);
564 | }
565 | });
566 | }
567 |
568 | private void confirmSwitchToComic(Comic newComic, int titleId) {
569 | new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle)
570 | .setTitle(titleId)
571 | .setMessage(newComic.getName())
572 | .setPositiveButton(R.string.switch_action_positive, (d, which) -> {
573 | ReaderActivity activity = (ReaderActivity) getActivity();
574 | activity.setFragment(ReaderFragment.create(newComic));
575 | })
576 | .setNegativeButton(R.string.switch_action_negative, (dialog, which) -> {
577 | })
578 | .create()
579 | .show();
580 | }
581 |
582 | private void switchToComic(Comic newComic, int titleId) {
583 | String text = String.format("%s: %s",
584 | getActivity().getResources().getString(titleId),
585 | newComic.getName());
586 |
587 | ReaderActivity activity = (ReaderActivity) getActivity();
588 | activity.setFragment(ReaderFragment.create(newComic));
589 | Toast.makeText(getContext(), text, Toast.LENGTH_SHORT).show();
590 | }
591 |
592 | private void noMoreComic(int messageId) {
593 | new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle)
594 | .setMessage(messageId)
595 | .setNeutralButton(R.string.switch_action_ok, (dialog, which) -> {
596 | })
597 | .create()
598 | .show();
599 | }
600 | }
601 |
--------------------------------------------------------------------------------