Almost showing a chapter reader

This commit is contained in:
inorichi 2015-10-21 00:04:04 +02:00
parent 49c69be38e
commit 5142df103b
28 changed files with 580 additions and 119 deletions

View File

@ -62,6 +62,7 @@ dependencies {
compile 'com.squareup.okhttp:okhttp-urlconnection:2.4.0' compile 'com.squareup.okhttp:okhttp-urlconnection:2.4.0'
compile 'com.squareup.okhttp:okhttp:2.4.0' compile 'com.squareup.okhttp:okhttp:2.4.0'
compile 'com.squareup.okio:okio:1.6.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 'com.jakewharton:disklrucache:2.0.2'
compile 'org.jsoup:jsoup:1.8.3' compile 'org.jsoup:jsoup:1.8.3'
compile 'io.reactivex:rxandroid:1.0.1' compile 'io.reactivex:rxandroid:1.0.1'
@ -76,6 +77,7 @@ dependencies {
compile 'com.jakewharton.timber:timber:3.1.0' compile 'com.jakewharton.timber:timber:3.1.0'
compile 'uk.co.ribot:easyadapter:1.5.0@aar' compile 'uk.co.ribot:easyadapter:1.5.0@aar'
compile 'ch.acra:acra:4.6.2' compile 'ch.acra:acra:4.6.2'
compile 'com.davemorrissey.labs:subsampling-scale-image-view:3.4.1'
compile "frankiesardo:icepick:$ICEPICK_VERSION" compile "frankiesardo:icepick:$ICEPICK_VERSION"
provided "frankiesardo:icepick-processor:$ICEPICK_VERSION" provided "frankiesardo:icepick-processor:$ICEPICK_VERSION"

View File

@ -37,6 +37,15 @@
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="eu.kanade.mangafeed.ui.activity.MainActivity" /> android:value="eu.kanade.mangafeed.ui.activity.MainActivity" />
</activity> </activity>
<activity
android:name=".ui.activity.ViewerActivity"
android:label="@string/title_activity_viewer"
android:parentActivityName=".ui.activity.MangaDetailActivity"
android:theme="@style/AppTheme" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="eu.kanade.mangafeed.ui.activity.MangaDetailActivity" />
</activity>
</application> </application>
</manifest> </manifest>

View File

@ -8,10 +8,12 @@ import dagger.Component;
import eu.kanade.mangafeed.data.DataModule; import eu.kanade.mangafeed.data.DataModule;
import eu.kanade.mangafeed.presenter.CataloguePresenter; import eu.kanade.mangafeed.presenter.CataloguePresenter;
import eu.kanade.mangafeed.presenter.LibraryPresenter; import eu.kanade.mangafeed.presenter.LibraryPresenter;
import eu.kanade.mangafeed.presenter.MainPresenter;
import eu.kanade.mangafeed.presenter.MangaChaptersPresenter; import eu.kanade.mangafeed.presenter.MangaChaptersPresenter;
import eu.kanade.mangafeed.presenter.MangaDetailPresenter; import eu.kanade.mangafeed.presenter.MangaDetailPresenter;
import eu.kanade.mangafeed.presenter.MangaInfoPresenter; import eu.kanade.mangafeed.presenter.MangaInfoPresenter;
import eu.kanade.mangafeed.presenter.SourcePresenter; import eu.kanade.mangafeed.presenter.SourcePresenter;
import eu.kanade.mangafeed.presenter.ViewerPresenter;
@Singleton @Singleton
@Component( @Component(
@ -22,12 +24,14 @@ import eu.kanade.mangafeed.presenter.SourcePresenter;
) )
public interface AppComponent { public interface AppComponent {
void inject(MainPresenter mainPresenter);
void inject(LibraryPresenter libraryPresenter); void inject(LibraryPresenter libraryPresenter);
void inject(MangaDetailPresenter mangaDetailPresenter); void inject(MangaDetailPresenter mangaDetailPresenter);
void inject(SourcePresenter sourcePresenter); void inject(SourcePresenter sourcePresenter);
void inject(CataloguePresenter cataloguePresenter); void inject(CataloguePresenter cataloguePresenter);
void inject(MangaInfoPresenter mangaInfoPresenter); void inject(MangaInfoPresenter mangaInfoPresenter);
void inject(MangaChaptersPresenter mangaChaptersPresenter); void inject(MangaChaptersPresenter mangaChaptersPresenter);
void inject(ViewerPresenter viewerPresenter);
Application application(); Application application();

View File

@ -5,20 +5,23 @@ import android.content.Context;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.request.FutureTarget; import com.bumptech.glide.request.FutureTarget;
import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.target.Target;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.jakewharton.disklrucache.DiskLruCache; import com.jakewharton.disklrucache.DiskLruCache;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.lang.reflect.Type;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import eu.kanade.mangafeed.data.models.Page;
import eu.kanade.mangafeed.util.DiskUtils; import eu.kanade.mangafeed.util.DiskUtils;
import rx.Observable; import rx.Observable;
import rx.functions.Action0;
public class CacheManager { public class CacheManager {
@ -29,11 +32,13 @@ public class CacheManager {
private static final int READ_TIMEOUT = 60; private static final int READ_TIMEOUT = 60;
private Context mContext; private Context mContext;
private Gson mGson;
private DiskLruCache mDiskCache; private DiskLruCache mDiskCache;
public CacheManager(Context context) { public CacheManager(Context context) {
mContext = context; mContext = context;
mGson = new Gson();
try { try {
mDiskCache = DiskLruCache.open( mDiskCache = DiskLruCache.open(
@ -109,16 +114,11 @@ public class CacheManager {
return isSuccessful; return isSuccessful;
} }
public Observable<String> getImageUrlsFromDiskCache(final String chapterUrl) { public Observable<List<Page>> getPageUrlsFromDiskCache(final String chapterUrl) {
return Observable.create(subscriber -> { return Observable.create(subscriber -> {
try { try {
String[] imageUrls = getImageUrlsFromDiskCacheImpl(chapterUrl); List<Page> pages = getPageUrlsFromDiskCacheImpl(chapterUrl);
subscriber.onNext(pages);
for (String imageUrl : imageUrls) {
if (!subscriber.isUnsubscribed()) {
subscriber.onNext(imageUrl);
}
}
subscriber.onCompleted(); subscriber.onCompleted();
} catch (Throwable e) { } catch (Throwable e) {
subscriber.onError(e); subscriber.onError(e);
@ -126,35 +126,28 @@ public class CacheManager {
}); });
} }
private String[] getImageUrlsFromDiskCacheImpl(String chapterUrl) throws IOException { private List<Page> getPageUrlsFromDiskCacheImpl(String chapterUrl) throws IOException {
DiskLruCache.Snapshot snapshot = null; DiskLruCache.Snapshot snapshot = null;
List<Page> pages = null;
try { try {
String key = DiskUtils.hashKeyForDisk(chapterUrl); String key = DiskUtils.hashKeyForDisk(chapterUrl);
snapshot = mDiskCache.get(key); snapshot = mDiskCache.get(key);
String joinedImageUrls = snapshot.getString(0); Type collectionType = new TypeToken<List<Page>>() {}.getType();
return joinedImageUrls.split(","); pages = mGson.fromJson(snapshot.getString(0), collectionType);
} catch (IOException e) {
// Do Nothing.
} finally { } finally {
if (snapshot != null) { if (snapshot != null) {
snapshot.close(); snapshot.close();
} }
} }
return pages;
} }
public Action0 putImageUrlsToDiskCache(final String chapterUrl, final List<String> imageUrls) { public void putPageUrlsToDiskCache(final String chapterUrl, final List<Page> pages) {
return () -> { String cachedValue = mGson.toJson(pages);
try {
putImageUrlsToDiskCacheImpl(chapterUrl, imageUrls);
} catch (IOException e) {
// Do Nothing.
}
};
}
private void putImageUrlsToDiskCacheImpl(String chapterUrl, List<String> imageUrls) throws IOException {
String cachedValue = joinImageUrlsToCacheValue(imageUrls);
DiskLruCache.Editor editor = null; DiskLruCache.Editor editor = null;
OutputStream outputStream = null; OutputStream outputStream = null;
@ -171,13 +164,11 @@ public class CacheManager {
mDiskCache.flush(); mDiskCache.flush();
editor.commit(); editor.commit();
} catch (Exception e) {
// Do Nothing.
} finally { } finally {
if (editor != null) { if (editor != null) {
try { editor.abortUnlessCommitted();
editor.abort();
} catch (IOException ignore) {
// Do Nothing.
}
} }
if (outputStream != null) { if (outputStream != null) {
try { try {
@ -189,22 +180,9 @@ public class CacheManager {
} }
} }
private String joinImageUrlsToCacheValue(List<String> 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() { public File getCacheDir() {
return mDiskCache.getDirectory(); return mDiskCache.getDirectory();
} }
} }

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -0,0 +1,7 @@
package eu.kanade.mangafeed.presenter;
import eu.kanade.mangafeed.ui.activity.MainActivity;
public class MainPresenter extends BasePresenter<MainActivity> {
}

View File

@ -13,13 +13,14 @@ import eu.kanade.mangafeed.data.helpers.DatabaseHelper;
import eu.kanade.mangafeed.data.helpers.SourceManager; import eu.kanade.mangafeed.data.helpers.SourceManager;
import eu.kanade.mangafeed.data.models.Chapter; import eu.kanade.mangafeed.data.models.Chapter;
import eu.kanade.mangafeed.data.models.Manga; import eu.kanade.mangafeed.data.models.Manga;
import eu.kanade.mangafeed.sources.Source;
import eu.kanade.mangafeed.ui.fragment.MangaChaptersFragment; import eu.kanade.mangafeed.ui.fragment.MangaChaptersFragment;
import eu.kanade.mangafeed.util.EventBusHook; import eu.kanade.mangafeed.util.EventBusHook;
import eu.kanade.mangafeed.util.events.ChapterCountEvent; import eu.kanade.mangafeed.util.events.ChapterCountEvent;
import eu.kanade.mangafeed.util.events.SourceChapterEvent;
import rx.Observable; import rx.Observable;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers; import rx.schedulers.Schedulers;
import timber.log.Timber;
public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment> { public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment> {
@ -27,6 +28,7 @@ public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment>
@Inject SourceManager sourceManager; @Inject SourceManager sourceManager;
private Manga manga; private Manga manga;
private Source source;
private static final int DB_CHAPTERS = 1; private static final int DB_CHAPTERS = 1;
private static final int ONLINE_CHAPTERS = 2; private static final int ONLINE_CHAPTERS = 2;
@ -71,6 +73,7 @@ public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment>
public void onEventMainThread(Manga manga) { public void onEventMainThread(Manga manga) {
if (this.manga == null) { if (this.manga == null) {
this.manga = manga; this.manga = manga;
source = sourceManager.get(manga.source);
start(DB_CHAPTERS); start(DB_CHAPTERS);
// Get chapters if it's an online source // Get chapters if it's an online source
@ -94,11 +97,14 @@ public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment>
} }
private Observable<PostResult> getOnlineChaptersObs() { private Observable<PostResult> getOnlineChaptersObs() {
return sourceManager.get(manga.source) return source
.pullChaptersFromNetwork(manga.url) .pullChaptersFromNetwork(manga.url)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters)) .flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters))
.observeOn(AndroidSchedulers.mainThread()); .observeOn(AndroidSchedulers.mainThread());
} }
public void onChapterClicked(Chapter chapter) {
EventBus.getDefault().postSticky(new SourceChapterEvent(source, chapter));
}
} }

View File

@ -10,7 +10,6 @@ import eu.kanade.mangafeed.ui.fragment.MangaInfoFragment;
import eu.kanade.mangafeed.util.EventBusHook; import eu.kanade.mangafeed.util.EventBusHook;
import eu.kanade.mangafeed.util.events.ChapterCountEvent; import eu.kanade.mangafeed.util.events.ChapterCountEvent;
import rx.Observable; import rx.Observable;
import timber.log.Timber;
public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> { public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {

View File

@ -3,7 +3,6 @@ package eu.kanade.mangafeed.presenter;
import javax.inject.Inject; import javax.inject.Inject;
import eu.kanade.mangafeed.data.helpers.SourceManager; import eu.kanade.mangafeed.data.helpers.SourceManager;
import eu.kanade.mangafeed.sources.Source;
import eu.kanade.mangafeed.ui.fragment.SourceFragment; import eu.kanade.mangafeed.ui.fragment.SourceFragment;

View File

@ -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<ViewerActivity> {
private static final int GET_PAGE_LIST = 1;
private Source source;
private Chapter chapter;
private List<Page> 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<Page> 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;
}
}
}
}

View File

@ -10,11 +10,49 @@ import eu.kanade.mangafeed.data.caches.CacheManager;
import eu.kanade.mangafeed.data.helpers.NetworkHelper; import eu.kanade.mangafeed.data.helpers.NetworkHelper;
import eu.kanade.mangafeed.data.models.Chapter; import eu.kanade.mangafeed.data.models.Chapter;
import eu.kanade.mangafeed.data.models.Manga; import eu.kanade.mangafeed.data.models.Manga;
import eu.kanade.mangafeed.data.models.Page;
import rx.Observable; import rx.Observable;
import rx.schedulers.Schedulers; import rx.schedulers.Schedulers;
public abstract class Source { 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<Manga> parsePopularMangasFromHtml(String unparsedHtml);
protected abstract List<Manga> parseSearchFromHtml(String unparsedHtml);
protected abstract Manga parseHtmlToManga(String mangaUrl, String unparsedHtml);
protected abstract List<Chapter> parseHtmlToChapters(String unparsedHtml);
protected abstract List<String> 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 NetworkHelper mNetworkService;
protected CacheManager mCacheManager; protected CacheManager mCacheManager;
protected Headers mRequestHeaders; protected Headers mRequestHeaders;
@ -25,13 +63,6 @@ public abstract class Source {
mRequestHeaders = headersBuilder().build(); 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 // Get the most popular mangas from the source
public Observable<List<Manga>> pullPopularMangasFromNetwork(int page) { public Observable<List<Manga>> pullPopularMangasFromNetwork(int page) {
String url = getUrlFromPageNumber(page); String url = getUrlFromPageNumber(page);
@ -62,56 +93,54 @@ public abstract class Source {
Observable.just(parseHtmlToChapters(unparsedHtml))); Observable.just(parseHtmlToChapters(unparsedHtml)));
} }
// Get the URLs of the images of a chapter public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) {
public Observable<String> getImageUrlsFromNetwork(final String chapterUrl) { return mCacheManager.getPageUrlsFromDiskCache(chapterUrl)
.onErrorResumeNext(throwable -> {
return mNetworkService return mNetworkService
.getStringResponse(chapterUrl, mNetworkService.NULL_CACHE_CONTROL, mRequestHeaders) .getStringResponse(chapterUrl, mNetworkService.NULL_CACHE_CONTROL, mRequestHeaders)
.flatMap(unparsedHtml -> Observable.from(parseHtmlToPageUrls(unparsedHtml))) .flatMap(unparsedHtml -> Observable.just(parseHtmlToPageUrls(unparsedHtml)))
.buffer(3) .flatMap(this::convertToPages)
.concatMap(batchedPageUrls -> { .doOnNext(pages -> savePageList(chapterUrl, pages));
List<Observable<String>> imageUrlObservables = new ArrayList<>();
for (String pageUrl : batchedPageUrls) {
Observable<String> 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<String> pullImageUrlsFromNetwork(final String chapterUrl) {
final List<String> temporaryCachedImageUrls = new ArrayList<>();
return mCacheManager.getImageUrlsFromDiskCache(chapterUrl)
.onErrorResumeNext(throwable -> {
return getImageUrlsFromNetwork(chapterUrl)
.doOnNext(imageUrl -> temporaryCachedImageUrls.add(imageUrl))
.doOnCompleted(mCacheManager.putImageUrlsToDiskCache(chapterUrl, temporaryCachedImageUrls));
}) })
.onBackpressureBuffer(); .onBackpressureBuffer();
} }
// Get the URL to the details of a manga, useful if the source provides some kind of API or fast calls // Get the URLs of the images of a chapter
protected String getMangaUrl(String defaultMangaUrl) { public Observable<Page> getRemainingImageUrlsFromPageList(final List<Page> pages) {
return defaultMangaUrl; return Observable.from(pages)
.filter(page -> page.getImageUrl() == null)
.buffer(getNumberOfConcurrentImageDownloads())
.concatMap(batchedPages -> {
List<Observable<Page>> pageObservable = new ArrayList<>();
for (Page page : batchedPages) {
pageObservable.add(getImageUrlFromPage(page));
}
return Observable.merge(pageObservable);
});
} }
public abstract String getName(); private Observable<Page> getImageUrlFromPage(final Page page) {
public abstract int getSourceId(); 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); public void savePageList(String chapterUrl, List<Page> pages) {
protected abstract String getSearchUrl(String query, int page); mCacheManager.putPageUrlsToDiskCache(chapterUrl, pages);
protected abstract List<Manga> parsePopularMangasFromHtml(String unparsedHtml); }
protected abstract List<Manga> parseSearchFromHtml(String unparsedHtml);
protected abstract Manga parseHtmlToManga(String mangaUrl, String unparsedHtml); private Observable<List<Page>> convertToPages(List<String> pageUrls) {
protected abstract List<Chapter> parseHtmlToChapters(String unparsedHtml); List<Page> pages = new ArrayList<>();
protected abstract List<String> parseHtmlToPageUrls(String unparsedHtml); for (int i = 0; i < pageUrls.size(); i++) {
protected abstract String parseHtmlToImageUrl(String unparsedHtml); pages.add(new Page(i, pageUrls.get(i)));
}
return Observable.just(pages);
}
} }

View File

@ -8,7 +8,6 @@ import eu.kanade.mangafeed.App;
import nucleus.factory.PresenterFactory; import nucleus.factory.PresenterFactory;
import nucleus.presenter.Presenter; import nucleus.presenter.Presenter;
import nucleus.view.NucleusAppCompatActivity; import nucleus.view.NucleusAppCompatActivity;
import timber.log.Timber;
public class BaseActivity<P extends Presenter> extends NucleusAppCompatActivity<P> { public class BaseActivity<P extends Presenter> extends NucleusAppCompatActivity<P> {
@ -17,11 +16,7 @@ public class BaseActivity<P extends Presenter> extends NucleusAppCompatActivity<
final PresenterFactory<P> superFactory = super.getPresenterFactory(); final PresenterFactory<P> superFactory = super.getPresenterFactory();
setPresenterFactory(() -> { setPresenterFactory(() -> {
P presenter = superFactory.createPresenter(); P presenter = superFactory.createPresenter();
try {
App.getComponentReflection(getActivity()).inject(presenter); App.getComponentReflection(getActivity()).inject(presenter);
} catch(Exception e) {
Timber.w("No injection for " + presenter.getClass().toString());
}
return presenter; return presenter;
}); });
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);

View File

@ -13,13 +13,13 @@ import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import eu.kanade.mangafeed.R; 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.LibraryFragment;
import eu.kanade.mangafeed.ui.fragment.SourceFragment; import eu.kanade.mangafeed.ui.fragment.SourceFragment;
import nucleus.factory.RequiresPresenter; import nucleus.factory.RequiresPresenter;
@RequiresPresenter(BasePresenter.class) @RequiresPresenter(MainPresenter.class)
public class MainActivity extends BaseActivity<BasePresenter> { public class MainActivity extends BaseActivity<MainPresenter> {
@Bind(R.id.toolbar) @Bind(R.id.toolbar)
Toolbar toolbar; Toolbar toolbar;

View File

@ -26,7 +26,7 @@ public class MangaDetailActivity extends BaseActivity<MangaDetailPresenter> {
@Bind(R.id.toolbar) Toolbar toolbar; @Bind(R.id.toolbar) Toolbar toolbar;
@Bind(R.id.tabs) TabLayout tabs; @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 MangaDetailAdapter adapter;
private long manga_id; private long manga_id;
@ -80,8 +80,7 @@ public class MangaDetailActivity extends BaseActivity<MangaDetailPresenter> {
private void setupViewPager() { private void setupViewPager() {
adapter = new MangaDetailAdapter( adapter = new MangaDetailAdapter(
getSupportFragmentManager(), getSupportFragmentManager(),
getActivity(), getActivity());
manga_id);
view_pager.setAdapter(adapter); view_pager.setAdapter(adapter);
tabs.setupWithViewPager(view_pager); tabs.setupWithViewPager(view_pager);
@ -107,19 +106,17 @@ public class MangaDetailActivity extends BaseActivity<MangaDetailPresenter> {
final int PAGE_COUNT = 2; final int PAGE_COUNT = 2;
private String tab_titles[]; private String tab_titles[];
private Context context; private Context context;
private long manga_id;
final static int INFO_FRAGMENT = 0; final static int INFO_FRAGMENT = 0;
final static int CHAPTERS_FRAGMENT = 1; final static int CHAPTERS_FRAGMENT = 1;
public MangaDetailAdapter(FragmentManager fm, Context context, long manga_id) { public MangaDetailAdapter(FragmentManager fm, Context context) {
super(fm); super(fm);
this.context = context; this.context = context;
tab_titles = new String[]{ tab_titles = new String[]{
context.getString(R.string.manga_detail_tab), context.getString(R.string.manga_detail_tab),
context.getString(R.string.manga_chapters_tab) context.getString(R.string.manga_chapters_tab)
}; };
this.manga_id = manga_id;
} }
@Override @Override

View File

@ -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<ViewerPresenter> {
@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);
}
}

View File

@ -20,12 +20,29 @@ public class ChapterListHolder extends ItemViewHolder<Chapter> {
@ViewId(R.id.chapter_download_image) @ViewId(R.id.chapter_download_image)
ImageView download_icon; ImageView download_icon;
View view;
public ChapterListHolder(View view) { public ChapterListHolder(View view) {
super(view); super(view);
this.view = view;
} }
public void onSetValues(Chapter chapter, PositionInfo positionInfo) { public void onSetValues(Chapter chapter, PositionInfo positionInfo) {
title.setText(chapter.name); title.setText(chapter.name);
download_icon.setImageResource(R.drawable.ic_file_download_black_48dp); 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);
}
} }

View File

@ -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<Fragment> registeredFragments = new SparseArray<Fragment>();
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);
}
}

View File

@ -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<String> 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<String> getImageUrls() {
return imageUrls;
}
public void setImageUrls(List<String> imageUrls) {
this.imageUrls = imageUrls;
}
}

View File

@ -1,5 +1,6 @@
package eu.kanade.mangafeed.ui.fragment; package eu.kanade.mangafeed.ui.fragment;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.widget.SwipeRefreshLayout; 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.data.models.Chapter;
import eu.kanade.mangafeed.presenter.MangaChaptersPresenter; import eu.kanade.mangafeed.presenter.MangaChaptersPresenter;
import eu.kanade.mangafeed.ui.activity.MangaDetailActivity; import eu.kanade.mangafeed.ui.activity.MangaDetailActivity;
import eu.kanade.mangafeed.ui.activity.ViewerActivity;
import eu.kanade.mangafeed.ui.adapter.ChapterListHolder; import eu.kanade.mangafeed.ui.adapter.ChapterListHolder;
import nucleus.factory.RequiresPresenter; import nucleus.factory.RequiresPresenter;
import uk.co.ribot.easyadapter.EasyRecyclerAdapter; import uk.co.ribot.easyadapter.EasyRecyclerAdapter;
@ -73,7 +75,13 @@ public class MangaChaptersFragment extends BaseFragment<MangaChaptersPresenter>
} }
private void createAdapter() { 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); chapters.setAdapter(adapter);
} }

View File

@ -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));
}
}

View File

@ -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<SubsamplingScaleImageView, File> {
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<? super File> glideAnimation) {
view.setImage(ImageSource.uri(Uri.fromFile(resource)));
}
}

View File

@ -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;
}
}

View File

@ -28,7 +28,7 @@
</android.support.design.widget.AppBarLayout> </android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager <android.support.v4.view.ViewPager
android:id="@+id/viewpager" android:id="@+id/view_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0px" android:layout_height="0px"
android:layout_weight="1" android:layout_weight="1"

View File

@ -0,0 +1,13 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</android.support.v4.view.ViewPager>
</FrameLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/page_image_view" />

View File

@ -45,5 +45,6 @@
<string name="description">Description</string> <string name="description">Description</string>
<string name="manga_detail_tab">Info</string> <string name="manga_detail_tab">Info</string>
<string name="manga_chapters_tab">Chapters</string> <string name="manga_chapters_tab">Chapters</string>
<string name="title_activity_viewer">ViewerActivity</string>
</resources> </resources>

View File

@ -19,9 +19,6 @@ import eu.kanade.mangafeed.data.models.Chapter;
import eu.kanade.mangafeed.data.models.Manga; import eu.kanade.mangafeed.data.models.Manga;
import eu.kanade.mangafeed.sources.Batoto; import eu.kanade.mangafeed.sources.Batoto;
import eu.kanade.mangafeed.sources.Source; 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) @Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
@RunWith(RobolectricGradleTestRunner.class) @RunWith(RobolectricGradleTestRunner.class)
@ -44,7 +41,7 @@ public class BatotoTest {
@Test @Test
public void testImageList() { public void testImageList() {
List<String> imageUrls = b.getImageUrlsFromNetwork(chapterUrl) List<String> imageUrls = b.getRemainingImageUrlsFromPageList(chapterUrl)
.toList().toBlocking().single(); .toList().toBlocking().single();
Assert.assertTrue(imageUrls.size() > 5); Assert.assertTrue(imageUrls.size() > 5);

View File

@ -39,7 +39,7 @@ public class MangahereTest {
@Test @Test
public void testImageList() { public void testImageList() {
List<String> imageUrls = b.getImageUrlsFromNetwork(chapterUrl) List<String> imageUrls = b.getRemainingImageUrlsFromPageList(chapterUrl)
.toList().toBlocking().single(); .toList().toBlocking().single();
Assert.assertTrue(imageUrls.size() > 5); Assert.assertTrue(imageUrls.size() > 5);