diff --git a/app/build.gradle b/app/build.gradle index e2c257a60..4d5e8854f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,6 +62,7 @@ dependencies { compile 'com.squareup.okhttp:okhttp-urlconnection:2.4.0' compile 'com.squareup.okhttp:okhttp:2.4.0' compile 'com.squareup.okio:okio:1.6.0' + compile 'com.google.code.gson:gson:2.4' compile 'com.jakewharton:disklrucache:2.0.2' compile 'org.jsoup:jsoup:1.8.3' compile 'io.reactivex:rxandroid:1.0.1' @@ -76,6 +77,7 @@ dependencies { compile 'com.jakewharton.timber:timber:3.1.0' compile 'uk.co.ribot:easyadapter:1.5.0@aar' compile 'ch.acra:acra:4.6.2' + compile 'com.davemorrissey.labs:subsampling-scale-image-view:3.4.1' compile "frankiesardo:icepick:$ICEPICK_VERSION" provided "frankiesardo:icepick-processor:$ICEPICK_VERSION" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0178dda65..152ca7f38 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,15 @@ android:name="android.support.PARENT_ACTIVITY" android:value="eu.kanade.mangafeed.ui.activity.MainActivity" /> + + + diff --git a/app/src/main/java/eu/kanade/mangafeed/AppComponent.java b/app/src/main/java/eu/kanade/mangafeed/AppComponent.java index 42f949288..758f1e334 100644 --- a/app/src/main/java/eu/kanade/mangafeed/AppComponent.java +++ b/app/src/main/java/eu/kanade/mangafeed/AppComponent.java @@ -8,10 +8,12 @@ import dagger.Component; import eu.kanade.mangafeed.data.DataModule; import eu.kanade.mangafeed.presenter.CataloguePresenter; import eu.kanade.mangafeed.presenter.LibraryPresenter; +import eu.kanade.mangafeed.presenter.MainPresenter; import eu.kanade.mangafeed.presenter.MangaChaptersPresenter; import eu.kanade.mangafeed.presenter.MangaDetailPresenter; import eu.kanade.mangafeed.presenter.MangaInfoPresenter; import eu.kanade.mangafeed.presenter.SourcePresenter; +import eu.kanade.mangafeed.presenter.ViewerPresenter; @Singleton @Component( @@ -22,12 +24,14 @@ import eu.kanade.mangafeed.presenter.SourcePresenter; ) public interface AppComponent { + void inject(MainPresenter mainPresenter); void inject(LibraryPresenter libraryPresenter); void inject(MangaDetailPresenter mangaDetailPresenter); void inject(SourcePresenter sourcePresenter); void inject(CataloguePresenter cataloguePresenter); void inject(MangaInfoPresenter mangaInfoPresenter); void inject(MangaChaptersPresenter mangaChaptersPresenter); + void inject(ViewerPresenter viewerPresenter); Application application(); diff --git a/app/src/main/java/eu/kanade/mangafeed/data/caches/CacheManager.java b/app/src/main/java/eu/kanade/mangafeed/data/caches/CacheManager.java index 715541e68..066e38a6e 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/caches/CacheManager.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/caches/CacheManager.java @@ -5,20 +5,23 @@ import android.content.Context; import com.bumptech.glide.Glide; import com.bumptech.glide.request.FutureTarget; import com.bumptech.glide.request.target.Target; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import com.jakewharton.disklrucache.DiskLruCache; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.lang.reflect.Type; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import eu.kanade.mangafeed.data.models.Page; import eu.kanade.mangafeed.util.DiskUtils; import rx.Observable; -import rx.functions.Action0; public class CacheManager { @@ -29,11 +32,13 @@ public class CacheManager { private static final int READ_TIMEOUT = 60; private Context mContext; + private Gson mGson; private DiskLruCache mDiskCache; public CacheManager(Context context) { mContext = context; + mGson = new Gson(); try { mDiskCache = DiskLruCache.open( @@ -109,16 +114,11 @@ public class CacheManager { return isSuccessful; } - public Observable getImageUrlsFromDiskCache(final String chapterUrl) { + public Observable> getPageUrlsFromDiskCache(final String chapterUrl) { return Observable.create(subscriber -> { try { - String[] imageUrls = getImageUrlsFromDiskCacheImpl(chapterUrl); - - for (String imageUrl : imageUrls) { - if (!subscriber.isUnsubscribed()) { - subscriber.onNext(imageUrl); - } - } + List pages = getPageUrlsFromDiskCacheImpl(chapterUrl); + subscriber.onNext(pages); subscriber.onCompleted(); } catch (Throwable e) { subscriber.onError(e); @@ -126,35 +126,28 @@ public class CacheManager { }); } - private String[] getImageUrlsFromDiskCacheImpl(String chapterUrl) throws IOException { + private List getPageUrlsFromDiskCacheImpl(String chapterUrl) throws IOException { DiskLruCache.Snapshot snapshot = null; + List pages = null; try { String key = DiskUtils.hashKeyForDisk(chapterUrl); - snapshot = mDiskCache.get(key); - String joinedImageUrls = snapshot.getString(0); - return joinedImageUrls.split(","); + Type collectionType = new TypeToken>() {}.getType(); + pages = mGson.fromJson(snapshot.getString(0), collectionType); + } catch (IOException e) { + // Do Nothing. } finally { if (snapshot != null) { snapshot.close(); } } + return pages; } - public Action0 putImageUrlsToDiskCache(final String chapterUrl, final List imageUrls) { - return () -> { - try { - putImageUrlsToDiskCacheImpl(chapterUrl, imageUrls); - } catch (IOException e) { - // Do Nothing. - } - }; - } - - private void putImageUrlsToDiskCacheImpl(String chapterUrl, List imageUrls) throws IOException { - String cachedValue = joinImageUrlsToCacheValue(imageUrls); + public void putPageUrlsToDiskCache(final String chapterUrl, final List pages) { + String cachedValue = mGson.toJson(pages); DiskLruCache.Editor editor = null; OutputStream outputStream = null; @@ -171,13 +164,11 @@ public class CacheManager { mDiskCache.flush(); editor.commit(); + } catch (Exception e) { + // Do Nothing. } finally { if (editor != null) { - try { - editor.abort(); - } catch (IOException ignore) { - // Do Nothing. - } + editor.abortUnlessCommitted(); } if (outputStream != null) { try { @@ -189,22 +180,9 @@ public class CacheManager { } } - private String joinImageUrlsToCacheValue(List imageUrls) { - StringBuilder stringBuilder = new StringBuilder(); - for (int index = 0; index < imageUrls.size(); index++) { - if (index == 0) { - stringBuilder.append(imageUrls.get(index)); - } else { - stringBuilder.append(","); - stringBuilder.append(imageUrls.get(index)); - } - } - - return stringBuilder.toString(); - } - public File getCacheDir() { return mDiskCache.getDirectory(); } + } diff --git a/app/src/main/java/eu/kanade/mangafeed/data/models/Page.java b/app/src/main/java/eu/kanade/mangafeed/data/models/Page.java new file mode 100644 index 000000000..e02f072c7 --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/data/models/Page.java @@ -0,0 +1,43 @@ +package eu.kanade.mangafeed.data.models; + +public class Page { + + private int pageNumber; + private String url; + private String imageUrl; + + public Page(int pageNumber, String url, String imageUrl) { + this.pageNumber = pageNumber; + this.url = url; + this.imageUrl = imageUrl; + } + + public Page(int pageNumber, String url) { + this(pageNumber, url, null); + } + + public int getPageNumber() { + return pageNumber; + } + + public String getUrl() { + return url; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + @Override + public String toString() { + return "Page{" + + "pageNumber=" + pageNumber + + ", url='" + url + '\'' + + ", imageUrl='" + imageUrl + '\'' + + '}'; + } +} diff --git a/app/src/main/java/eu/kanade/mangafeed/presenter/MainPresenter.java b/app/src/main/java/eu/kanade/mangafeed/presenter/MainPresenter.java new file mode 100644 index 000000000..792d8a12a --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/presenter/MainPresenter.java @@ -0,0 +1,7 @@ +package eu.kanade.mangafeed.presenter; + +import eu.kanade.mangafeed.ui.activity.MainActivity; + +public class MainPresenter extends BasePresenter { + +} diff --git a/app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java b/app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java index 833d212ef..5900784b2 100644 --- a/app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java @@ -13,13 +13,14 @@ import eu.kanade.mangafeed.data.helpers.DatabaseHelper; import eu.kanade.mangafeed.data.helpers.SourceManager; import eu.kanade.mangafeed.data.models.Chapter; import eu.kanade.mangafeed.data.models.Manga; +import eu.kanade.mangafeed.sources.Source; import eu.kanade.mangafeed.ui.fragment.MangaChaptersFragment; import eu.kanade.mangafeed.util.EventBusHook; import eu.kanade.mangafeed.util.events.ChapterCountEvent; +import eu.kanade.mangafeed.util.events.SourceChapterEvent; import rx.Observable; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; -import timber.log.Timber; public class MangaChaptersPresenter extends BasePresenter { @@ -27,6 +28,7 @@ public class MangaChaptersPresenter extends BasePresenter @Inject SourceManager sourceManager; private Manga manga; + private Source source; private static final int DB_CHAPTERS = 1; private static final int ONLINE_CHAPTERS = 2; @@ -71,6 +73,7 @@ public class MangaChaptersPresenter extends BasePresenter public void onEventMainThread(Manga manga) { if (this.manga == null) { this.manga = manga; + source = sourceManager.get(manga.source); start(DB_CHAPTERS); // Get chapters if it's an online source @@ -94,11 +97,14 @@ public class MangaChaptersPresenter extends BasePresenter } private Observable getOnlineChaptersObs() { - return sourceManager.get(manga.source) + return source .pullChaptersFromNetwork(manga.url) .subscribeOn(Schedulers.io()) .flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters)) .observeOn(AndroidSchedulers.mainThread()); } + public void onChapterClicked(Chapter chapter) { + EventBus.getDefault().postSticky(new SourceChapterEvent(source, chapter)); + } } diff --git a/app/src/main/java/eu/kanade/mangafeed/presenter/MangaInfoPresenter.java b/app/src/main/java/eu/kanade/mangafeed/presenter/MangaInfoPresenter.java index d960f21de..de4362889 100644 --- a/app/src/main/java/eu/kanade/mangafeed/presenter/MangaInfoPresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/presenter/MangaInfoPresenter.java @@ -10,7 +10,6 @@ import eu.kanade.mangafeed.ui.fragment.MangaInfoFragment; import eu.kanade.mangafeed.util.EventBusHook; import eu.kanade.mangafeed.util.events.ChapterCountEvent; import rx.Observable; -import timber.log.Timber; public class MangaInfoPresenter extends BasePresenter { diff --git a/app/src/main/java/eu/kanade/mangafeed/presenter/SourcePresenter.java b/app/src/main/java/eu/kanade/mangafeed/presenter/SourcePresenter.java index 5e29dde3f..c4e23f484 100644 --- a/app/src/main/java/eu/kanade/mangafeed/presenter/SourcePresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/presenter/SourcePresenter.java @@ -3,7 +3,6 @@ package eu.kanade.mangafeed.presenter; import javax.inject.Inject; import eu.kanade.mangafeed.data.helpers.SourceManager; -import eu.kanade.mangafeed.sources.Source; import eu.kanade.mangafeed.ui.fragment.SourceFragment; diff --git a/app/src/main/java/eu/kanade/mangafeed/presenter/ViewerPresenter.java b/app/src/main/java/eu/kanade/mangafeed/presenter/ViewerPresenter.java new file mode 100644 index 000000000..e736a39f8 --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/presenter/ViewerPresenter.java @@ -0,0 +1,93 @@ +package eu.kanade.mangafeed.presenter; + +import android.os.Bundle; + +import java.util.List; + +import javax.inject.Inject; + +import de.greenrobot.event.EventBus; +import eu.kanade.mangafeed.data.caches.CacheManager; +import eu.kanade.mangafeed.data.models.Chapter; +import eu.kanade.mangafeed.data.models.Page; +import eu.kanade.mangafeed.sources.Source; +import eu.kanade.mangafeed.ui.activity.ViewerActivity; +import eu.kanade.mangafeed.util.EventBusHook; +import eu.kanade.mangafeed.util.events.SourceChapterEvent; +import rx.Observable; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + +public class ViewerPresenter extends BasePresenter { + + private static final int GET_PAGE_LIST = 1; + private Source source; + private Chapter chapter; + private List pageList; + + @Inject CacheManager cacheManager; + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + restartableReplay(GET_PAGE_LIST, + this::getPageListObservable, + (view, page) -> { + }); + } + + @Override + protected void onTakeView(ViewerActivity view) { + super.onTakeView(view); + registerForStickyEvents(); + } + + @Override + protected void onDropView() { + unregisterForEvents(); + super.onDropView(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + EventBus.getDefault().removeStickyEvent(SourceChapterEvent.class); + source.savePageList(chapter.url, pageList); + } + + @EventBusHook + public void onEventMainThread(SourceChapterEvent event) { + if (source == null || chapter == null) { + source = event.getSource(); + chapter = event.getChapter(); + + start(1); + } + } + + private Observable getPageListObservable() { + return source.pullPageListFromNetwork(chapter.url) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .flatMap(pageList -> { + this.pageList = pageList; + + return Observable.merge( + Observable.from(pageList) + .filter(page -> page.getImageUrl() != null), + + source.getRemainingImageUrlsFromPageList(pageList) + .doOnNext(this::replacePageUrl)); + }); + } + + private void replacePageUrl(Page page) { + for (int i = 0; i < pageList.size(); i++) { + if (pageList.get(i).getPageNumber() == page.getPageNumber()) { + pageList.set(i, page); + return; + } + } + } +} diff --git a/app/src/main/java/eu/kanade/mangafeed/sources/Source.java b/app/src/main/java/eu/kanade/mangafeed/sources/Source.java index fa91b6a0f..87d2a8e92 100644 --- a/app/src/main/java/eu/kanade/mangafeed/sources/Source.java +++ b/app/src/main/java/eu/kanade/mangafeed/sources/Source.java @@ -10,11 +10,49 @@ import eu.kanade.mangafeed.data.caches.CacheManager; import eu.kanade.mangafeed.data.helpers.NetworkHelper; import eu.kanade.mangafeed.data.models.Chapter; import eu.kanade.mangafeed.data.models.Manga; +import eu.kanade.mangafeed.data.models.Page; import rx.Observable; import rx.schedulers.Schedulers; public abstract class Source { + // Methods to implement or optionally override + + // Name of the source to display + public abstract String getName(); + + // Id of the source (must be declared and obtained from SourceManager to avoid conflicts) + public abstract int getSourceId(); + + protected abstract String getUrlFromPageNumber(int page); + protected abstract String getSearchUrl(String query, int page); + protected abstract List parsePopularMangasFromHtml(String unparsedHtml); + protected abstract List parseSearchFromHtml(String unparsedHtml); + protected abstract Manga parseHtmlToManga(String mangaUrl, String unparsedHtml); + protected abstract List parseHtmlToChapters(String unparsedHtml); + protected abstract List parseHtmlToPageUrls(String unparsedHtml); + protected abstract String parseHtmlToImageUrl(String unparsedHtml); + + // Get the URL to the details of a manga, useful if the source provides some kind of API or fast calls + protected String getMangaUrl(String defaultMangaUrl) { + return defaultMangaUrl; + } + + // Default headers, it can be overriden by children or just add new keys + protected Headers.Builder headersBuilder() { + Headers.Builder builder = new Headers.Builder(); + builder.add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)"); + return builder; + } + + // Number of images to download at the same time + protected int getNumberOfConcurrentImageDownloads() { + return 3; + } + + + // ***** Source class implementation ***** + protected NetworkHelper mNetworkService; protected CacheManager mCacheManager; protected Headers mRequestHeaders; @@ -25,13 +63,6 @@ public abstract class Source { mRequestHeaders = headersBuilder().build(); } - // Default headers, it can be overriden by children or add new keys - protected Headers.Builder headersBuilder() { - Headers.Builder builder = new Headers.Builder(); - builder.add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)"); - return builder; - } - // Get the most popular mangas from the source public Observable> pullPopularMangasFromNetwork(int page) { String url = getUrlFromPageNumber(page); @@ -62,56 +93,54 @@ public abstract class Source { Observable.just(parseHtmlToChapters(unparsedHtml))); } - // Get the URLs of the images of a chapter - public Observable getImageUrlsFromNetwork(final String chapterUrl) { - return mNetworkService - .getStringResponse(chapterUrl, mNetworkService.NULL_CACHE_CONTROL, mRequestHeaders) - .flatMap(unparsedHtml -> Observable.from(parseHtmlToPageUrls(unparsedHtml))) - .buffer(3) - .concatMap(batchedPageUrls -> { - List> imageUrlObservables = new ArrayList<>(); - for (String pageUrl : batchedPageUrls) { - Observable temporaryObservable = mNetworkService - .getStringResponse(pageUrl, mNetworkService.NULL_CACHE_CONTROL, mRequestHeaders) - .flatMap(unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml))) - .subscribeOn(Schedulers.io()); - - imageUrlObservables.add(temporaryObservable); - } - - return Observable.merge(imageUrlObservables); - }); - } - - // Store the URLs of a chapter in the cache - public Observable pullImageUrlsFromNetwork(final String chapterUrl) { - final List temporaryCachedImageUrls = new ArrayList<>(); - - return mCacheManager.getImageUrlsFromDiskCache(chapterUrl) + public Observable> pullPageListFromNetwork(final String chapterUrl) { + return mCacheManager.getPageUrlsFromDiskCache(chapterUrl) .onErrorResumeNext(throwable -> { - return getImageUrlsFromNetwork(chapterUrl) - .doOnNext(imageUrl -> temporaryCachedImageUrls.add(imageUrl)) - .doOnCompleted(mCacheManager.putImageUrlsToDiskCache(chapterUrl, temporaryCachedImageUrls)); + return mNetworkService + .getStringResponse(chapterUrl, mNetworkService.NULL_CACHE_CONTROL, mRequestHeaders) + .flatMap(unparsedHtml -> Observable.just(parseHtmlToPageUrls(unparsedHtml))) + .flatMap(this::convertToPages) + .doOnNext(pages -> savePageList(chapterUrl, pages)); }) .onBackpressureBuffer(); } - // Get the URL to the details of a manga, useful if the source provides some kind of API or fast calls - protected String getMangaUrl(String defaultMangaUrl) { - return defaultMangaUrl; + // Get the URLs of the images of a chapter + public Observable getRemainingImageUrlsFromPageList(final List pages) { + return Observable.from(pages) + .filter(page -> page.getImageUrl() == null) + .buffer(getNumberOfConcurrentImageDownloads()) + .concatMap(batchedPages -> { + List> pageObservable = new ArrayList<>(); + for (Page page : batchedPages) { + pageObservable.add(getImageUrlFromPage(page)); + } + return Observable.merge(pageObservable); + }); } - public abstract String getName(); - public abstract int getSourceId(); + private Observable getImageUrlFromPage(final Page page) { + return mNetworkService + .getStringResponse(page.getUrl(), mNetworkService.NULL_CACHE_CONTROL, mRequestHeaders) + .flatMap(unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml))) + .flatMap(imageUrl -> { + page.setImageUrl(imageUrl); + return Observable.just(page); + }) + .subscribeOn(Schedulers.io()); + } - protected abstract String getUrlFromPageNumber(int page); - protected abstract String getSearchUrl(String query, int page); - protected abstract List parsePopularMangasFromHtml(String unparsedHtml); - protected abstract List parseSearchFromHtml(String unparsedHtml); - protected abstract Manga parseHtmlToManga(String mangaUrl, String unparsedHtml); - protected abstract List parseHtmlToChapters(String unparsedHtml); - protected abstract List parseHtmlToPageUrls(String unparsedHtml); - protected abstract String parseHtmlToImageUrl(String unparsedHtml); + public void savePageList(String chapterUrl, List pages) { + mCacheManager.putPageUrlsToDiskCache(chapterUrl, pages); + } + + private Observable> convertToPages(List pageUrls) { + List pages = new ArrayList<>(); + for (int i = 0; i < pageUrls.size(); i++) { + pages.add(new Page(i, pageUrls.get(i))); + } + return Observable.just(pages); + } } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/activity/BaseActivity.java b/app/src/main/java/eu/kanade/mangafeed/ui/activity/BaseActivity.java index 079bd28ee..e40afaef6 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/activity/BaseActivity.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/activity/BaseActivity.java @@ -8,7 +8,6 @@ import eu.kanade.mangafeed.App; import nucleus.factory.PresenterFactory; import nucleus.presenter.Presenter; import nucleus.view.NucleusAppCompatActivity; -import timber.log.Timber; public class BaseActivity

extends NucleusAppCompatActivity

{ @@ -17,11 +16,7 @@ public class BaseActivity

extends NucleusAppCompatActivity< final PresenterFactory

superFactory = super.getPresenterFactory(); setPresenterFactory(() -> { P presenter = superFactory.createPresenter(); - try { - App.getComponentReflection(getActivity()).inject(presenter); - } catch(Exception e) { - Timber.w("No injection for " + presenter.getClass().toString()); - } + App.getComponentReflection(getActivity()).inject(presenter); return presenter; }); super.onCreate(savedInstanceState); diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/activity/MainActivity.java b/app/src/main/java/eu/kanade/mangafeed/ui/activity/MainActivity.java index 72ec06721..13e46ab0c 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/activity/MainActivity.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/activity/MainActivity.java @@ -13,13 +13,13 @@ import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import butterknife.Bind; import butterknife.ButterKnife; import eu.kanade.mangafeed.R; -import eu.kanade.mangafeed.presenter.BasePresenter; +import eu.kanade.mangafeed.presenter.MainPresenter; import eu.kanade.mangafeed.ui.fragment.LibraryFragment; import eu.kanade.mangafeed.ui.fragment.SourceFragment; import nucleus.factory.RequiresPresenter; -@RequiresPresenter(BasePresenter.class) -public class MainActivity extends BaseActivity { +@RequiresPresenter(MainPresenter.class) +public class MainActivity extends BaseActivity { @Bind(R.id.toolbar) Toolbar toolbar; diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/activity/MangaDetailActivity.java b/app/src/main/java/eu/kanade/mangafeed/ui/activity/MangaDetailActivity.java index 64e1b43f9..97333cc7a 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/activity/MangaDetailActivity.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/activity/MangaDetailActivity.java @@ -26,7 +26,7 @@ public class MangaDetailActivity extends BaseActivity { @Bind(R.id.toolbar) Toolbar toolbar; @Bind(R.id.tabs) TabLayout tabs; - @Bind(R.id.viewpager) ViewPager view_pager; + @Bind(R.id.view_pager) ViewPager view_pager; private MangaDetailAdapter adapter; private long manga_id; @@ -80,8 +80,7 @@ public class MangaDetailActivity extends BaseActivity { private void setupViewPager() { adapter = new MangaDetailAdapter( getSupportFragmentManager(), - getActivity(), - manga_id); + getActivity()); view_pager.setAdapter(adapter); tabs.setupWithViewPager(view_pager); @@ -107,19 +106,17 @@ public class MangaDetailActivity extends BaseActivity { final int PAGE_COUNT = 2; private String tab_titles[]; private Context context; - private long manga_id; final static int INFO_FRAGMENT = 0; final static int CHAPTERS_FRAGMENT = 1; - public MangaDetailAdapter(FragmentManager fm, Context context, long manga_id) { + public MangaDetailAdapter(FragmentManager fm, Context context) { super(fm); this.context = context; tab_titles = new String[]{ context.getString(R.string.manga_detail_tab), context.getString(R.string.manga_chapters_tab) }; - this.manga_id = manga_id; } @Override diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/activity/ViewerActivity.java b/app/src/main/java/eu/kanade/mangafeed/ui/activity/ViewerActivity.java new file mode 100644 index 000000000..8c641a9ed --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/ui/activity/ViewerActivity.java @@ -0,0 +1,40 @@ +package eu.kanade.mangafeed.ui.activity; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.view.ViewPager; + +import butterknife.Bind; +import butterknife.ButterKnife; +import eu.kanade.mangafeed.R; +import eu.kanade.mangafeed.presenter.ViewerPresenter; +import eu.kanade.mangafeed.ui.adapter.ViewerPageAdapter; +import nucleus.factory.RequiresPresenter; + +@RequiresPresenter(ViewerPresenter.class) +public class ViewerActivity extends BaseActivity { + + @Bind(R.id.view_pager) ViewPager viewPager; + + private ViewerPageAdapter adapter; + + public static Intent newInstance(Context context) { + return new Intent(context, ViewerActivity.class); + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + setContentView(R.layout.activity_viewer); + ButterKnife.bind(this); + + createAdapter(); + } + + private void createAdapter() { + adapter = new ViewerPageAdapter(getSupportFragmentManager()); + viewPager.setAdapter(adapter); + } + +} diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/adapter/ChapterListHolder.java b/app/src/main/java/eu/kanade/mangafeed/ui/adapter/ChapterListHolder.java index 0157a1261..ee18a9c39 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/adapter/ChapterListHolder.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/adapter/ChapterListHolder.java @@ -20,12 +20,29 @@ public class ChapterListHolder extends ItemViewHolder { @ViewId(R.id.chapter_download_image) ImageView download_icon; + View view; + public ChapterListHolder(View view) { super(view); + this.view = view; } public void onSetValues(Chapter chapter, PositionInfo positionInfo) { title.setText(chapter.name); download_icon.setImageResource(R.drawable.ic_file_download_black_48dp); } + + @Override + public void onSetListeners() { + view.setOnClickListener(view -> { + ChapterListener listener = getListener(ChapterListener.class); + if (listener != null) { + listener.onRowClicked(getItem()); + } + }); + } + + public interface ChapterListener { + void onRowClicked(Chapter chapter); + } } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/adapter/SmartFragmentStatePagerAdapter.java b/app/src/main/java/eu/kanade/mangafeed/ui/adapter/SmartFragmentStatePagerAdapter.java new file mode 100644 index 000000000..38ef584e4 --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/ui/adapter/SmartFragmentStatePagerAdapter.java @@ -0,0 +1,36 @@ +package eu.kanade.mangafeed.ui.adapter; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.util.SparseArray; +import android.view.ViewGroup; + +public abstract class SmartFragmentStatePagerAdapter extends FragmentStatePagerAdapter { + // Sparse array to keep track of registered fragments in memory + private SparseArray registeredFragments = new SparseArray(); + + public SmartFragmentStatePagerAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + } + + // Register the fragment when the item is instantiated + @Override + public Object instantiateItem(ViewGroup container, int position) { + Fragment fragment = (Fragment) super.instantiateItem(container, position); + registeredFragments.put(position, fragment); + return fragment; + } + + // Unregister when the item is inactive + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + registeredFragments.remove(position); + super.destroyItem(container, position, object); + } + + // Returns the fragment for the position (if instantiated) + public Fragment getRegisteredFragment(int position) { + return registeredFragments.get(position); + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/adapter/ViewerPageAdapter.java b/app/src/main/java/eu/kanade/mangafeed/ui/adapter/ViewerPageAdapter.java new file mode 100644 index 000000000..f6e50f232 --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/ui/adapter/ViewerPageAdapter.java @@ -0,0 +1,39 @@ +package eu.kanade.mangafeed.ui.adapter; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; + +import java.util.List; + +import eu.kanade.mangafeed.ui.fragment.ViewerPageFragment; + +public class ViewerPageAdapter extends SmartFragmentStatePagerAdapter { + + private List imageUrls; + + public ViewerPageAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + } + + @Override + public int getCount() { + if (imageUrls != null) + return imageUrls.size(); + + return 0; + } + + @Override + public Fragment getItem(int position) { + return ViewerPageFragment.newInstance(imageUrls.get(position), position); + } + + public List getImageUrls() { + return imageUrls; + } + + public void setImageUrls(List imageUrls) { + this.imageUrls = imageUrls; + } + +} diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/fragment/MangaChaptersFragment.java b/app/src/main/java/eu/kanade/mangafeed/ui/fragment/MangaChaptersFragment.java index 161948c24..d9e210290 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/fragment/MangaChaptersFragment.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/fragment/MangaChaptersFragment.java @@ -1,5 +1,6 @@ package eu.kanade.mangafeed.ui.fragment; +import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.widget.SwipeRefreshLayout; @@ -20,6 +21,7 @@ import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.data.models.Chapter; import eu.kanade.mangafeed.presenter.MangaChaptersPresenter; import eu.kanade.mangafeed.ui.activity.MangaDetailActivity; +import eu.kanade.mangafeed.ui.activity.ViewerActivity; import eu.kanade.mangafeed.ui.adapter.ChapterListHolder; import nucleus.factory.RequiresPresenter; import uk.co.ribot.easyadapter.EasyRecyclerAdapter; @@ -73,7 +75,13 @@ public class MangaChaptersFragment extends BaseFragment } private void createAdapter() { - adapter = new EasyRecyclerAdapter<>(getActivity(), ChapterListHolder.class); + ChapterListHolder.ChapterListener listener = chapter -> { + getPresenter().onChapterClicked(chapter); + Intent intent = ViewerActivity.newInstance(getActivity()); + startActivity(intent); + }; + + adapter = new EasyRecyclerAdapter<>(getActivity(), ChapterListHolder.class, listener); chapters.setAdapter(adapter); } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/fragment/ViewerPageFragment.java b/app/src/main/java/eu/kanade/mangafeed/ui/fragment/ViewerPageFragment.java new file mode 100644 index 000000000..56640932c --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/ui/fragment/ViewerPageFragment.java @@ -0,0 +1,84 @@ +package eu.kanade.mangafeed.ui.fragment; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.bumptech.glide.Glide; +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; + +import eu.kanade.mangafeed.R; +import eu.kanade.mangafeed.util.PageFileTarget; + +public class ViewerPageFragment extends Fragment { + public static final String URL_ARGUMENT_KEY = "UrlArgumentKey"; + + private SubsamplingScaleImageView mPageImageView; + + private String mUrl; + + public static ViewerPageFragment newInstance(String url, int position) { + ViewerPageFragment newInstance = new ViewerPageFragment(); + Bundle arguments = new Bundle(); + arguments.putString(URL_ARGUMENT_KEY, url); + newInstance.setArguments(arguments); + return newInstance; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle arguments = getArguments(); + if (arguments != null) { + if (arguments.containsKey(URL_ARGUMENT_KEY)) { + mUrl = arguments.getString(URL_ARGUMENT_KEY); + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + mPageImageView = (SubsamplingScaleImageView)inflater.inflate(R.layout.fragment_page, container, false); + mPageImageView.setVisibility(View.INVISIBLE); + mPageImageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED); + mPageImageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE); + mPageImageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE); + mPageImageView.setOnImageEventListener(new SubsamplingScaleImageView.OnImageEventListener() { + @Override + public void onReady() { + mPageImageView.setVisibility(View.VISIBLE); + } + + @Override + public void onImageLoaded() { + } + + @Override + public void onPreviewLoadError(Exception e) { + } + + @Override + public void onImageLoadError(Exception e) { + } + + @Override + public void onTileLoadError(Exception e) { + } + }); + + return mPageImageView; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + Glide.with(getActivity()) + .load(mUrl) + .downloadOnly(new PageFileTarget(mPageImageView)); + } +} diff --git a/app/src/main/java/eu/kanade/mangafeed/util/PageFileTarget.java b/app/src/main/java/eu/kanade/mangafeed/util/PageFileTarget.java new file mode 100644 index 000000000..a94a9d45f --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/util/PageFileTarget.java @@ -0,0 +1,36 @@ +package eu.kanade.mangafeed.util; + +import android.graphics.drawable.Drawable; +import android.net.Uri; + +import com.bumptech.glide.request.animation.GlideAnimation; +import com.bumptech.glide.request.target.ViewTarget; +import com.davemorrissey.labs.subscaleview.ImageSource; +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; + +import java.io.File; + +import eu.kanade.mangafeed.R; + +public class PageFileTarget extends ViewTarget { + public static final String TAG = PageFileTarget.class.getSimpleName(); + + public PageFileTarget(SubsamplingScaleImageView view) { + super(view); + } + + @Override + public void onLoadCleared(Drawable placeholder) { + view.setImage(ImageSource.resource(R.drawable.ic_action_refresh)); + } + + @Override + public void onLoadStarted(Drawable placeholder) { + view.setImage(ImageSource.resource(R.drawable.ic_action_refresh)); + } + + @Override + public void onResourceReady(File resource, GlideAnimation glideAnimation) { + view.setImage(ImageSource.uri(Uri.fromFile(resource))); + } +} diff --git a/app/src/main/java/eu/kanade/mangafeed/util/events/SourceChapterEvent.java b/app/src/main/java/eu/kanade/mangafeed/util/events/SourceChapterEvent.java new file mode 100644 index 000000000..c033bdd71 --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/util/events/SourceChapterEvent.java @@ -0,0 +1,23 @@ +package eu.kanade.mangafeed.util.events; + +import eu.kanade.mangafeed.data.models.Chapter; +import eu.kanade.mangafeed.sources.Source; + +public class SourceChapterEvent { + + private Source source; + private Chapter chapter; + + public SourceChapterEvent(Source source, Chapter chapter) { + this.source = source; + this.chapter = chapter; + } + + public Source getSource() { + return source; + } + + public Chapter getChapter() { + return chapter; + } +} diff --git a/app/src/main/res/layout/activity_manga_detail.xml b/app/src/main/res/layout/activity_manga_detail.xml index 3faf4ca94..423ac4f7b 100644 --- a/app/src/main/res/layout/activity_manga_detail.xml +++ b/app/src/main/res/layout/activity_manga_detail.xml @@ -28,7 +28,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_page.xml b/app/src/main/res/layout/fragment_page.xml new file mode 100644 index 000000000..1ae04344b --- /dev/null +++ b/app/src/main/res/layout/fragment_page.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b3d4aa065..adf1611a6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,5 +45,6 @@ Description Info Chapters + ViewerActivity diff --git a/app/src/test/java/eu/kanade/mangafeed/BatotoTest.java b/app/src/test/java/eu/kanade/mangafeed/BatotoTest.java index 42046965b..215627e70 100644 --- a/app/src/test/java/eu/kanade/mangafeed/BatotoTest.java +++ b/app/src/test/java/eu/kanade/mangafeed/BatotoTest.java @@ -19,9 +19,6 @@ import eu.kanade.mangafeed.data.models.Chapter; import eu.kanade.mangafeed.data.models.Manga; import eu.kanade.mangafeed.sources.Batoto; import eu.kanade.mangafeed.sources.Source; -import rx.android.schedulers.AndroidSchedulers; -import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; @Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP) @RunWith(RobolectricGradleTestRunner.class) @@ -44,7 +41,7 @@ public class BatotoTest { @Test public void testImageList() { - List imageUrls = b.getImageUrlsFromNetwork(chapterUrl) + List imageUrls = b.getRemainingImageUrlsFromPageList(chapterUrl) .toList().toBlocking().single(); Assert.assertTrue(imageUrls.size() > 5); diff --git a/app/src/test/java/eu/kanade/mangafeed/MangahereTest.java b/app/src/test/java/eu/kanade/mangafeed/MangahereTest.java index c494a8c04..3d6097075 100644 --- a/app/src/test/java/eu/kanade/mangafeed/MangahereTest.java +++ b/app/src/test/java/eu/kanade/mangafeed/MangahereTest.java @@ -39,7 +39,7 @@ public class MangahereTest { @Test public void testImageList() { - List imageUrls = b.getImageUrlsFromNetwork(chapterUrl) + List imageUrls = b.getRemainingImageUrlsFromPageList(chapterUrl) .toList().toBlocking().single(); Assert.assertTrue(imageUrls.size() > 5);