mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-21 20:47:03 -05:00
Almost showing a chapter reader
This commit is contained in:
parent
49c69be38e
commit
5142df103b
28 changed files with 580 additions and 119 deletions
|
@ -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"
|
||||
|
||||
|
|
|
@ -37,6 +37,15 @@
|
|||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="eu.kanade.mangafeed.ui.activity.MainActivity" />
|
||||
</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>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<String> getImageUrlsFromDiskCache(final String chapterUrl) {
|
||||
public Observable<List<Page>> getPageUrlsFromDiskCache(final String chapterUrl) {
|
||||
return Observable.create(subscriber -> {
|
||||
try {
|
||||
String[] imageUrls = getImageUrlsFromDiskCacheImpl(chapterUrl);
|
||||
|
||||
for (String imageUrl : imageUrls) {
|
||||
if (!subscriber.isUnsubscribed()) {
|
||||
subscriber.onNext(imageUrl);
|
||||
}
|
||||
}
|
||||
List<Page> 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<Page> getPageUrlsFromDiskCacheImpl(String chapterUrl) throws IOException {
|
||||
DiskLruCache.Snapshot snapshot = null;
|
||||
List<Page> pages = null;
|
||||
|
||||
try {
|
||||
String key = DiskUtils.hashKeyForDisk(chapterUrl);
|
||||
|
||||
snapshot = mDiskCache.get(key);
|
||||
|
||||
String joinedImageUrls = snapshot.getString(0);
|
||||
return joinedImageUrls.split(",");
|
||||
Type collectionType = new TypeToken<List<Page>>() {}.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<String> imageUrls) {
|
||||
return () -> {
|
||||
try {
|
||||
putImageUrlsToDiskCacheImpl(chapterUrl, imageUrls);
|
||||
} catch (IOException e) {
|
||||
// Do Nothing.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void putImageUrlsToDiskCacheImpl(String chapterUrl, List<String> imageUrls) throws IOException {
|
||||
String cachedValue = joinImageUrlsToCacheValue(imageUrls);
|
||||
public void putPageUrlsToDiskCache(final String chapterUrl, final List<Page> 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<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() {
|
||||
return mDiskCache.getDirectory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
43
app/src/main/java/eu/kanade/mangafeed/data/models/Page.java
Normal file
43
app/src/main/java/eu/kanade/mangafeed/data/models/Page.java
Normal 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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package eu.kanade.mangafeed.presenter;
|
||||
|
||||
import eu.kanade.mangafeed.ui.activity.MainActivity;
|
||||
|
||||
public class MainPresenter extends BasePresenter<MainActivity> {
|
||||
|
||||
}
|
|
@ -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<MangaChaptersFragment> {
|
||||
|
||||
|
@ -27,6 +28,7 @@ public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment>
|
|||
@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<MangaChaptersFragment>
|
|||
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<MangaChaptersFragment>
|
|||
}
|
||||
|
||||
private Observable<PostResult> 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<MangaInfoFragment> {
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<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 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<List<Manga>> 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<String> getImageUrlsFromNetwork(final String chapterUrl) {
|
||||
public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) {
|
||||
return mCacheManager.getPageUrlsFromDiskCache(chapterUrl)
|
||||
.onErrorResumeNext(throwable -> {
|
||||
return mNetworkService
|
||||
.getStringResponse(chapterUrl, mNetworkService.NULL_CACHE_CONTROL, mRequestHeaders)
|
||||
.flatMap(unparsedHtml -> Observable.from(parseHtmlToPageUrls(unparsedHtml)))
|
||||
.buffer(3)
|
||||
.concatMap(batchedPageUrls -> {
|
||||
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));
|
||||
.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<Page> getRemainingImageUrlsFromPageList(final List<Page> pages) {
|
||||
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();
|
||||
public abstract int getSourceId();
|
||||
private Observable<Page> 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<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);
|
||||
public void savePageList(String chapterUrl, List<Page> pages) {
|
||||
mCacheManager.putPageUrlsToDiskCache(chapterUrl, pages);
|
||||
}
|
||||
|
||||
private Observable<List<Page>> convertToPages(List<String> pageUrls) {
|
||||
List<Page> pages = new ArrayList<>();
|
||||
for (int i = 0; i < pageUrls.size(); i++) {
|
||||
pages.add(new Page(i, pageUrls.get(i)));
|
||||
}
|
||||
return Observable.just(pages);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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<P extends Presenter> extends NucleusAppCompatActivity<P> {
|
||||
|
||||
|
@ -17,11 +16,7 @@ public class BaseActivity<P extends Presenter> extends NucleusAppCompatActivity<
|
|||
final PresenterFactory<P> 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());
|
||||
}
|
||||
return presenter;
|
||||
});
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
|
@ -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<BasePresenter> {
|
||||
@RequiresPresenter(MainPresenter.class)
|
||||
public class MainActivity extends BaseActivity<MainPresenter> {
|
||||
|
||||
@Bind(R.id.toolbar)
|
||||
Toolbar toolbar;
|
||||
|
|
|
@ -26,7 +26,7 @@ public class MangaDetailActivity extends BaseActivity<MangaDetailPresenter> {
|
|||
|
||||
@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<MangaDetailPresenter> {
|
|||
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<MangaDetailPresenter> {
|
|||
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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -20,12 +20,29 @@ public class ChapterListHolder extends ItemViewHolder<Chapter> {
|
|||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<MangaChaptersPresenter>
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@
|
|||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
<android.support.v4.view.ViewPager
|
||||
android:id="@+id/viewpager"
|
||||
android:id="@+id/view_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0px"
|
||||
android:layout_weight="1"
|
||||
|
|
13
app/src/main/res/layout/activity_viewer.xml
Normal file
13
app/src/main/res/layout/activity_viewer.xml
Normal 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>
|
6
app/src/main/res/layout/fragment_page.xml
Normal file
6
app/src/main/res/layout/fragment_page.xml
Normal 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" />
|
|
@ -45,5 +45,6 @@
|
|||
<string name="description">Description</string>
|
||||
<string name="manga_detail_tab">Info</string>
|
||||
<string name="manga_chapters_tab">Chapters</string>
|
||||
<string name="title_activity_viewer">ViewerActivity</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -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<String> imageUrls = b.getImageUrlsFromNetwork(chapterUrl)
|
||||
List<String> imageUrls = b.getRemainingImageUrlsFromPageList(chapterUrl)
|
||||
.toList().toBlocking().single();
|
||||
|
||||
Assert.assertTrue(imageUrls.size() > 5);
|
||||
|
|
|
@ -39,7 +39,7 @@ public class MangahereTest {
|
|||
|
||||
@Test
|
||||
public void testImageList() {
|
||||
List<String> imageUrls = b.getImageUrlsFromNetwork(chapterUrl)
|
||||
List<String> imageUrls = b.getRemainingImageUrlsFromPageList(chapterUrl)
|
||||
.toList().toBlocking().single();
|
||||
|
||||
Assert.assertTrue(imageUrls.size() > 5);
|
||||
|
|
Loading…
Reference in a new issue