├── 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 | 3 | 5 | 8 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /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 | 4 | 8 | 9 | 10 | 13 | 15 | 17 | 19 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------