mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-30 13:57:57 +01:00
Rename project
This commit is contained in:
57
app/src/main/java/eu/kanade/tachiyomi/App.java
Normal file
57
app/src/main/java/eu/kanade/tachiyomi/App.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package eu.kanade.tachiyomi;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.annotation.ReportsCrashes;
|
||||
|
||||
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector;
|
||||
import eu.kanade.tachiyomi.injection.component.AppComponent;
|
||||
import eu.kanade.tachiyomi.injection.component.DaggerAppComponent;
|
||||
import eu.kanade.tachiyomi.injection.module.AppModule;
|
||||
import timber.log.Timber;
|
||||
|
||||
@ReportsCrashes(
|
||||
formUri = "http://mangafeed.kanade.eu/crash_report",
|
||||
reportType = org.acra.sender.HttpSender.Type.JSON,
|
||||
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
||||
excludeMatchingSharedPreferencesKeys={".*username.*",".*password.*"}
|
||||
)
|
||||
public class App extends Application {
|
||||
|
||||
AppComponent applicationComponent;
|
||||
ComponentReflectionInjector<AppComponent> componentInjector;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree());
|
||||
|
||||
applicationComponent = DaggerAppComponent.builder()
|
||||
.appModule(new AppModule(this))
|
||||
.build();
|
||||
|
||||
componentInjector =
|
||||
new ComponentReflectionInjector<>(AppComponent.class, applicationComponent);
|
||||
|
||||
ACRA.init(this);
|
||||
}
|
||||
|
||||
public static App get(Context context) {
|
||||
return (App) context.getApplicationContext();
|
||||
}
|
||||
|
||||
public AppComponent getComponent() {
|
||||
return applicationComponent;
|
||||
}
|
||||
|
||||
public ComponentReflectionInjector<AppComponent> getComponentReflection() {
|
||||
return componentInjector;
|
||||
}
|
||||
|
||||
// Needed to replace the component with a test specific one
|
||||
public void setComponent(AppComponent applicationComponent) {
|
||||
this.applicationComponent = applicationComponent;
|
||||
}
|
||||
}
|
||||
197
app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.java
vendored
Normal file
197
app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.java
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
package eu.kanade.tachiyomi.data.cache;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.Formatter;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.jakewharton.disklrucache.DiskLruCache;
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
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 eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.util.DiskUtils;
|
||||
import okio.BufferedSink;
|
||||
import okio.Okio;
|
||||
import rx.Observable;
|
||||
|
||||
public class ChapterCache {
|
||||
|
||||
private static final String PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache";
|
||||
private static final int PARAMETER_APP_VERSION = 1;
|
||||
private static final int PARAMETER_VALUE_COUNT = 1;
|
||||
private static final int PARAMETER_CACHE_SIZE = 75 * 1024 * 1024;
|
||||
|
||||
private Context context;
|
||||
private Gson gson;
|
||||
|
||||
private DiskLruCache diskCache;
|
||||
|
||||
public ChapterCache(Context context) {
|
||||
this.context = context;
|
||||
gson = new Gson();
|
||||
|
||||
try {
|
||||
diskCache = DiskLruCache.open(
|
||||
new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY),
|
||||
PARAMETER_APP_VERSION,
|
||||
PARAMETER_VALUE_COUNT,
|
||||
PARAMETER_CACHE_SIZE
|
||||
);
|
||||
} catch (IOException e) {
|
||||
// Do Nothing.
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(String file) {
|
||||
if (file.equals("journal") || file.startsWith("journal."))
|
||||
return false;
|
||||
|
||||
try {
|
||||
String key = file.substring(0, file.lastIndexOf("."));
|
||||
return diskCache.remove(key);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public File getCacheDir() {
|
||||
return diskCache.getDirectory();
|
||||
}
|
||||
|
||||
public long getRealSize() {
|
||||
return DiskUtils.getDirectorySize(getCacheDir());
|
||||
}
|
||||
|
||||
public String getReadableSize() {
|
||||
return Formatter.formatFileSize(context, getRealSize());
|
||||
}
|
||||
|
||||
public void setSize(int value) {
|
||||
diskCache.setMaxSize(value * 1024 * 1024);
|
||||
}
|
||||
|
||||
public Observable<List<Page>> getPageUrlsFromDiskCache(final String chapterUrl) {
|
||||
return Observable.create(subscriber -> {
|
||||
try {
|
||||
List<Page> pages = getPageUrlsFromDiskCacheImpl(chapterUrl);
|
||||
subscriber.onNext(pages);
|
||||
subscriber.onCompleted();
|
||||
} catch (Throwable e) {
|
||||
subscriber.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private List<Page> getPageUrlsFromDiskCacheImpl(String chapterUrl) throws IOException {
|
||||
DiskLruCache.Snapshot snapshot = null;
|
||||
List<Page> pages = null;
|
||||
|
||||
try {
|
||||
String key = DiskUtils.hashKeyForDisk(chapterUrl);
|
||||
snapshot = diskCache.get(key);
|
||||
|
||||
Type collectionType = new TypeToken<List<Page>>() {}.getType();
|
||||
pages = gson.fromJson(snapshot.getString(0), collectionType);
|
||||
} catch (IOException e) {
|
||||
// Do Nothing.
|
||||
} finally {
|
||||
if (snapshot != null) {
|
||||
snapshot.close();
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
public void putPageUrlsToDiskCache(final String chapterUrl, final List<Page> pages) {
|
||||
String cachedValue = gson.toJson(pages);
|
||||
|
||||
DiskLruCache.Editor editor = null;
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
String key = DiskUtils.hashKeyForDisk(chapterUrl);
|
||||
editor = diskCache.edit(key);
|
||||
if (editor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
outputStream = new BufferedOutputStream(editor.newOutputStream(0));
|
||||
outputStream.write(cachedValue.getBytes());
|
||||
outputStream.flush();
|
||||
|
||||
diskCache.flush();
|
||||
editor.commit();
|
||||
} catch (Exception e) {
|
||||
// Do Nothing.
|
||||
} finally {
|
||||
if (editor != null) {
|
||||
editor.abortUnlessCommitted();
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (IOException ignore) {
|
||||
// Do Nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isImageInCache(final String imageUrl) {
|
||||
try {
|
||||
return diskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getImagePath(final String imageUrl) {
|
||||
try {
|
||||
String imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0";
|
||||
File file = new File(diskCache.getDirectory(), imageName);
|
||||
return file.getCanonicalPath();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void putImageToDiskCache(final String imageUrl, final Response response) throws IOException {
|
||||
DiskLruCache.Editor editor = null;
|
||||
BufferedSink sink = null;
|
||||
|
||||
try {
|
||||
String key = DiskUtils.hashKeyForDisk(imageUrl);
|
||||
editor = diskCache.edit(key);
|
||||
if (editor == null) {
|
||||
throw new IOException("Unable to edit key");
|
||||
}
|
||||
|
||||
OutputStream outputStream = new BufferedOutputStream(editor.newOutputStream(0));
|
||||
sink = Okio.buffer(Okio.sink(outputStream));
|
||||
sink.writeAll(response.body().source());
|
||||
|
||||
diskCache.flush();
|
||||
editor.commit();
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to save image");
|
||||
} finally {
|
||||
if (editor != null) {
|
||||
editor.abortUnlessCommitted();
|
||||
}
|
||||
if (sink != null) {
|
||||
sink.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
148
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.java
vendored
Normal file
148
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.java
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
package eu.kanade.tachiyomi.data.cache;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.bumptech.glide.request.animation.GlideAnimation;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import eu.kanade.tachiyomi.util.DiskUtils;
|
||||
|
||||
public class CoverCache {
|
||||
|
||||
private static final String PARAMETER_CACHE_DIRECTORY = "cover_disk_cache";
|
||||
|
||||
private Context context;
|
||||
private File cacheDir;
|
||||
|
||||
public CoverCache(Context context) {
|
||||
this.context = context;
|
||||
cacheDir = new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY);
|
||||
createCacheDir();
|
||||
}
|
||||
|
||||
private boolean createCacheDir() {
|
||||
return !cacheDir.exists() && cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
public void save(String thumbnailUrl, LazyHeaders headers) {
|
||||
save(thumbnailUrl, headers, null);
|
||||
}
|
||||
|
||||
// Download the cover with Glide (it can avoid repeating requests) and save the file on this cache
|
||||
// Optionally, load the image in the given image view when the resource is ready, if not null
|
||||
public void save(String thumbnailUrl, LazyHeaders headers, ImageView imageView) {
|
||||
if (TextUtils.isEmpty(thumbnailUrl))
|
||||
return;
|
||||
|
||||
GlideUrl url = new GlideUrl(thumbnailUrl, headers);
|
||||
Glide.with(context)
|
||||
.load(url)
|
||||
.downloadOnly(new SimpleTarget<File>() {
|
||||
@Override
|
||||
public void onResourceReady(File resource, GlideAnimation<? super File> anim) {
|
||||
try {
|
||||
add(thumbnailUrl, resource);
|
||||
if (imageView != null) {
|
||||
loadFromCache(imageView, resource);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Copy the cover from Glide's cache to this cache
|
||||
public void add(String thumbnailUrl, File source) throws IOException {
|
||||
createCacheDir();
|
||||
File dest = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
|
||||
if (dest.exists())
|
||||
dest.delete();
|
||||
|
||||
InputStream in = new FileInputStream(source);
|
||||
try {
|
||||
OutputStream out = new FileOutputStream(dest);
|
||||
try {
|
||||
// Transfer bytes from in to out
|
||||
byte[] buf = new byte[1024];
|
||||
int len;
|
||||
while ((len = in.read(buf)) > 0) {
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
} finally {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the cover from cache
|
||||
public File get(String thumbnailUrl) {
|
||||
return new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
|
||||
}
|
||||
|
||||
// Delete the cover from cache
|
||||
public boolean delete(String thumbnailUrl) {
|
||||
if (TextUtils.isEmpty(thumbnailUrl))
|
||||
return false;
|
||||
|
||||
File file = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
|
||||
return file.exists() && file.delete();
|
||||
}
|
||||
|
||||
// Save and load the image from cache
|
||||
public void saveAndLoadFromCache(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
|
||||
File localCover = get(thumbnailUrl);
|
||||
if (localCover.exists()) {
|
||||
loadFromCache(imageView, localCover);
|
||||
} else {
|
||||
save(thumbnailUrl, headers, imageView);
|
||||
}
|
||||
}
|
||||
|
||||
// If the image is already in our cache, use it. If not, load it with glide
|
||||
public void loadFromCacheOrNetwork(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
|
||||
File localCover = get(thumbnailUrl);
|
||||
if (localCover.exists()) {
|
||||
loadFromCache(imageView, localCover);
|
||||
} else {
|
||||
loadFromNetwork(imageView, thumbnailUrl, headers);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to load the cover from the cache directory into the specified image view
|
||||
// The file must exist
|
||||
private void loadFromCache(ImageView imageView, File file) {
|
||||
Glide.with(context)
|
||||
.load(file)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
// Helper method to load the cover from network into the specified image view.
|
||||
// It does NOT save the image in cache
|
||||
public void loadFromNetwork(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
|
||||
GlideUrl url = new GlideUrl(thumbnailUrl, headers);
|
||||
Glide.with(context)
|
||||
.load(url)
|
||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||
.centerCrop()
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
}
|
||||
21
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.java
vendored
Normal file
21
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.java
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
package eu.kanade.tachiyomi.data.cache;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.GlideBuilder;
|
||||
import com.bumptech.glide.load.DecodeFormat;
|
||||
import com.bumptech.glide.module.GlideModule;
|
||||
|
||||
public class CoverGlideModule implements GlideModule {
|
||||
|
||||
@Override
|
||||
public void applyOptions(Context context, GlideBuilder builder) {
|
||||
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerComponents(Context context, Glide glide) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
package eu.kanade.tachiyomi.data.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.pushtorefresh.storio.Queries;
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite;
|
||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite;
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteByQuery;
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteCollectionOfObjects;
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteObject;
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects;
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject;
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutCollectionOfObjects;
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutObject;
|
||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery;
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query;
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.data.database.models.CategorySQLiteTypeMapping;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterSQLiteTypeMapping;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategorySQLiteTypeMapping;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSQLiteTypeMapping;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSyncSQLiteTypeMapping;
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver;
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||
import eu.kanade.tachiyomi.util.ChapterRecognition;
|
||||
import rx.Observable;
|
||||
|
||||
public class DatabaseHelper {
|
||||
|
||||
private StorIOSQLite db;
|
||||
|
||||
public DatabaseHelper(Context context) {
|
||||
|
||||
db = DefaultStorIOSQLite.builder()
|
||||
.sqliteOpenHelper(new DbOpenHelper(context))
|
||||
.addTypeMapping(Manga.class, new MangaSQLiteTypeMapping())
|
||||
.addTypeMapping(Chapter.class, new ChapterSQLiteTypeMapping())
|
||||
.addTypeMapping(MangaSync.class, new MangaSyncSQLiteTypeMapping())
|
||||
.addTypeMapping(Category.class, new CategorySQLiteTypeMapping())
|
||||
.addTypeMapping(MangaCategory.class, new MangaCategorySQLiteTypeMapping())
|
||||
.build();
|
||||
}
|
||||
|
||||
// Mangas related queries
|
||||
|
||||
public PreparedGetListOfObjects<Manga> getMangas() {
|
||||
return db.get()
|
||||
.listOfObjects(Manga.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedGetListOfObjects<Manga> getLibraryMangas() {
|
||||
return db.get()
|
||||
.listOfObjects(Manga.class)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(LibraryMangaGetResolver.QUERY)
|
||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
|
||||
.build())
|
||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedGetListOfObjects<Manga> getFavoriteMangas() {
|
||||
return db.get()
|
||||
.listOfObjects(Manga.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where(MangaTable.COLUMN_FAVORITE + "=?")
|
||||
.whereArgs(1)
|
||||
.orderBy(MangaTable.COLUMN_TITLE)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedGetObject<Manga> getManga(String url, int sourceId) {
|
||||
return db.get()
|
||||
.object(Manga.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where(MangaTable.COLUMN_URL + "=? AND " + MangaTable.COLUMN_SOURCE + "=?")
|
||||
.whereArgs(url, sourceId)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedGetObject<Manga> getManga(long id) {
|
||||
return db.get()
|
||||
.object(Manga.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where(MangaTable.COLUMN_ID + "=?")
|
||||
.whereArgs(id)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedPutObject<Manga> insertManga(Manga manga) {
|
||||
return db.put()
|
||||
.object(manga)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedPutCollectionOfObjects<Manga> insertMangas(List<Manga> mangas) {
|
||||
return db.put()
|
||||
.objects(mangas)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedDeleteObject<Manga> deleteManga(Manga manga) {
|
||||
return db.delete()
|
||||
.object(manga)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedDeleteCollectionOfObjects<Manga> deleteMangas(List<Manga> mangas) {
|
||||
return db.delete()
|
||||
.objects(mangas)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedDeleteByQuery deleteMangasNotInLibrary() {
|
||||
return db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where(MangaTable.COLUMN_FAVORITE + "=?")
|
||||
.whereArgs(0)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
|
||||
// Chapters related queries
|
||||
|
||||
public PreparedGetListOfObjects<Chapter> getChapters(Manga manga) {
|
||||
return db.get()
|
||||
.listOfObjects(Chapter.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where(ChapterTable.COLUMN_MANGA_ID + "=?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedGetListOfObjects<Chapter> getChapters(long manga_id, boolean sortAToZ, boolean onlyUnread) {
|
||||
Query.CompleteBuilder query = Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
|
||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + (sortAToZ ? " ASC" : " DESC"));
|
||||
|
||||
if (onlyUnread) {
|
||||
query = query.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " + ChapterTable.COLUMN_READ + "=?")
|
||||
.whereArgs(manga_id, 0);
|
||||
} else {
|
||||
query = query.where(ChapterTable.COLUMN_MANGA_ID + "=?")
|
||||
.whereArgs(manga_id);
|
||||
}
|
||||
|
||||
return db.get()
|
||||
.listOfObjects(Chapter.class)
|
||||
.withQuery(query.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedGetObject<Chapter> getNextChapter(Chapter chapter) {
|
||||
// Add a delta to the chapter number, because binary decimal representation
|
||||
// can retrieve the same chapter again
|
||||
double chapterNumber = chapter.chapter_number + 0.00001;
|
||||
|
||||
return db.get()
|
||||
.object(Chapter.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
|
||||
ChapterTable.COLUMN_CHAPTER_NUMBER + ">? AND " +
|
||||
ChapterTable.COLUMN_CHAPTER_NUMBER + "<=?")
|
||||
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
|
||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
|
||||
.limit(1)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedGetObject<Chapter> getPreviousChapter(Chapter chapter) {
|
||||
// Add a delta to the chapter number, because binary decimal representation
|
||||
// can retrieve the same chapter again
|
||||
double chapterNumber = chapter.chapter_number - 0.00001;
|
||||
|
||||
return db.get()
|
||||
.object(Chapter.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
|
||||
ChapterTable.COLUMN_CHAPTER_NUMBER + "<? AND " +
|
||||
ChapterTable.COLUMN_CHAPTER_NUMBER + ">=?")
|
||||
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
|
||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + " DESC")
|
||||
.limit(1)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedGetObject<Chapter> getNextUnreadChapter(Manga manga) {
|
||||
return db.get()
|
||||
.object(Chapter.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
|
||||
ChapterTable.COLUMN_READ + "=? AND " +
|
||||
ChapterTable.COLUMN_CHAPTER_NUMBER + ">=?")
|
||||
.whereArgs(manga.id, 0, 0)
|
||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
|
||||
.limit(1)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedPutObject<Chapter> insertChapter(Chapter chapter) {
|
||||
return db.put()
|
||||
.object(chapter)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedPutCollectionOfObjects<Chapter> insertChapters(List<Chapter> chapters) {
|
||||
return db.put()
|
||||
.objects(chapters)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
// Add new chapters or delete if the source deletes them
|
||||
public Observable<Pair<Integer, Integer>> insertOrRemoveChapters(Manga manga, List<Chapter> sourceChapters) {
|
||||
List<Chapter> dbChapters = getChapters(manga).executeAsBlocking();
|
||||
|
||||
Observable<List<Chapter>> newChapters = Observable.from(sourceChapters)
|
||||
.filter(c -> !dbChapters.contains(c))
|
||||
.doOnNext(c -> {
|
||||
c.manga_id = manga.id;
|
||||
ChapterRecognition.parseChapterNumber(c, manga);
|
||||
})
|
||||
.toList();
|
||||
|
||||
Observable<List<Chapter>> deletedChapters = Observable.from(dbChapters)
|
||||
.filter(c -> !sourceChapters.contains(c))
|
||||
.toList();
|
||||
|
||||
return Observable.zip(newChapters, deletedChapters, (toAdd, toDelete) -> {
|
||||
int added = 0;
|
||||
int deleted = 0;
|
||||
db.internal().beginTransaction();
|
||||
try {
|
||||
if (!toAdd.isEmpty()) {
|
||||
// Set the date fetch for new items in reverse order to allow another sorting method.
|
||||
// Sources MUST return the chapters from most to less recent, which is common.
|
||||
long now = new Date().getTime();
|
||||
|
||||
for (int i = toAdd.size() - 1; i >= 0; i--) {
|
||||
toAdd.get(i).date_fetch = now++;
|
||||
}
|
||||
added = insertChapters(toAdd).executeAsBlocking().numberOfInserts();
|
||||
}
|
||||
|
||||
if (!toDelete.isEmpty()) {
|
||||
deleted = deleteChapters(toDelete).executeAsBlocking().results().size();
|
||||
}
|
||||
|
||||
db.internal().setTransactionSuccessful();
|
||||
} finally {
|
||||
db.internal().endTransaction();
|
||||
}
|
||||
return Pair.create(added, deleted);
|
||||
});
|
||||
}
|
||||
|
||||
public PreparedDeleteObject<Chapter> deleteChapter(Chapter chapter) {
|
||||
return db.delete()
|
||||
.object(chapter)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedDeleteCollectionOfObjects<Chapter> deleteChapters(List<Chapter> chapters) {
|
||||
return db.delete()
|
||||
.objects(chapters)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
// Manga sync related queries
|
||||
|
||||
public PreparedGetObject<MangaSync> getMangaSync(Manga manga, MangaSyncService sync) {
|
||||
return db.get()
|
||||
.object(MangaSync.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where(MangaSyncTable.COLUMN_MANGA_ID + "=? AND " +
|
||||
MangaSyncTable.COLUMN_SYNC_ID + "=?")
|
||||
.whereArgs(manga.id, sync.getId())
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedGetListOfObjects<MangaSync> getMangasSync(Manga manga) {
|
||||
return db.get()
|
||||
.listOfObjects(MangaSync.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaSyncTable.TABLE)
|
||||
.where(MangaSyncTable.COLUMN_MANGA_ID + "=?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedPutObject<MangaSync> insertMangaSync(MangaSync manga) {
|
||||
return db.put()
|
||||
.object(manga)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedDeleteObject<MangaSync> deleteMangaSync(MangaSync manga) {
|
||||
return db.delete()
|
||||
.object(manga)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
// Categories related queries
|
||||
|
||||
public PreparedGetListOfObjects<Category> getCategories() {
|
||||
return db.get()
|
||||
.listOfObjects(Category.class)
|
||||
.withQuery(Query.builder()
|
||||
.table(CategoryTable.TABLE)
|
||||
.orderBy(CategoryTable.COLUMN_ORDER)
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedPutObject<Category> insertCategory(Category category) {
|
||||
return db.put()
|
||||
.object(category)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedPutCollectionOfObjects<Category> insertCategories(List<Category> categories) {
|
||||
return db.put()
|
||||
.objects(categories)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedDeleteObject<Category> deleteCategory(Category category) {
|
||||
return db.delete()
|
||||
.object(category)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedDeleteCollectionOfObjects<Category> deleteCategories(List<Category> categories) {
|
||||
return db.delete()
|
||||
.objects(categories)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedPutObject<MangaCategory> insertMangaCategory(MangaCategory mangaCategory) {
|
||||
return db.put()
|
||||
.object(mangaCategory)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedPutCollectionOfObjects<MangaCategory> insertMangasCategories(List<MangaCategory> mangasCategories) {
|
||||
return db.put()
|
||||
.objects(mangasCategories)
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public PreparedDeleteByQuery deleteOldMangasCategories(List<Manga> mangas) {
|
||||
List<Long> mangaIds = Observable.from(mangas)
|
||||
.map(manga -> manga.id)
|
||||
.toList().toBlocking().single();
|
||||
|
||||
return db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaCategoryTable.TABLE)
|
||||
.where(MangaCategoryTable.COLUMN_MANGA_ID + " IN ("
|
||||
+ Queries.placeholders(mangas.size()) + ")")
|
||||
.whereArgs(mangaIds.toArray())
|
||||
.build())
|
||||
.prepare();
|
||||
}
|
||||
|
||||
public void setMangaCategories(List<MangaCategory> mangasCategories, List<Manga> mangas) {
|
||||
db.internal().beginTransaction();
|
||||
try {
|
||||
deleteOldMangasCategories(mangas).executeAsBlocking();
|
||||
insertMangasCategories(mangasCategories).executeAsBlocking();
|
||||
db.internal().setTransactionSuccessful();
|
||||
} finally {
|
||||
db.internal().endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package eu.kanade.tachiyomi.data.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
||||
|
||||
public class DbOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
public static final String DATABASE_NAME = "tachiyomi.db";
|
||||
public static final int DATABASE_VERSION = 1;
|
||||
|
||||
public DbOpenHelper(@NonNull Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@NonNull SQLiteDatabase db) {
|
||||
db.execSQL(MangaTable.getCreateTableQuery());
|
||||
db.execSQL(ChapterTable.getCreateTableQuery());
|
||||
db.execSQL(MangaSyncTable.getCreateTableQuery());
|
||||
db.execSQL(CategoryTable.getCreateTableQuery());
|
||||
db.execSQL(MangaCategoryTable.getCreateTableQuery());
|
||||
|
||||
// DB indexes
|
||||
db.execSQL(MangaTable.getCreateUrlIndexQuery());
|
||||
db.execSQL(MangaTable.getCreateFavoriteIndexQuery());
|
||||
db.execSQL(ChapterTable.getCreateMangaIdIndexQuery());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigure(@NonNull SQLiteDatabase db) {
|
||||
db.setForeignKeyConstraintsEnabled(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
||||
|
||||
@StorIOSQLiteType(table = CategoryTable.TABLE)
|
||||
public class Category implements Serializable {
|
||||
|
||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ID, key = true)
|
||||
public Integer id;
|
||||
|
||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_NAME)
|
||||
public String name;
|
||||
|
||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ORDER)
|
||||
public int order;
|
||||
|
||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_FLAGS)
|
||||
public int flags;
|
||||
|
||||
public Category() {}
|
||||
|
||||
public static Category create(String name) {
|
||||
Category c = new Category();
|
||||
c.name = name;
|
||||
return c;
|
||||
}
|
||||
|
||||
public static Category createDefault() {
|
||||
Category c = create("Default");
|
||||
c.id = 0;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
||||
import eu.kanade.tachiyomi.util.UrlUtil;
|
||||
|
||||
@StorIOSQLiteType(table = ChapterTable.TABLE)
|
||||
public class Chapter implements Serializable {
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_ID, key = true)
|
||||
public Long id;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_MANGA_ID)
|
||||
public Long manga_id;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_URL)
|
||||
public String url;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_NAME)
|
||||
public String name;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_READ)
|
||||
public boolean read;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_LAST_PAGE_READ)
|
||||
public int last_page_read;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_FETCH)
|
||||
public long date_fetch;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_UPLOAD)
|
||||
public long date_upload;
|
||||
|
||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_CHAPTER_NUMBER)
|
||||
public float chapter_number;
|
||||
|
||||
public int status;
|
||||
|
||||
public Chapter() {}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = UrlUtil.getPath(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Chapter chapter = (Chapter) o;
|
||||
|
||||
return url.equals(chapter.url);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return url.hashCode();
|
||||
}
|
||||
|
||||
public static Chapter create() {
|
||||
Chapter chapter = new Chapter();
|
||||
chapter.chapter_number = -1;
|
||||
return chapter;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
||||
import eu.kanade.tachiyomi.util.UrlUtil;
|
||||
|
||||
@StorIOSQLiteType(table = MangaTable.TABLE)
|
||||
public class Manga implements Serializable {
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ID, key = true)
|
||||
public Long id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_SOURCE)
|
||||
public int source;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_URL)
|
||||
public String url;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ARTIST)
|
||||
public String artist;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_AUTHOR)
|
||||
public String author;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_DESCRIPTION)
|
||||
public String description;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_GENRE)
|
||||
public String genre;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_TITLE)
|
||||
public String title;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_STATUS)
|
||||
public int status;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_THUMBNAIL_URL)
|
||||
public String thumbnail_url;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_FAVORITE)
|
||||
public boolean favorite;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_LAST_UPDATE)
|
||||
public long last_update;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_INITIALIZED)
|
||||
public boolean initialized;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_VIEWER)
|
||||
public int viewer;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_CHAPTER_FLAGS)
|
||||
public int chapter_flags;
|
||||
|
||||
public int unread;
|
||||
|
||||
public int category;
|
||||
|
||||
public static final int UNKNOWN = 0;
|
||||
public static final int ONGOING = 1;
|
||||
public static final int COMPLETED = 2;
|
||||
public static final int LICENSED = 3;
|
||||
|
||||
public Manga() {}
|
||||
|
||||
public static Manga create(String pathUrl) {
|
||||
Manga m = new Manga();
|
||||
m.url = pathUrl;
|
||||
return m;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = UrlUtil.getPath(url);
|
||||
}
|
||||
|
||||
public void copyFrom(Manga other) {
|
||||
if (other.title != null)
|
||||
title = other.title;
|
||||
|
||||
if (other.author != null)
|
||||
author = other.author;
|
||||
|
||||
if (other.artist != null)
|
||||
artist = other.artist;
|
||||
|
||||
if (other.url != null)
|
||||
url = other.url;
|
||||
|
||||
if (other.description != null)
|
||||
description = other.description;
|
||||
|
||||
if (other.genre != null)
|
||||
genre = other.genre;
|
||||
|
||||
if (other.thumbnail_url != null)
|
||||
thumbnail_url = other.thumbnail_url;
|
||||
|
||||
status = other.status;
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public String getStatus(Context context) {
|
||||
switch (status) {
|
||||
case ONGOING:
|
||||
return context.getString(R.string.ongoing);
|
||||
case COMPLETED:
|
||||
return context.getString(R.string.completed);
|
||||
case LICENSED:
|
||||
return context.getString(R.string.licensed);
|
||||
default:
|
||||
return context.getString(R.string.unknown);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Manga manga = (Manga) o;
|
||||
|
||||
return url.equals(manga.url);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return url.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
||||
|
||||
@StorIOSQLiteType(table = MangaCategoryTable.TABLE)
|
||||
public class MangaCategory {
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_ID, key = true)
|
||||
public Long id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_MANGA_ID)
|
||||
public long manga_id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_CATEGORY_ID)
|
||||
public int category_id;
|
||||
|
||||
public MangaCategory() {}
|
||||
|
||||
public static MangaCategory create(Manga manga, Category category) {
|
||||
MangaCategory mc = new MangaCategory();
|
||||
mc.manga_id = manga.id;
|
||||
mc.category_id = category.id;
|
||||
return mc;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package eu.kanade.tachiyomi.data.database.models;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
||||
|
||||
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
|
||||
public class MangaSync implements Serializable {
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_ID, key = true)
|
||||
public Long id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_MANGA_ID)
|
||||
public long manga_id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_SYNC_ID)
|
||||
public int sync_id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_REMOTE_ID)
|
||||
public int remote_id;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_TITLE)
|
||||
public String title;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_LAST_CHAPTER_READ)
|
||||
public int last_chapter_read;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_TOTAL_CHAPTERS)
|
||||
public int total_chapters;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_SCORE)
|
||||
public float score;
|
||||
|
||||
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_STATUS)
|
||||
public int status;
|
||||
|
||||
public boolean update;
|
||||
|
||||
public static MangaSync create(MangaSyncService service) {
|
||||
MangaSync mangasync = new MangaSync();
|
||||
mangasync.sync_id = service.getId();
|
||||
return mangasync;
|
||||
}
|
||||
|
||||
public void copyPersonalFrom(MangaSync other) {
|
||||
last_chapter_read = other.last_chapter_read;
|
||||
score = other.score;
|
||||
status = other.status;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver;
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
||||
|
||||
public class LibraryMangaGetResolver extends MangaStorIOSQLiteGetResolver {
|
||||
|
||||
public static final LibraryMangaGetResolver INSTANCE = new LibraryMangaGetResolver();
|
||||
|
||||
public static final String QUERY = String.format(
|
||||
"SELECT M.*, COALESCE(MC.%10$s, 0) AS %12$s " +
|
||||
"FROM (" +
|
||||
"SELECT %1$s.*, COALESCE(C.unread, 0) AS %6$s " +
|
||||
"FROM %1$s " +
|
||||
"LEFT JOIN (" +
|
||||
"SELECT %5$s, COUNT(*) AS unread " +
|
||||
"FROM %2$s " +
|
||||
"WHERE %7$s = 0 " +
|
||||
"GROUP BY %5$s" +
|
||||
") AS C " +
|
||||
"ON %4$s = C.%5$s " +
|
||||
"WHERE %8$s = 1 " +
|
||||
"GROUP BY %4$s " +
|
||||
"ORDER BY %9$s" +
|
||||
") AS M " +
|
||||
"LEFT JOIN (SELECT * FROM %3$s) AS MC ON MC.%11$s = M.%4$s",
|
||||
MangaTable.TABLE,
|
||||
ChapterTable.TABLE,
|
||||
MangaCategoryTable.TABLE,
|
||||
MangaTable.COLUMN_ID,
|
||||
ChapterTable.COLUMN_MANGA_ID,
|
||||
MangaTable.COLUMN_UNREAD,
|
||||
ChapterTable.COLUMN_READ,
|
||||
MangaTable.COLUMN_FAVORITE,
|
||||
MangaTable.COLUMN_TITLE,
|
||||
MangaCategoryTable.COLUMN_CATEGORY_ID,
|
||||
MangaCategoryTable.COLUMN_MANGA_ID,
|
||||
MangaTable.COLUMN_CATEGORY
|
||||
);
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Manga mapFromCursor(@NonNull Cursor cursor) {
|
||||
Manga manga = super.mapFromCursor(cursor);
|
||||
|
||||
int unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD);
|
||||
manga.unread = cursor.getInt(unreadColumn);
|
||||
|
||||
int categoryColumn = cursor.getColumnIndex(MangaTable.COLUMN_CATEGORY);
|
||||
manga.category = cursor.getInt(categoryColumn);
|
||||
|
||||
return manga;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver;
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
||||
|
||||
public class MangaWithUnreadGetResolver extends MangaStorIOSQLiteGetResolver {
|
||||
|
||||
public static final MangaWithUnreadGetResolver INSTANCE = new MangaWithUnreadGetResolver();
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Manga mapFromCursor(@NonNull Cursor cursor) {
|
||||
Manga manga = super.mapFromCursor(cursor);
|
||||
int unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD);
|
||||
manga.unread = cursor.getInt(unreadColumn);
|
||||
return manga;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class CategoryTable {
|
||||
|
||||
@NonNull
|
||||
public static final String TABLE = "categories";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_NAME = "name";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ORDER = "sort";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_FLAGS = "flags";
|
||||
|
||||
// This is just class with Meta Data, we don't need instances
|
||||
private CategoryTable() {
|
||||
throw new IllegalStateException("No instances please");
|
||||
}
|
||||
|
||||
// Better than static final field -> allows VM to unload useless String
|
||||
// Because you need this string only once per application life on the device
|
||||
@NonNull
|
||||
public static String getCreateTableQuery() {
|
||||
return "CREATE TABLE " + TABLE + "("
|
||||
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ COLUMN_NAME + " TEXT NOT NULL, "
|
||||
+ COLUMN_ORDER + " INTEGER NOT NULL, "
|
||||
+ COLUMN_FLAGS + " INTEGER NOT NULL"
|
||||
+ ");";
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class ChapterTable {
|
||||
|
||||
@NonNull
|
||||
public static final String TABLE = "chapters";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_MANGA_ID = "manga_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_URL = "url";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_NAME = "name";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_READ = "read";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_DATE_FETCH = "date_fetch";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_DATE_UPLOAD = "date_upload";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_LAST_PAGE_READ = "last_page_read";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_CHAPTER_NUMBER = "chapter_number";
|
||||
|
||||
@NonNull
|
||||
public static String getCreateTableQuery() {
|
||||
return "CREATE TABLE " + TABLE + "("
|
||||
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
|
||||
+ COLUMN_URL + " TEXT NOT NULL, "
|
||||
+ COLUMN_NAME + " TEXT NOT NULL, "
|
||||
+ COLUMN_READ + " BOOLEAN NOT NULL, "
|
||||
+ COLUMN_LAST_PAGE_READ + " INT NOT NULL, "
|
||||
+ COLUMN_CHAPTER_NUMBER + " FLOAT NOT NULL, "
|
||||
+ COLUMN_DATE_FETCH + " LONG NOT NULL, "
|
||||
+ COLUMN_DATE_UPLOAD + " LONG NOT NULL, "
|
||||
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
|
||||
+ "ON DELETE CASCADE"
|
||||
+ ");";
|
||||
}
|
||||
|
||||
public static String getCreateMangaIdIndexQuery() {
|
||||
return "CREATE INDEX " + TABLE + "_" + COLUMN_MANGA_ID + "_index ON " + TABLE + "(" + COLUMN_MANGA_ID + ");";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class MangaCategoryTable {
|
||||
|
||||
@NonNull
|
||||
public static final String TABLE = "mangas_categories";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_MANGA_ID = "manga_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_CATEGORY_ID = "category_id";
|
||||
|
||||
// This is just class with Meta Data, we don't need instances
|
||||
private MangaCategoryTable() {
|
||||
throw new IllegalStateException("No instances please");
|
||||
}
|
||||
|
||||
// Better than static final field -> allows VM to unload useless String
|
||||
// Because you need this string only once per application life on the device
|
||||
@NonNull
|
||||
public static String getCreateTableQuery() {
|
||||
return "CREATE TABLE " + TABLE + "("
|
||||
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
|
||||
+ COLUMN_CATEGORY_ID + " INTEGER NOT NULL, "
|
||||
+ "FOREIGN KEY(" + COLUMN_CATEGORY_ID + ") REFERENCES " + CategoryTable.TABLE + "(" + CategoryTable.COLUMN_ID + ") "
|
||||
+ "ON DELETE CASCADE, "
|
||||
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
|
||||
+ "ON DELETE CASCADE"
|
||||
+ ");";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class MangaSyncTable {
|
||||
|
||||
public static final String TABLE = "manga_sync";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
public static final String COLUMN_MANGA_ID = "manga_id";
|
||||
|
||||
public static final String COLUMN_SYNC_ID = "sync_id";
|
||||
|
||||
public static final String COLUMN_REMOTE_ID = "remote_id";
|
||||
|
||||
public static final String COLUMN_TITLE = "title";
|
||||
|
||||
public static final String COLUMN_LAST_CHAPTER_READ = "last_chapter_read";
|
||||
|
||||
public static final String COLUMN_STATUS = "status";
|
||||
|
||||
public static final String COLUMN_SCORE = "score";
|
||||
|
||||
public static final String COLUMN_TOTAL_CHAPTERS = "total_chapters";
|
||||
|
||||
@NonNull
|
||||
public static String getCreateTableQuery() {
|
||||
return "CREATE TABLE " + TABLE + "("
|
||||
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
|
||||
+ COLUMN_SYNC_ID + " INTEGER NOT NULL, "
|
||||
+ COLUMN_REMOTE_ID + " INTEGER NOT NULL, "
|
||||
+ COLUMN_TITLE + " TEXT NOT NULL, "
|
||||
+ COLUMN_LAST_CHAPTER_READ + " INTEGER NOT NULL, "
|
||||
+ COLUMN_TOTAL_CHAPTERS + " INTEGER NOT NULL, "
|
||||
+ COLUMN_STATUS + " INTEGER NOT NULL, "
|
||||
+ COLUMN_SCORE + " FLOAT NOT NULL, "
|
||||
+ "UNIQUE (" + COLUMN_MANGA_ID + ", " + COLUMN_SYNC_ID + ") ON CONFLICT REPLACE, "
|
||||
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
|
||||
+ "ON DELETE CASCADE"
|
||||
+ ");";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package eu.kanade.tachiyomi.data.database.tables;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class MangaTable {
|
||||
|
||||
@NonNull
|
||||
public static final String TABLE = "mangas";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_SOURCE = "source";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_URL = "url";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_ARTIST = "artist";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_AUTHOR = "author" ;
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_DESCRIPTION = "description";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_GENRE = "genre";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_TITLE = "title";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_STATUS = "status";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_THUMBNAIL_URL = "thumbnail_url";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_FAVORITE = "favorite";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_LAST_UPDATE = "last_update";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_INITIALIZED = "initialized";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_VIEWER = "viewer";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_CHAPTER_FLAGS = "chapter_flags";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_UNREAD = "unread";
|
||||
|
||||
@NonNull
|
||||
public static final String COLUMN_CATEGORY = "category";
|
||||
|
||||
// This is just class with Meta Data, we don't need instances
|
||||
private MangaTable() {
|
||||
throw new IllegalStateException("No instances please");
|
||||
}
|
||||
|
||||
// Better than static final field -> allows VM to unload useless String
|
||||
// Because you need this string only once per application life on the device
|
||||
@NonNull
|
||||
public static String getCreateTableQuery() {
|
||||
return "CREATE TABLE " + TABLE + "("
|
||||
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||
+ COLUMN_SOURCE + " INTEGER NOT NULL, "
|
||||
+ COLUMN_URL + " TEXT NOT NULL, "
|
||||
+ COLUMN_ARTIST + " TEXT, "
|
||||
+ COLUMN_AUTHOR + " TEXT, "
|
||||
+ COLUMN_DESCRIPTION + " TEXT, "
|
||||
+ COLUMN_GENRE + " TEXT, "
|
||||
+ COLUMN_TITLE + " TEXT NOT NULL, "
|
||||
+ COLUMN_STATUS + " INTEGER NOT NULL, "
|
||||
+ COLUMN_THUMBNAIL_URL + " TEXT, "
|
||||
+ COLUMN_FAVORITE + " INTEGER NOT NULL, "
|
||||
+ COLUMN_LAST_UPDATE + " LONG, "
|
||||
+ COLUMN_INITIALIZED + " BOOLEAN NOT NULL, "
|
||||
+ COLUMN_VIEWER + " INTEGER NOT NULL, "
|
||||
+ COLUMN_CHAPTER_FLAGS + " INTEGER NOT NULL"
|
||||
+ ");";
|
||||
|
||||
}
|
||||
|
||||
public static String getCreateUrlIndexQuery() {
|
||||
return "CREATE INDEX " + TABLE + "_" + COLUMN_URL + "_index ON " + TABLE + "(" + COLUMN_URL + ");";
|
||||
}
|
||||
|
||||
public static String getCreateFavoriteIndexQuery() {
|
||||
return "CREATE INDEX " + TABLE + "_" + COLUMN_FAVORITE + "_index ON " + TABLE + "(" + COLUMN_FAVORITE + ");";
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
package eu.kanade.tachiyomi.data.download;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
|
||||
import eu.kanade.tachiyomi.util.DiskUtils;
|
||||
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator;
|
||||
import rx.Observable;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import rx.subjects.BehaviorSubject;
|
||||
import rx.subjects.PublishSubject;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class DownloadManager {
|
||||
|
||||
private Context context;
|
||||
private SourceManager sourceManager;
|
||||
private PreferencesHelper preferences;
|
||||
private Gson gson;
|
||||
|
||||
private PublishSubject<Download> downloadsQueueSubject;
|
||||
private BehaviorSubject<Integer> threadsNumber;
|
||||
private BehaviorSubject<Boolean> runningSubject;
|
||||
private Subscription downloadsSubscription;
|
||||
private Subscription threadsNumberSubscription;
|
||||
|
||||
private DownloadQueue queue;
|
||||
private volatile boolean isRunning;
|
||||
|
||||
public static final String PAGE_LIST_FILE = "index.json";
|
||||
|
||||
public DownloadManager(Context context, SourceManager sourceManager, PreferencesHelper preferences) {
|
||||
this.context = context;
|
||||
this.sourceManager = sourceManager;
|
||||
this.preferences = preferences;
|
||||
|
||||
gson = new Gson();
|
||||
queue = new DownloadQueue();
|
||||
|
||||
downloadsQueueSubject = PublishSubject.create();
|
||||
threadsNumber = BehaviorSubject.create();
|
||||
runningSubject = BehaviorSubject.create();
|
||||
}
|
||||
|
||||
private void initializeSubscriptions() {
|
||||
if (downloadsSubscription != null && !downloadsSubscription.isUnsubscribed())
|
||||
downloadsSubscription.unsubscribe();
|
||||
|
||||
if (threadsNumberSubscription != null && !threadsNumberSubscription.isUnsubscribed())
|
||||
threadsNumberSubscription.unsubscribe();
|
||||
|
||||
threadsNumberSubscription = preferences.downloadThreads().asObservable()
|
||||
.subscribe(threadsNumber::onNext);
|
||||
|
||||
downloadsSubscription = downloadsQueueSubject
|
||||
.lift(new DynamicConcurrentMergeOperator<>(this::downloadChapter, threadsNumber))
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.map(download -> areAllDownloadsFinished())
|
||||
.subscribe(finished -> {
|
||||
if (finished) {
|
||||
DownloadService.stop(context);
|
||||
}
|
||||
}, e -> Timber.e(e.getCause(), e.getMessage()));
|
||||
|
||||
if (!isRunning) {
|
||||
isRunning = true;
|
||||
runningSubject.onNext(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void destroySubscriptions() {
|
||||
if (isRunning) {
|
||||
isRunning = false;
|
||||
runningSubject.onNext(false);
|
||||
}
|
||||
|
||||
if (downloadsSubscription != null && !downloadsSubscription.isUnsubscribed()) {
|
||||
downloadsSubscription.unsubscribe();
|
||||
downloadsSubscription = null;
|
||||
}
|
||||
|
||||
if (threadsNumberSubscription != null && !threadsNumberSubscription.isUnsubscribed()) {
|
||||
threadsNumberSubscription.unsubscribe();
|
||||
threadsNumberSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a download object for every chapter in the event and add them to the downloads queue
|
||||
public void onDownloadChaptersEvent(DownloadChaptersEvent event) {
|
||||
final Manga manga = event.getManga();
|
||||
final Source source = sourceManager.get(manga.source);
|
||||
|
||||
for (Chapter chapter : event.getChapters()) {
|
||||
Download download = new Download(source, manga, chapter);
|
||||
|
||||
if (!prepareDownload(download)) {
|
||||
queue.add(download);
|
||||
if (isRunning) downloadsQueueSubject.onNext(download);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public method to check if a chapter is downloaded
|
||||
public boolean isChapterDownloaded(Source source, Manga manga, Chapter chapter) {
|
||||
File directory = getAbsoluteChapterDirectory(source, manga, chapter);
|
||||
if (!directory.exists())
|
||||
return false;
|
||||
|
||||
List<Page> pages = getSavedPageList(source, manga, chapter);
|
||||
return isChapterDownloaded(directory, pages);
|
||||
}
|
||||
|
||||
// Prepare the download. Returns true if the chapter is already downloaded
|
||||
private boolean prepareDownload(Download download) {
|
||||
// If the chapter is already queued, don't add it again
|
||||
for (Download queuedDownload : queue) {
|
||||
if (download.chapter.id.equals(queuedDownload.chapter.id))
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add the directory to the download object for future access
|
||||
download.directory = getAbsoluteChapterDirectory(download);
|
||||
|
||||
// If the directory doesn't exist, the chapter isn't downloaded.
|
||||
if (!download.directory.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the page list doesn't exist, the chapter isn't downloaded
|
||||
List<Page> savedPages = getSavedPageList(download);
|
||||
if (savedPages == null)
|
||||
return false;
|
||||
|
||||
// Add the page list to the download object for future access
|
||||
download.pages = savedPages;
|
||||
|
||||
// If the number of files matches the number of pages, the chapter is downloaded.
|
||||
// We have the index file, so we check one file more
|
||||
return isChapterDownloaded(download.directory, download.pages);
|
||||
}
|
||||
|
||||
// Check that all the images are downloaded
|
||||
private boolean isChapterDownloaded(File directory, List<Page> pages) {
|
||||
return pages != null && !pages.isEmpty() && pages.size() + 1 == directory.listFiles().length;
|
||||
}
|
||||
|
||||
// Download the entire chapter
|
||||
private Observable<Download> downloadChapter(Download download) {
|
||||
try {
|
||||
DiskUtils.createDirectory(download.directory);
|
||||
} catch (IOException e) {
|
||||
return Observable.error(e);
|
||||
}
|
||||
|
||||
Observable<List<Page>> pageListObservable = download.pages == null ?
|
||||
// Pull page list from network and add them to download object
|
||||
download.source
|
||||
.pullPageListFromNetwork(download.chapter.url)
|
||||
.doOnNext(pages -> download.pages = pages)
|
||||
.doOnNext(pages -> savePageList(download)) :
|
||||
// Or if the page list already exists, start from the file
|
||||
Observable.just(download.pages);
|
||||
|
||||
return pageListObservable
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnNext(pages -> {
|
||||
download.downloadedImages = 0;
|
||||
download.setStatus(Download.DOWNLOADING);
|
||||
})
|
||||
// Get all the URLs to the source images, fetch pages if necessary
|
||||
.flatMap(download.source::getAllImageUrlsFromPageList)
|
||||
// Start downloading images, consider we can have downloaded images already
|
||||
.concatMap(page -> getOrDownloadImage(page, download))
|
||||
// Do after download completes
|
||||
.doOnCompleted(() -> onDownloadCompleted(download))
|
||||
.toList()
|
||||
.map(pages -> download)
|
||||
// If the page list threw, it will resume here
|
||||
.onErrorResumeNext(error -> {
|
||||
download.setStatus(Download.ERROR);
|
||||
return Observable.just(download);
|
||||
});
|
||||
}
|
||||
|
||||
// Get the image from the filesystem if it exists or download from network
|
||||
private Observable<Page> getOrDownloadImage(final Page page, Download download) {
|
||||
// If the image URL is empty, do nothing
|
||||
if (page.getImageUrl() == null)
|
||||
return Observable.just(page);
|
||||
|
||||
String filename = getImageFilename(page);
|
||||
File imagePath = new File(download.directory, filename);
|
||||
|
||||
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||
Observable<Page> pageObservable = isImageDownloaded(imagePath) ?
|
||||
Observable.just(page) :
|
||||
downloadImage(page, download.source, download.directory, filename);
|
||||
|
||||
return pageObservable
|
||||
// When the image is ready, set image path, progress (just in case) and status
|
||||
.doOnNext(p -> {
|
||||
page.setImagePath(imagePath.getAbsolutePath());
|
||||
page.setProgress(100);
|
||||
download.downloadedImages++;
|
||||
page.setStatus(Page.READY);
|
||||
})
|
||||
// Mark this page as error and allow to download the remaining
|
||||
.onErrorResumeNext(e -> {
|
||||
page.setProgress(0);
|
||||
page.setStatus(Page.ERROR);
|
||||
return Observable.just(page);
|
||||
});
|
||||
}
|
||||
|
||||
// Save image on disk
|
||||
private Observable<Page> downloadImage(Page page, Source source, File directory, String filename) {
|
||||
page.setStatus(Page.DOWNLOAD_IMAGE);
|
||||
return source.getImageProgressResponse(page)
|
||||
.flatMap(resp -> {
|
||||
try {
|
||||
DiskUtils.saveBufferedSourceToDirectory(resp.body().source(), directory, filename);
|
||||
} catch (Exception e) {
|
||||
Timber.e(e.getCause(), e.getMessage());
|
||||
return Observable.error(e);
|
||||
}
|
||||
return Observable.just(page);
|
||||
})
|
||||
.retry(2);
|
||||
}
|
||||
|
||||
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
|
||||
public Observable<Page> getDownloadedImage(final Page page, File chapterDir) {
|
||||
if (page.getImageUrl() == null) {
|
||||
page.setStatus(Page.ERROR);
|
||||
return Observable.just(page);
|
||||
}
|
||||
|
||||
File imagePath = new File(chapterDir, getImageFilename(page));
|
||||
|
||||
// When the image is ready, set image path, progress (just in case) and status
|
||||
if (isImageDownloaded(imagePath)) {
|
||||
page.setImagePath(imagePath.getAbsolutePath());
|
||||
page.setProgress(100);
|
||||
page.setStatus(Page.READY);
|
||||
} else {
|
||||
page.setStatus(Page.ERROR);
|
||||
}
|
||||
return Observable.just(page);
|
||||
}
|
||||
|
||||
// Get the filename for an image given the page
|
||||
private String getImageFilename(Page page) {
|
||||
String url = page.getImageUrl();
|
||||
return url.substring(url.lastIndexOf("/") + 1, url.length());
|
||||
}
|
||||
|
||||
private boolean isImageDownloaded(File imagePath) {
|
||||
return imagePath.exists();
|
||||
}
|
||||
|
||||
// Called when a download finishes. This doesn't mean the download was successful, so we check it
|
||||
private void onDownloadCompleted(final Download download) {
|
||||
checkDownloadIsSuccessful(download);
|
||||
savePageList(download);
|
||||
}
|
||||
|
||||
private void checkDownloadIsSuccessful(final Download download) {
|
||||
int actualProgress = 0;
|
||||
int status = Download.DOWNLOADED;
|
||||
// If any page has an error, the download result will be error
|
||||
for (Page page : download.pages) {
|
||||
actualProgress += page.getProgress();
|
||||
if (page.getStatus() != Page.READY) status = Download.ERROR;
|
||||
}
|
||||
// Ensure that the chapter folder has all the images
|
||||
if (!isChapterDownloaded(download.directory, download.pages)) {
|
||||
status = Download.ERROR;
|
||||
}
|
||||
download.totalProgress = actualProgress;
|
||||
download.setStatus(status);
|
||||
// Delete successful downloads from queue after notifying
|
||||
if (status == Download.DOWNLOADED) {
|
||||
queue.remove(download);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the page list from the chapter's directory if it exists, null otherwise
|
||||
public List<Page> getSavedPageList(Source source, Manga manga, Chapter chapter) {
|
||||
List<Page> pages = null;
|
||||
File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter);
|
||||
File pagesFile = new File(chapterDir, PAGE_LIST_FILE);
|
||||
|
||||
JsonReader reader = null;
|
||||
try {
|
||||
if (pagesFile.exists()) {
|
||||
reader = new JsonReader(new FileReader(pagesFile.getAbsolutePath()));
|
||||
Type collectionType = new TypeToken<List<Page>>() {}.getType();
|
||||
pages = gson.fromJson(reader, collectionType);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Timber.e(e.getCause(), e.getMessage());
|
||||
} finally {
|
||||
if (reader != null) try { reader.close(); } catch (IOException e) { /* Do nothing */ }
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
// Shortcut for the method above
|
||||
private List<Page> getSavedPageList(Download download) {
|
||||
return getSavedPageList(download.source, download.manga, download.chapter);
|
||||
}
|
||||
|
||||
// Save the page list to the chapter's directory
|
||||
public void savePageList(Source source, Manga manga, Chapter chapter, List<Page> pages) {
|
||||
File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter);
|
||||
File pagesFile = new File(chapterDir, PAGE_LIST_FILE);
|
||||
|
||||
FileOutputStream out = null;
|
||||
try {
|
||||
out = new FileOutputStream(pagesFile);
|
||||
out.write(gson.toJson(pages).getBytes());
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
Timber.e(e.getCause(), e.getMessage());
|
||||
} finally {
|
||||
if (out != null) try { out.close(); } catch (IOException e) { /* Do nothing */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Shortcut for the method above
|
||||
private void savePageList(Download download) {
|
||||
savePageList(download.source, download.manga, download.chapter, download.pages);
|
||||
}
|
||||
|
||||
// Get the absolute path to the chapter directory
|
||||
public File getAbsoluteChapterDirectory(Source source, Manga manga, Chapter chapter) {
|
||||
String chapterRelativePath = source.getName() +
|
||||
File.separator +
|
||||
manga.title.replaceAll("[^\\sa-zA-Z0-9.-]", "_") +
|
||||
File.separator +
|
||||
chapter.name.replaceAll("[^\\sa-zA-Z0-9.-]", "_") + " (" + chapter.id + ")";
|
||||
|
||||
return new File(preferences.getDownloadsDirectory(), chapterRelativePath);
|
||||
}
|
||||
|
||||
// Shortcut for the method above
|
||||
private File getAbsoluteChapterDirectory(Download download) {
|
||||
return getAbsoluteChapterDirectory(download.source, download.manga, download.chapter);
|
||||
}
|
||||
|
||||
public void deleteChapter(Source source, Manga manga, Chapter chapter) {
|
||||
File path = getAbsoluteChapterDirectory(source, manga, chapter);
|
||||
DiskUtils.deleteFiles(path);
|
||||
}
|
||||
|
||||
public DownloadQueue getQueue() {
|
||||
return queue;
|
||||
}
|
||||
|
||||
public boolean areAllDownloadsFinished() {
|
||||
for (Download download : queue) {
|
||||
if (download.getStatus() <= Download.DOWNLOADING)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean startDownloads() {
|
||||
if (queue.isEmpty())
|
||||
return false;
|
||||
|
||||
boolean hasPendingDownloads = false;
|
||||
if (downloadsSubscription == null)
|
||||
initializeSubscriptions();
|
||||
|
||||
for (Download download : queue) {
|
||||
if (download.getStatus() != Download.DOWNLOADED) {
|
||||
if (download.getStatus() != Download.QUEUE) download.setStatus(Download.QUEUE);
|
||||
if (!hasPendingDownloads) hasPendingDownloads = true;
|
||||
downloadsQueueSubject.onNext(download);
|
||||
}
|
||||
}
|
||||
return hasPendingDownloads;
|
||||
}
|
||||
|
||||
public void stopDownloads() {
|
||||
destroySubscriptions();
|
||||
for (Download download : queue) {
|
||||
if (download.getStatus() == Download.DOWNLOADING) {
|
||||
download.setStatus(Download.ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BehaviorSubject<Boolean> getRunningSubject() {
|
||||
return runningSubject;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package eu.kanade.tachiyomi.data.download;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
|
||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
public class DownloadService extends Service {
|
||||
|
||||
@Inject DownloadManager downloadManager;
|
||||
@Inject PreferencesHelper preferences;
|
||||
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
private Subscription networkChangeSubscription;
|
||||
private Subscription queueRunningSubscription;
|
||||
private boolean isRunning;
|
||||
|
||||
public static void start(Context context) {
|
||||
context.startService(new Intent(context, DownloadService.class));
|
||||
}
|
||||
|
||||
public static void stop(Context context) {
|
||||
context.stopService(new Intent(context, DownloadService.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
App.get(this).getComponent().inject(this);
|
||||
|
||||
createWakeLock();
|
||||
|
||||
listenQueueRunningChanges();
|
||||
EventBus.getDefault().registerSticky(this);
|
||||
listenNetworkChanges();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
EventBus.getDefault().unregister(this);
|
||||
queueRunningSubscription.unsubscribe();
|
||||
networkChangeSubscription.unsubscribe();
|
||||
downloadManager.destroySubscriptions();
|
||||
destroyWakeLock();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEvent(DownloadChaptersEvent event) {
|
||||
EventBus.getDefault().removeStickyEvent(event);
|
||||
downloadManager.onDownloadChaptersEvent(event);
|
||||
}
|
||||
|
||||
private void listenNetworkChanges() {
|
||||
networkChangeSubscription = new ReactiveNetwork().enableInternetCheck()
|
||||
.observeConnectivity(getApplicationContext())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(state -> {
|
||||
switch (state) {
|
||||
case WIFI_CONNECTED_HAS_INTERNET:
|
||||
// If there are no remaining downloads, destroy the service
|
||||
if (!isRunning && !downloadManager.startDownloads()) {
|
||||
stopSelf();
|
||||
}
|
||||
break;
|
||||
case MOBILE_CONNECTED:
|
||||
if (!preferences.downloadOnlyOverWifi()) {
|
||||
if (!isRunning && !downloadManager.startDownloads()) {
|
||||
stopSelf();
|
||||
}
|
||||
} else if (isRunning) {
|
||||
downloadManager.stopDownloads();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (isRunning) {
|
||||
downloadManager.stopDownloads();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}, error -> {
|
||||
ToastUtil.showShort(this, R.string.download_queue_error);
|
||||
stopSelf();
|
||||
});
|
||||
}
|
||||
|
||||
private void listenQueueRunningChanges() {
|
||||
queueRunningSubscription = downloadManager.getRunningSubject()
|
||||
.subscribe(running -> {
|
||||
isRunning = running;
|
||||
if (running)
|
||||
acquireWakeLock();
|
||||
else
|
||||
releaseWakeLock();
|
||||
});
|
||||
}
|
||||
|
||||
private void createWakeLock() {
|
||||
wakeLock = ((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, "DownloadService:WakeLock");
|
||||
}
|
||||
|
||||
private void destroyWakeLock() {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
wakeLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void acquireWakeLock() {
|
||||
if (wakeLock != null && !wakeLock.isHeld()) {
|
||||
wakeLock.acquire();
|
||||
}
|
||||
}
|
||||
|
||||
public void releaseWakeLock() {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package eu.kanade.tachiyomi.data.download.model;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import rx.subjects.PublishSubject;
|
||||
|
||||
public class Download {
|
||||
public Source source;
|
||||
public Manga manga;
|
||||
public Chapter chapter;
|
||||
public List<Page> pages;
|
||||
public File directory;
|
||||
|
||||
public transient volatile int totalProgress;
|
||||
public transient volatile int downloadedImages;
|
||||
private transient volatile int status;
|
||||
|
||||
private transient PublishSubject<Download> statusSubject;
|
||||
|
||||
public static final int NOT_DOWNLOADED = 0;
|
||||
public static final int QUEUE = 1;
|
||||
public static final int DOWNLOADING = 2;
|
||||
public static final int DOWNLOADED = 3;
|
||||
public static final int ERROR = 4;
|
||||
|
||||
|
||||
public Download(Source source, Manga manga, Chapter chapter) {
|
||||
this.source = source;
|
||||
this.manga = manga;
|
||||
this.chapter = chapter;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(int status) {
|
||||
this.status = status;
|
||||
notifyStatus();
|
||||
}
|
||||
|
||||
public void setStatusSubject(PublishSubject<Download> subject) {
|
||||
this.statusSubject = subject;
|
||||
}
|
||||
|
||||
private void notifyStatus() {
|
||||
if (statusSubject != null)
|
||||
statusSubject.onNext(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package eu.kanade.tachiyomi.data.download.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import rx.Observable;
|
||||
import rx.subjects.PublishSubject;
|
||||
|
||||
public class DownloadQueue extends ArrayList<Download> {
|
||||
|
||||
private PublishSubject<Download> statusSubject;
|
||||
|
||||
public DownloadQueue() {
|
||||
super();
|
||||
statusSubject = PublishSubject.create();
|
||||
}
|
||||
|
||||
public boolean add(Download download) {
|
||||
download.setStatusSubject(statusSubject);
|
||||
download.setStatus(Download.QUEUE);
|
||||
return super.add(download);
|
||||
}
|
||||
|
||||
public void remove(Download download) {
|
||||
super.remove(download);
|
||||
download.setStatusSubject(null);
|
||||
}
|
||||
|
||||
public void remove(Chapter chapter) {
|
||||
for (Download download : this) {
|
||||
if (download.chapter.id.equals(chapter.id)) {
|
||||
remove(download);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Observable<Download> getActiveDownloads() {
|
||||
return Observable.from(this)
|
||||
.filter(download -> download.getStatus() == Download.DOWNLOADING);
|
||||
}
|
||||
|
||||
public Observable<Download> getStatusObservable() {
|
||||
return statusSubject;
|
||||
}
|
||||
|
||||
public Observable<Download> getProgressObservable() {
|
||||
return statusSubject
|
||||
.startWith(getActiveDownloads())
|
||||
.flatMap(download -> {
|
||||
if (download.getStatus() == Download.DOWNLOADING) {
|
||||
PublishSubject<Integer> pageStatusSubject = PublishSubject.create();
|
||||
setPagesSubject(download.pages, pageStatusSubject);
|
||||
return pageStatusSubject
|
||||
.filter(status -> status == Page.READY)
|
||||
.map(status -> download);
|
||||
|
||||
} else if (download.getStatus() == Download.DOWNLOADED ||
|
||||
download.getStatus() == Download.ERROR) {
|
||||
|
||||
setPagesSubject(download.pages, null);
|
||||
}
|
||||
return Observable.just(download);
|
||||
})
|
||||
.filter(download -> download.getStatus() == Download.DOWNLOADING);
|
||||
}
|
||||
|
||||
private void setPagesSubject(List<Page> pages, PublishSubject<Integer> subject) {
|
||||
if (pages != null) {
|
||||
for (Page page : pages) {
|
||||
page.setStatusSubject(subject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
|
||||
|
||||
public class MangaSyncManager {
|
||||
|
||||
private List<MangaSyncService> services;
|
||||
private MyAnimeList myAnimeList;
|
||||
|
||||
public static final int MYANIMELIST = 1;
|
||||
|
||||
public MangaSyncManager(Context context) {
|
||||
services = new ArrayList<>();
|
||||
myAnimeList = new MyAnimeList(context);
|
||||
services.add(myAnimeList);
|
||||
}
|
||||
|
||||
public MyAnimeList getMyAnimeList() {
|
||||
return myAnimeList;
|
||||
}
|
||||
|
||||
public List<MangaSyncService> getSyncServices() {
|
||||
return services;
|
||||
}
|
||||
|
||||
public MangaSyncService getSyncService(int id) {
|
||||
switch (id) {
|
||||
case MYANIMELIST:
|
||||
return myAnimeList;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.base;
|
||||
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import rx.Observable;
|
||||
|
||||
public abstract class MangaSyncService {
|
||||
|
||||
// Name of the manga sync service to display
|
||||
public abstract String getName();
|
||||
|
||||
// Id of the sync service (must be declared and obtained from MangaSyncManager to avoid conflicts)
|
||||
public abstract int getId();
|
||||
|
||||
public abstract Observable<Boolean> login(String username, String password);
|
||||
|
||||
public abstract boolean isLogged();
|
||||
|
||||
public abstract Observable<Response> update(MangaSync manga);
|
||||
|
||||
public abstract Observable<Response> add(MangaSync manga);
|
||||
|
||||
public abstract Observable<Response> bind(MangaSync manga);
|
||||
|
||||
public abstract String getStatus(int status);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.services;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.squareup.okhttp.Credentials;
|
||||
import com.squareup.okhttp.FormEncodingBuilder;
|
||||
import com.squareup.okhttp.Headers;
|
||||
import com.squareup.okhttp.RequestBody;
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import rx.Observable;
|
||||
|
||||
public class MyAnimeList extends MangaSyncService {
|
||||
|
||||
@Inject PreferencesHelper preferences;
|
||||
@Inject NetworkHelper networkService;
|
||||
|
||||
private Headers headers;
|
||||
private String username;
|
||||
|
||||
public static final String BASE_URL = "http://myanimelist.net";
|
||||
|
||||
private static final String ENTRY_TAG = "entry";
|
||||
private static final String CHAPTER_TAG = "chapter";
|
||||
private static final String SCORE_TAG = "score";
|
||||
private static final String STATUS_TAG = "status";
|
||||
|
||||
public static final int READING = 1;
|
||||
public static final int COMPLETED = 2;
|
||||
public static final int ON_HOLD = 3;
|
||||
public static final int DROPPED = 4;
|
||||
public static final int PLAN_TO_READ = 6;
|
||||
|
||||
public static final int DEFAULT_STATUS = READING;
|
||||
public static final int DEFAULT_SCORE = 0;
|
||||
|
||||
private Context context;
|
||||
|
||||
public MyAnimeList(Context context) {
|
||||
this.context = context;
|
||||
App.get(context).getComponent().inject(this);
|
||||
|
||||
String username = preferences.getMangaSyncUsername(this);
|
||||
String password = preferences.getMangaSyncPassword(this);
|
||||
|
||||
if (!username.isEmpty() && !password.isEmpty()) {
|
||||
createHeaders(username, password);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "MyAnimeList";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return MangaSyncManager.MYANIMELIST;
|
||||
}
|
||||
|
||||
public String getLoginUrl() {
|
||||
return Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/account/verify_credentials.xml")
|
||||
.toString();
|
||||
}
|
||||
|
||||
public Observable<Boolean> login(String username, String password) {
|
||||
createHeaders(username, password);
|
||||
return networkService.getResponse(getLoginUrl(), headers, null)
|
||||
.map(response -> response.code() == 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLogged() {
|
||||
return !preferences.getMangaSyncUsername(this).isEmpty()
|
||||
&& !preferences.getMangaSyncPassword(this).isEmpty();
|
||||
}
|
||||
|
||||
public String getSearchUrl(String query) {
|
||||
return Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/manga/search.xml")
|
||||
.appendQueryParameter("q", query)
|
||||
.toString();
|
||||
}
|
||||
|
||||
public Observable<List<MangaSync>> search(String query) {
|
||||
return networkService.getStringResponse(getSearchUrl(query), headers, null)
|
||||
.map(Jsoup::parse)
|
||||
.flatMap(doc -> Observable.from(doc.select("entry")))
|
||||
.filter(entry -> !entry.select("type").text().equals("Novel"))
|
||||
.map(entry -> {
|
||||
MangaSync manga = MangaSync.create(this);
|
||||
manga.title = entry.select("title").first().text();
|
||||
manga.remote_id = Integer.parseInt(entry.select("id").first().text());
|
||||
manga.total_chapters = Integer.parseInt(entry.select("chapters").first().text());
|
||||
return manga;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
public String getListUrl(String username) {
|
||||
return Uri.parse(BASE_URL).buildUpon()
|
||||
.appendPath("malappinfo.php")
|
||||
.appendQueryParameter("u", username)
|
||||
.appendQueryParameter("status", "all")
|
||||
.appendQueryParameter("type", "manga")
|
||||
.toString();
|
||||
}
|
||||
|
||||
public Observable<List<MangaSync>> getList() {
|
||||
// TODO cache this list for a few minutes
|
||||
return networkService.getStringResponse(getListUrl(username), headers, null)
|
||||
.map(Jsoup::parse)
|
||||
.flatMap(doc -> Observable.from(doc.select("manga")))
|
||||
.map(entry -> {
|
||||
MangaSync manga = MangaSync.create(this);
|
||||
manga.title = entry.select("series_title").first().text();
|
||||
manga.remote_id = Integer.parseInt(
|
||||
entry.select("series_mangadb_id").first().text());
|
||||
manga.last_chapter_read = Integer.parseInt(
|
||||
entry.select("my_read_chapters").first().text());
|
||||
manga.status = Integer.parseInt(
|
||||
entry.select("my_status").first().text());
|
||||
// MAL doesn't support score with decimals
|
||||
manga.score = Integer.parseInt(
|
||||
entry.select("my_score").first().text());
|
||||
manga.total_chapters = Integer.parseInt(
|
||||
entry.select("series_chapters").first().text());
|
||||
return manga;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
public String getUpdateUrl(MangaSync manga) {
|
||||
return Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/update")
|
||||
.appendPath(manga.remote_id + ".xml")
|
||||
.toString();
|
||||
}
|
||||
|
||||
public Observable<Response> update(MangaSync manga) {
|
||||
try {
|
||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
||||
manga.status = COMPLETED;
|
||||
}
|
||||
RequestBody payload = getMangaPostPayload(manga);
|
||||
return networkService.postData(getUpdateUrl(manga), payload, headers);
|
||||
} catch (IOException e) {
|
||||
return Observable.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getAddUrl(MangaSync manga) {
|
||||
return Uri.parse(BASE_URL).buildUpon()
|
||||
.appendEncodedPath("api/mangalist/add")
|
||||
.appendPath(manga.remote_id + ".xml")
|
||||
.toString();
|
||||
}
|
||||
|
||||
public Observable<Response> add(MangaSync manga) {
|
||||
try {
|
||||
RequestBody payload = getMangaPostPayload(manga);
|
||||
return networkService.postData(getAddUrl(manga), payload, headers);
|
||||
} catch (IOException e) {
|
||||
return Observable.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private RequestBody getMangaPostPayload(MangaSync manga) throws IOException {
|
||||
XmlSerializer xml = Xml.newSerializer();
|
||||
StringWriter writer = new StringWriter();
|
||||
xml.setOutput(writer);
|
||||
xml.startDocument("UTF-8", false);
|
||||
xml.startTag("", ENTRY_TAG);
|
||||
|
||||
// Last chapter read
|
||||
if (manga.last_chapter_read != 0) {
|
||||
xml.startTag("", CHAPTER_TAG);
|
||||
xml.text(manga.last_chapter_read + "");
|
||||
xml.endTag("", CHAPTER_TAG);
|
||||
}
|
||||
// Manga status in the list
|
||||
xml.startTag("", STATUS_TAG);
|
||||
xml.text(manga.status + "");
|
||||
xml.endTag("", STATUS_TAG);
|
||||
// Manga score
|
||||
xml.startTag("", SCORE_TAG);
|
||||
xml.text(manga.score + "");
|
||||
xml.endTag("", SCORE_TAG);
|
||||
|
||||
xml.endTag("", ENTRY_TAG);
|
||||
xml.endDocument();
|
||||
|
||||
FormEncodingBuilder form = new FormEncodingBuilder();
|
||||
form.add("data", writer.toString());
|
||||
return form.build();
|
||||
}
|
||||
|
||||
public Observable<Response> bind(MangaSync manga) {
|
||||
return getList()
|
||||
.flatMap(list -> {
|
||||
manga.sync_id = getId();
|
||||
for (MangaSync remoteManga : list) {
|
||||
if (remoteManga.remote_id == manga.remote_id) {
|
||||
// Manga is already in the list
|
||||
manga.copyPersonalFrom(remoteManga);
|
||||
return update(manga);
|
||||
}
|
||||
}
|
||||
// Set default fields if it's not found in the list
|
||||
manga.score = DEFAULT_SCORE;
|
||||
manga.status = DEFAULT_STATUS;
|
||||
return add(manga);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStatus(int status) {
|
||||
switch (status) {
|
||||
case READING:
|
||||
return context.getString(R.string.reading);
|
||||
case COMPLETED:
|
||||
return context.getString(R.string.completed);
|
||||
case ON_HOLD:
|
||||
return context.getString(R.string.on_hold);
|
||||
case DROPPED:
|
||||
return context.getString(R.string.dropped);
|
||||
case PLAN_TO_READ:
|
||||
return context.getString(R.string.plan_to_read);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public void createHeaders(String username, String password) {
|
||||
this.username = username;
|
||||
Headers.Builder builder = new Headers.Builder();
|
||||
builder.add("Authorization", Credentials.basic(username, password));
|
||||
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C");
|
||||
setHeaders(builder.build());
|
||||
}
|
||||
|
||||
public void setHeaders(Headers headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package eu.kanade.tachiyomi.data.network;
|
||||
|
||||
|
||||
import com.squareup.okhttp.CacheControl;
|
||||
import com.squareup.okhttp.FormEncodingBuilder;
|
||||
import com.squareup.okhttp.Headers;
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.RequestBody;
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.net.CookieStore;
|
||||
|
||||
import rx.Observable;
|
||||
|
||||
public final class NetworkHelper {
|
||||
|
||||
private OkHttpClient client;
|
||||
private CookieManager cookieManager;
|
||||
|
||||
public final CacheControl NULL_CACHE_CONTROL = new CacheControl.Builder().noCache().build();
|
||||
public final Headers NULL_HEADERS = new Headers.Builder().build();
|
||||
public final RequestBody NULL_REQUEST_BODY = new FormEncodingBuilder().build();
|
||||
|
||||
public NetworkHelper() {
|
||||
client = new OkHttpClient();
|
||||
cookieManager = new CookieManager();
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
|
||||
client.setCookieHandler(cookieManager);
|
||||
}
|
||||
|
||||
public Observable<Response> getResponse(final String url, final Headers headers, final CacheControl cacheControl) {
|
||||
return Observable.defer(() -> {
|
||||
try {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.cacheControl(cacheControl != null ? cacheControl : NULL_CACHE_CONTROL)
|
||||
.headers(headers != null ? headers : NULL_HEADERS)
|
||||
.build();
|
||||
|
||||
return Observable.just(client.newCall(request).execute());
|
||||
} catch (Throwable e) {
|
||||
return Observable.error(e);
|
||||
}
|
||||
}).retry(1);
|
||||
}
|
||||
|
||||
public Observable<String> mapResponseToString(final Response response) {
|
||||
return Observable.defer(() -> {
|
||||
try {
|
||||
return Observable.just(response.body().string());
|
||||
} catch (Throwable e) {
|
||||
return Observable.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Observable<String> getStringResponse(final String url, final Headers headers, final CacheControl cacheControl) {
|
||||
return getResponse(url, headers, cacheControl)
|
||||
.flatMap(this::mapResponseToString);
|
||||
}
|
||||
|
||||
public Observable<Response> postData(final String url, final RequestBody formBody, final Headers headers) {
|
||||
return Observable.defer(() -> {
|
||||
try {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(formBody != null ? formBody : NULL_REQUEST_BODY)
|
||||
.headers(headers != null ? headers : NULL_HEADERS)
|
||||
.build();
|
||||
return Observable.just(client.newCall(request).execute());
|
||||
} catch (Throwable e) {
|
||||
return Observable.error(e);
|
||||
}
|
||||
}).retry(1);
|
||||
}
|
||||
|
||||
public Observable<Response> getProgressResponse(final String url, final Headers headers, final ProgressListener listener) {
|
||||
return Observable.defer(() -> {
|
||||
try {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.cacheControl(NULL_CACHE_CONTROL)
|
||||
.headers(headers != null ? headers : NULL_HEADERS)
|
||||
.build();
|
||||
|
||||
OkHttpClient progressClient = client.clone();
|
||||
|
||||
progressClient.networkInterceptors().add(chain -> {
|
||||
Response originalResponse = chain.proceed(chain.request());
|
||||
return originalResponse.newBuilder()
|
||||
.body(new ProgressResponseBody(originalResponse.body(), listener))
|
||||
.build();
|
||||
});
|
||||
return Observable.just(progressClient.newCall(request).execute());
|
||||
} catch (Throwable e) {
|
||||
return Observable.error(e);
|
||||
}
|
||||
}).retry(2);
|
||||
}
|
||||
|
||||
public CookieStore getCookies() {
|
||||
return cookieManager.getCookieStore();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.network;
|
||||
|
||||
public interface ProgressListener {
|
||||
void update(long bytesRead, long contentLength, boolean done);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package eu.kanade.tachiyomi.data.network;
|
||||
|
||||
import com.squareup.okhttp.MediaType;
|
||||
import com.squareup.okhttp.ResponseBody;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
import okio.ForwardingSource;
|
||||
import okio.Okio;
|
||||
import okio.Source;
|
||||
|
||||
public class ProgressResponseBody extends ResponseBody {
|
||||
|
||||
private final ResponseBody responseBody;
|
||||
private final ProgressListener progressListener;
|
||||
private BufferedSource bufferedSource;
|
||||
|
||||
public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
|
||||
this.responseBody = responseBody;
|
||||
this.progressListener = progressListener;
|
||||
}
|
||||
|
||||
@Override public MediaType contentType() {
|
||||
return responseBody.contentType();
|
||||
}
|
||||
|
||||
@Override public long contentLength() throws IOException {
|
||||
return responseBody.contentLength();
|
||||
}
|
||||
|
||||
@Override public BufferedSource source() throws IOException {
|
||||
if (bufferedSource == null) {
|
||||
bufferedSource = Okio.buffer(source(responseBody.source()));
|
||||
}
|
||||
return bufferedSource;
|
||||
}
|
||||
|
||||
private Source source(Source source) {
|
||||
return new ForwardingSource(source) {
|
||||
long totalBytesRead = 0L;
|
||||
@Override public long read(Buffer sink, long byteCount) throws IOException {
|
||||
long bytesRead = super.read(sink, byteCount);
|
||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
|
||||
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
|
||||
return bytesRead;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package eu.kanade.tachiyomi.data.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.f2prateek.rx.preferences.Preference;
|
||||
import com.f2prateek.rx.preferences.RxSharedPreferences;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
|
||||
public class PreferencesHelper {
|
||||
|
||||
private Context context;
|
||||
private SharedPreferences prefs;
|
||||
private RxSharedPreferences rxPrefs;
|
||||
|
||||
private static final String SOURCE_ACCOUNT_USERNAME = "pref_source_username_";
|
||||
private static final String SOURCE_ACCOUNT_PASSWORD = "pref_source_password_";
|
||||
private static final String MANGASYNC_ACCOUNT_USERNAME = "pref_mangasync_username_";
|
||||
private static final String MANGASYNC_ACCOUNT_PASSWORD = "pref_mangasync_password_";
|
||||
|
||||
private File defaultDownloadsDir;
|
||||
|
||||
public PreferencesHelper(Context context) {
|
||||
this.context = context;
|
||||
PreferenceManager.setDefaultValues(context, R.xml.pref_reader, false);
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
rxPrefs = RxSharedPreferences.create(prefs);
|
||||
|
||||
defaultDownloadsDir = new File(Environment.getExternalStorageDirectory() +
|
||||
File.separator + context.getString(R.string.app_name), "downloads");
|
||||
|
||||
// Create default directory
|
||||
if (getDownloadsDirectory().equals(defaultDownloadsDir.getAbsolutePath()) &&
|
||||
!defaultDownloadsDir.exists()) {
|
||||
defaultDownloadsDir.mkdirs();
|
||||
try {
|
||||
new File(defaultDownloadsDir, ".nomedia").createNewFile();
|
||||
} catch (IOException e) { /* Ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
private String getKey(int keyResource) {
|
||||
return context.getString(keyResource);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
prefs.edit().clear().apply();
|
||||
}
|
||||
|
||||
public Preference<Boolean> lockOrientation() {
|
||||
return rxPrefs.getBoolean(getKey(R.string.pref_lock_orientation_key), true);
|
||||
}
|
||||
|
||||
public Preference<Boolean> enableTransitions() {
|
||||
return rxPrefs.getBoolean(getKey(R.string.pref_enable_transitions_key), true);
|
||||
}
|
||||
|
||||
public Preference<Boolean> showPageNumber() {
|
||||
return rxPrefs.getBoolean(getKey(R.string.pref_show_page_number_key), true);
|
||||
}
|
||||
|
||||
public Preference<Boolean> hideStatusBar() {
|
||||
return rxPrefs.getBoolean(getKey(R.string.pref_hide_status_bar_key), true);
|
||||
}
|
||||
|
||||
public Preference<Boolean> keepScreenOn() {
|
||||
return rxPrefs.getBoolean(getKey(R.string.pref_keep_screen_on_key), true);
|
||||
}
|
||||
|
||||
public Preference<Boolean> customBrightness() {
|
||||
return rxPrefs.getBoolean(getKey(R.string.pref_custom_brightness_key), false);
|
||||
}
|
||||
|
||||
public Preference<Float> customBrightnessValue() {
|
||||
return rxPrefs.getFloat(getKey(R.string.pref_custom_brightness_value_key), 0F);
|
||||
}
|
||||
|
||||
public int getDefaultViewer() {
|
||||
// TODO use IntListPreference
|
||||
return Integer.parseInt(prefs.getString(getKey(R.string.pref_default_viewer_key), "1"));
|
||||
}
|
||||
|
||||
public Preference<Integer> portraitColumns() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_portrait_key), 0);
|
||||
}
|
||||
|
||||
public Preference<Integer> landscapeColumns() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_landscape_key), 0);
|
||||
}
|
||||
|
||||
public boolean updateOnlyNonCompleted() {
|
||||
return prefs.getBoolean(getKey(R.string.pref_update_only_non_completed_key), false);
|
||||
}
|
||||
|
||||
public boolean autoUpdateMangaSync() {
|
||||
return prefs.getBoolean(getKey(R.string.pref_auto_update_manga_sync_key), true);
|
||||
}
|
||||
|
||||
public boolean askUpdateMangaSync() {
|
||||
return prefs.getBoolean(getKey(R.string.pref_ask_update_manga_sync_key), false);
|
||||
}
|
||||
|
||||
public Preference<Integer> imageDecoder() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_image_decoder_key), 0);
|
||||
}
|
||||
|
||||
public Preference<Integer> readerTheme() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0);
|
||||
}
|
||||
|
||||
public String getSourceUsername(Source source) {
|
||||
return prefs.getString(SOURCE_ACCOUNT_USERNAME + source.getId(), "");
|
||||
}
|
||||
|
||||
public String getSourcePassword(Source source) {
|
||||
return prefs.getString(SOURCE_ACCOUNT_PASSWORD + source.getId(), "");
|
||||
}
|
||||
|
||||
public void setSourceCredentials(Source source, String username, String password) {
|
||||
prefs.edit()
|
||||
.putString(SOURCE_ACCOUNT_USERNAME + source.getId(), username)
|
||||
.putString(SOURCE_ACCOUNT_PASSWORD + source.getId(), password)
|
||||
.apply();
|
||||
}
|
||||
|
||||
public String getMangaSyncUsername(MangaSyncService sync) {
|
||||
return prefs.getString(MANGASYNC_ACCOUNT_USERNAME + sync.getId(), "");
|
||||
}
|
||||
|
||||
public String getMangaSyncPassword(MangaSyncService sync) {
|
||||
return prefs.getString(MANGASYNC_ACCOUNT_PASSWORD + sync.getId(), "");
|
||||
}
|
||||
|
||||
public void setMangaSyncCredentials(MangaSyncService sync, String username, String password) {
|
||||
prefs.edit()
|
||||
.putString(MANGASYNC_ACCOUNT_USERNAME + sync.getId(), username)
|
||||
.putString(MANGASYNC_ACCOUNT_PASSWORD + sync.getId(), password)
|
||||
.apply();
|
||||
}
|
||||
|
||||
public String getDownloadsDirectory() {
|
||||
return prefs.getString(getKey(R.string.pref_download_directory_key),
|
||||
defaultDownloadsDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
public void setDownloadsDirectory(String path) {
|
||||
prefs.edit().putString(getKey(R.string.pref_download_directory_key), path).apply();
|
||||
}
|
||||
|
||||
public Preference<Integer> downloadThreads() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_download_slots_key), 1);
|
||||
}
|
||||
|
||||
public boolean downloadOnlyOverWifi() {
|
||||
return prefs.getBoolean(getKey(R.string.pref_download_only_over_wifi_key), true);
|
||||
}
|
||||
|
||||
public static int getLibraryUpdateInterval(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
|
||||
context.getString(R.string.pref_library_update_interval_key), 0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package eu.kanade.tachiyomi.data.source;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.online.english.Batoto;
|
||||
import eu.kanade.tachiyomi.data.source.online.english.Kissmanga;
|
||||
import eu.kanade.tachiyomi.data.source.online.english.Mangafox;
|
||||
import eu.kanade.tachiyomi.data.source.online.english.Mangahere;
|
||||
|
||||
public class SourceManager {
|
||||
|
||||
public static final int BATOTO = 1;
|
||||
public static final int MANGAHERE = 2;
|
||||
public static final int MANGAFOX = 3;
|
||||
public static final int KISSMANGA = 4;
|
||||
|
||||
private HashMap<Integer, Source> sourcesMap;
|
||||
private Context context;
|
||||
|
||||
public SourceManager(Context context) {
|
||||
sourcesMap = new HashMap<>();
|
||||
this.context = context;
|
||||
|
||||
initializeSources();
|
||||
}
|
||||
|
||||
public Source get(int sourceKey) {
|
||||
if (!sourcesMap.containsKey(sourceKey)) {
|
||||
sourcesMap.put(sourceKey, createSource(sourceKey));
|
||||
}
|
||||
return sourcesMap.get(sourceKey);
|
||||
}
|
||||
|
||||
private Source createSource(int sourceKey) {
|
||||
switch (sourceKey) {
|
||||
case BATOTO:
|
||||
return new Batoto(context);
|
||||
case MANGAHERE:
|
||||
return new Mangahere(context);
|
||||
case MANGAFOX:
|
||||
return new Mangafox(context);
|
||||
case KISSMANGA:
|
||||
return new Kissmanga(context);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void initializeSources() {
|
||||
sourcesMap.put(BATOTO, createSource(BATOTO));
|
||||
sourcesMap.put(MANGAHERE, createSource(MANGAHERE));
|
||||
sourcesMap.put(MANGAFOX, createSource(MANGAFOX));
|
||||
sourcesMap.put(KISSMANGA, createSource(KISSMANGA));
|
||||
}
|
||||
|
||||
public List<Source> getSources() {
|
||||
return new ArrayList<>(sourcesMap.values());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package eu.kanade.tachiyomi.data.source.base;
|
||||
|
||||
import com.squareup.okhttp.Headers;
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
import rx.Observable;
|
||||
|
||||
public abstract class BaseSource {
|
||||
|
||||
// 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 getId();
|
||||
|
||||
// Base url of the source, like: http://example.com
|
||||
public abstract String getBaseUrl();
|
||||
|
||||
// True if the source requires a login
|
||||
public abstract boolean isLoginRequired();
|
||||
|
||||
// Return the initial popular mangas URL
|
||||
protected abstract String getInitialPopularMangasUrl();
|
||||
|
||||
// Return the initial search url given a query
|
||||
protected abstract String getInitialSearchUrl(String query);
|
||||
|
||||
// Get the popular list of mangas from the source's parsed document
|
||||
protected abstract List<Manga> parsePopularMangasFromHtml(Document parsedHtml);
|
||||
|
||||
// Get the next popular page URL or null if it's the last
|
||||
protected abstract String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page);
|
||||
|
||||
// Get the searched list of mangas from the source's parsed document
|
||||
protected abstract List<Manga> parseSearchFromHtml(Document parsedHtml);
|
||||
|
||||
// Get the next search page URL or null if it's the last
|
||||
protected abstract String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query);
|
||||
|
||||
// Given the URL of a manga and the result of the request, return the details of the manga
|
||||
protected abstract Manga parseHtmlToManga(String mangaUrl, String unparsedHtml);
|
||||
|
||||
// Given the result of the request to mangas' chapters, return a list of chapters
|
||||
protected abstract List<Chapter> parseHtmlToChapters(String unparsedHtml);
|
||||
|
||||
// Given the result of the request to a chapter, return the list of URLs of the chapter
|
||||
protected abstract List<String> parseHtmlToPageUrls(String unparsedHtml);
|
||||
|
||||
// Given the result of the request to a chapter's page, return the URL of the image of the page
|
||||
protected abstract String parseHtmlToImageUrl(String unparsedHtml);
|
||||
|
||||
|
||||
// Login related methods, shouldn't be overriden if the source doesn't require it
|
||||
public Observable<Boolean> login(String username, String password) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
public boolean isLogged() {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
protected boolean isAuthenticationSuccessful(Response response) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
|
||||
// Default fields, they can be overriden by sources' implementation
|
||||
|
||||
// Get the URL to the details of a manga, useful if the source provides some kind of API or fast calls
|
||||
protected String overrideMangaUrl(String defaultMangaUrl) {
|
||||
return defaultMangaUrl;
|
||||
}
|
||||
|
||||
// Get the URL of the first page that contains a source image and the page list
|
||||
protected String overrideChapterUrl(String defaultPageUrl) {
|
||||
return defaultPageUrl;
|
||||
}
|
||||
|
||||
// Get the URL of the pages that contains source images
|
||||
protected String overridePageUrl(String defaultPageUrl) {
|
||||
return defaultPageUrl;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package eu.kanade.tachiyomi.data.source.base;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public abstract class LoginSource extends Source {
|
||||
|
||||
public LoginSource() {}
|
||||
|
||||
public LoginSource(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoginRequired() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package eu.kanade.tachiyomi.data.source.base;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.squareup.okhttp.Headers;
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import rx.Observable;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
public abstract class Source extends BaseSource {
|
||||
|
||||
@Inject protected NetworkHelper networkService;
|
||||
@Inject protected ChapterCache chapterCache;
|
||||
@Inject protected PreferencesHelper prefs;
|
||||
protected Headers requestHeaders;
|
||||
protected LazyHeaders glideHeaders;
|
||||
|
||||
public Source() {}
|
||||
|
||||
public Source(Context context) {
|
||||
App.get(context).getComponent().inject(this);
|
||||
requestHeaders = headersBuilder().build();
|
||||
glideHeaders = glideHeadersBuilder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoginRequired() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the most popular mangas from the source
|
||||
public Observable<MangasPage> pullPopularMangasFromNetwork(MangasPage page) {
|
||||
if (page.page == 1)
|
||||
page.url = getInitialPopularMangasUrl();
|
||||
|
||||
return networkService
|
||||
.getStringResponse(page.url, requestHeaders, null)
|
||||
.map(Jsoup::parse)
|
||||
.doOnNext(doc -> page.mangas = parsePopularMangasFromHtml(doc))
|
||||
.doOnNext(doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page))
|
||||
.map(response -> page);
|
||||
}
|
||||
|
||||
// Get mangas from the source with a query
|
||||
public Observable<MangasPage> searchMangasFromNetwork(MangasPage page, String query) {
|
||||
if (page.page == 1)
|
||||
page.url = getInitialSearchUrl(query);
|
||||
|
||||
return networkService
|
||||
.getStringResponse(page.url, requestHeaders, null)
|
||||
.map(Jsoup::parse)
|
||||
.doOnNext(doc -> page.mangas = parseSearchFromHtml(doc))
|
||||
.doOnNext(doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query))
|
||||
.map(response -> page);
|
||||
}
|
||||
|
||||
// Get manga details from the source
|
||||
public Observable<Manga> pullMangaFromNetwork(final String mangaUrl) {
|
||||
return networkService
|
||||
.getStringResponse(getBaseUrl() + overrideMangaUrl(mangaUrl), requestHeaders, null)
|
||||
.flatMap(unparsedHtml -> Observable.just(parseHtmlToManga(mangaUrl, unparsedHtml)));
|
||||
}
|
||||
|
||||
// Get chapter list of a manga from the source
|
||||
public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
|
||||
return networkService
|
||||
.getStringResponse(getBaseUrl() + mangaUrl, requestHeaders, null)
|
||||
.flatMap(unparsedHtml -> {
|
||||
List<Chapter> chapters = parseHtmlToChapters(unparsedHtml);
|
||||
return !chapters.isEmpty() ?
|
||||
Observable.just(chapters) :
|
||||
Observable.error(new Exception("No chapters found"));
|
||||
});
|
||||
}
|
||||
|
||||
public Observable<List<Page>> getCachedPageListOrPullFromNetwork(final String chapterUrl) {
|
||||
return chapterCache.getPageUrlsFromDiskCache(getChapterCacheKey(chapterUrl))
|
||||
.onErrorResumeNext(throwable -> {
|
||||
return pullPageListFromNetwork(chapterUrl);
|
||||
})
|
||||
.onBackpressureBuffer();
|
||||
}
|
||||
|
||||
public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) {
|
||||
return networkService
|
||||
.getStringResponse(getBaseUrl() + overrideChapterUrl(chapterUrl), requestHeaders, null)
|
||||
.flatMap(unparsedHtml -> {
|
||||
List<Page> pages = convertToPages(parseHtmlToPageUrls(unparsedHtml));
|
||||
return !pages.isEmpty() ?
|
||||
Observable.just(parseFirstPage(pages, unparsedHtml)) :
|
||||
Observable.error(new Exception("Page list is empty"));
|
||||
});
|
||||
}
|
||||
|
||||
public Observable<Page> getAllImageUrlsFromPageList(final List<Page> pages) {
|
||||
return Observable.from(pages)
|
||||
.filter(page -> page.getImageUrl() != null)
|
||||
.mergeWith(getRemainingImageUrlsFromPageList(pages));
|
||||
}
|
||||
|
||||
// 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)
|
||||
.concatMap(this::getImageUrlFromPage);
|
||||
}
|
||||
|
||||
public Observable<Page> getImageUrlFromPage(final Page page) {
|
||||
page.setStatus(Page.LOAD_PAGE);
|
||||
return networkService
|
||||
.getStringResponse(overridePageUrl(page.getUrl()), requestHeaders, null)
|
||||
.flatMap(unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)))
|
||||
.onErrorResumeNext(e -> {
|
||||
page.setStatus(Page.ERROR);
|
||||
return Observable.just(null);
|
||||
})
|
||||
.flatMap(imageUrl -> {
|
||||
page.setImageUrl(imageUrl);
|
||||
return Observable.just(page);
|
||||
})
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Observable<Page> getCachedImage(final Page page) {
|
||||
Observable<Page> pageObservable = Observable.just(page);
|
||||
if (page.getImageUrl() == null)
|
||||
return pageObservable;
|
||||
|
||||
return pageObservable
|
||||
.flatMap(p -> {
|
||||
if (!chapterCache.isImageInCache(page.getImageUrl())) {
|
||||
return cacheImage(page);
|
||||
}
|
||||
return Observable.just(page);
|
||||
})
|
||||
.flatMap(p -> {
|
||||
page.setImagePath(chapterCache.getImagePath(page.getImageUrl()));
|
||||
page.setStatus(Page.READY);
|
||||
return Observable.just(page);
|
||||
})
|
||||
.onErrorResumeNext(e -> {
|
||||
page.setStatus(Page.ERROR);
|
||||
return Observable.just(page);
|
||||
});
|
||||
}
|
||||
|
||||
private Observable<Page> cacheImage(final Page page) {
|
||||
page.setStatus(Page.DOWNLOAD_IMAGE);
|
||||
return getImageProgressResponse(page)
|
||||
.flatMap(resp -> {
|
||||
try {
|
||||
chapterCache.putImageToDiskCache(page.getImageUrl(), resp);
|
||||
} catch (IOException e) {
|
||||
return Observable.error(e);
|
||||
}
|
||||
return Observable.just(page);
|
||||
});
|
||||
}
|
||||
|
||||
public Observable<Response> getImageProgressResponse(final Page page) {
|
||||
return networkService.getProgressResponse(page.getImageUrl(), requestHeaders, page);
|
||||
}
|
||||
|
||||
public void savePageList(String chapterUrl, List<Page> pages) {
|
||||
if (pages != null)
|
||||
chapterCache.putPageUrlsToDiskCache(getChapterCacheKey(chapterUrl), pages);
|
||||
}
|
||||
|
||||
protected 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 pages;
|
||||
}
|
||||
|
||||
protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
|
||||
String firstImage = parseHtmlToImageUrl(unparsedHtml);
|
||||
pages.get(0).setImageUrl(firstImage);
|
||||
return pages;
|
||||
}
|
||||
|
||||
protected String getChapterCacheKey(String chapterUrl) {
|
||||
return getId() + chapterUrl;
|
||||
}
|
||||
|
||||
protected LazyHeaders.Builder glideHeadersBuilder() {
|
||||
LazyHeaders.Builder builder = new LazyHeaders.Builder();
|
||||
for (Map.Entry<String, List<String>> entry : requestHeaders.toMultimap().entrySet()) {
|
||||
builder.addHeader(entry.getKey(), entry.getValue().get(0));
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public LazyHeaders getGlideHeaders() {
|
||||
return glideHeaders;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package eu.kanade.tachiyomi.data.source.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
|
||||
public class MangasPage {
|
||||
|
||||
public List<Manga> mangas;
|
||||
public int page;
|
||||
public String url;
|
||||
public String nextPageUrl;
|
||||
|
||||
public MangasPage(int page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package eu.kanade.tachiyomi.data.source.model;
|
||||
|
||||
import eu.kanade.tachiyomi.data.network.ProgressListener;
|
||||
import rx.subjects.PublishSubject;
|
||||
|
||||
public class Page implements ProgressListener {
|
||||
|
||||
private int pageNumber;
|
||||
private String url;
|
||||
private String imageUrl;
|
||||
private transient String imagePath;
|
||||
private transient volatile int status;
|
||||
private transient volatile int progress;
|
||||
|
||||
private transient PublishSubject<Integer> statusSubject;
|
||||
|
||||
public static final int QUEUE = 0;
|
||||
public static final int LOAD_PAGE = 1;
|
||||
public static final int DOWNLOAD_IMAGE = 2;
|
||||
public static final int READY = 3;
|
||||
public static final int ERROR = 4;
|
||||
|
||||
public Page(int pageNumber, String url, String imageUrl, String imagePath) {
|
||||
this.pageNumber = pageNumber;
|
||||
this.url = url;
|
||||
this.imageUrl = imageUrl;
|
||||
this.imagePath = imagePath;
|
||||
}
|
||||
|
||||
public Page(int pageNumber, String url) {
|
||||
this(pageNumber, url, null, null);
|
||||
}
|
||||
|
||||
public int getPageNumber() {
|
||||
return pageNumber;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public String getImagePath() {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
public void setImagePath(String imagePath) {
|
||||
this.imagePath = imagePath;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(int status) {
|
||||
this.status = status;
|
||||
if (statusSubject != null)
|
||||
statusSubject.onNext(status);
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public void setProgress(int value) {
|
||||
progress = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(long bytesRead, long contentLength, boolean done) {
|
||||
progress = (int) ((100 * bytesRead) / contentLength);
|
||||
}
|
||||
|
||||
public void setStatusSubject(PublishSubject<Integer> subject) {
|
||||
this.statusSubject = subject;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.squareup.okhttp.FormEncodingBuilder;
|
||||
import com.squareup.okhttp.Headers;
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
import java.net.HttpCookie;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.LoginSource;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.util.Parser;
|
||||
import rx.Observable;
|
||||
|
||||
public class Batoto extends LoginSource {
|
||||
|
||||
public static final String NAME = "Batoto (EN)";
|
||||
public static final String BASE_URL = "http://bato.to";
|
||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/search_ajax?order_cond=views&order=desc&p=%d";
|
||||
public static final String SEARCH_URL = BASE_URL + "/search_ajax?name=%s&p=%s";
|
||||
public static final String CHAPTER_URL = "/areader?id=%s&p=1";
|
||||
public static final String PAGE_URL = BASE_URL + "/areader?id=%s&p=%s";
|
||||
public static final String MANGA_URL = "/comic_pop?id=%s";
|
||||
public static final String LOGIN_URL = BASE_URL + "/forums/index.php?app=core&module=global§ion=login";
|
||||
|
||||
private Pattern datePattern;
|
||||
private Map<String, Integer> dateFields;
|
||||
|
||||
public Batoto(Context context) {
|
||||
super(context);
|
||||
|
||||
datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*");
|
||||
dateFields = new HashMap<String, Integer>() {{
|
||||
put("second", Calendar.SECOND);
|
||||
put("minute", Calendar.MINUTE);
|
||||
put("hour", Calendar.HOUR);
|
||||
put("day", Calendar.DATE);
|
||||
put("week", Calendar.WEEK_OF_YEAR);
|
||||
put("month", Calendar.MONTH);
|
||||
put("year", Calendar.YEAR);
|
||||
}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return SourceManager.BATOTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return BASE_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Headers.Builder headersBuilder() {
|
||||
Headers.Builder builder = super.headersBuilder();
|
||||
builder.add("Cookie", "lang_option=English");
|
||||
builder.add("Referer", "http://bato.to/reader");
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInitialPopularMangasUrl() {
|
||||
return String.format(POPULAR_MANGAS_URL, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInitialSearchUrl(String query) {
|
||||
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String overrideMangaUrl(String defaultMangaUrl) {
|
||||
String mangaId = defaultMangaUrl.substring(defaultMangaUrl.lastIndexOf("r") + 1);
|
||||
return String.format(MANGA_URL, mangaId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String overrideChapterUrl(String defaultPageUrl) {
|
||||
String id = defaultPageUrl.substring(defaultPageUrl.indexOf("#") + 1);
|
||||
return String.format(CHAPTER_URL, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String overridePageUrl(String defaultPageUrl) {
|
||||
int start = defaultPageUrl.indexOf("#") + 1;
|
||||
int end = defaultPageUrl.indexOf("_", start);
|
||||
String id = defaultPageUrl.substring(start, end);
|
||||
return String.format(PAGE_URL, id, defaultPageUrl.substring(end+1));
|
||||
}
|
||||
|
||||
private List<Manga> parseMangasFromHtml(Document parsedHtml) {
|
||||
List<Manga> mangaList = new ArrayList<>();
|
||||
|
||||
if (!parsedHtml.text().contains("No (more) comics found!")) {
|
||||
for (Element currentHtmlBlock : parsedHtml.select("tr:not([id]):not([class])")) {
|
||||
Manga manga = constructMangaFromHtmlBlock(currentHtmlBlock);
|
||||
mangaList.add(manga);
|
||||
}
|
||||
}
|
||||
return mangaList;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||
return parseMangasFromHtml(parsedHtml);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||
Element next = Parser.element(parsedHtml, "#show_more_row");
|
||||
return next != null ? String.format(POPULAR_MANGAS_URL, page.page + 1) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||
return parseMangasFromHtml(parsedHtml);
|
||||
}
|
||||
|
||||
private Manga constructMangaFromHtmlBlock(Element htmlBlock) {
|
||||
Manga manga = new Manga();
|
||||
manga.source = getId();
|
||||
|
||||
Element urlElement = Parser.element(htmlBlock, "a[href^=http://bato.to]");
|
||||
if (urlElement != null) {
|
||||
manga.setUrl(urlElement.attr("href"));
|
||||
manga.title = urlElement.text().trim();
|
||||
}
|
||||
return manga;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||
Element next = Parser.element(parsedHtml, "#show_more_row");
|
||||
return next != null ? String.format(SEARCH_URL, query, page.page + 1) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
|
||||
Element tbody = parsedDocument.select("tbody").first();
|
||||
Element artistElement = tbody.select("tr:contains(Author/Artist:)").first();
|
||||
Elements genreElements = tbody.select("tr:contains(Genres:) img");
|
||||
|
||||
Manga manga = Manga.create(mangaUrl);
|
||||
manga.author = Parser.text(artistElement, "td:eq(1)");
|
||||
manga.artist = Parser.text(artistElement, "td:eq(2)", manga.author);
|
||||
manga.description = Parser.text(tbody, "tr:contains(Description:) > td:eq(1)");
|
||||
manga.thumbnail_url = Parser.src(parsedDocument, "img[src^=http://img.bato.to/forums/uploads/]");
|
||||
manga.status = parseStatus(Parser.text(parsedDocument, "tr:contains(Status:) > td:eq(1)"));
|
||||
|
||||
if (!genreElements.isEmpty()) {
|
||||
List<String> genres = new ArrayList<>();
|
||||
for (Element element : genreElements) {
|
||||
genres.add(element.attr("alt"));
|
||||
}
|
||||
manga.genre = TextUtils.join(", ", genres);
|
||||
}
|
||||
|
||||
manga.initialized = true;
|
||||
return manga;
|
||||
}
|
||||
|
||||
private int parseStatus(String status) {
|
||||
switch (status) {
|
||||
case "Ongoing":
|
||||
return Manga.ONGOING;
|
||||
case "Complete":
|
||||
return Manga.COMPLETED;
|
||||
default:
|
||||
return Manga.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
|
||||
List<Chapter> chapterList = new ArrayList<>();
|
||||
|
||||
Elements chapterElements = parsedDocument.select("tr.row.lang_English.chapter_row");
|
||||
for (Element chapterElement : chapterElements) {
|
||||
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
||||
chapterList.add(chapter);
|
||||
}
|
||||
return chapterList;
|
||||
|
||||
}
|
||||
|
||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||
Chapter chapter = Chapter.create();
|
||||
|
||||
Element urlElement = chapterElement.select("a[href^=http://bato.to/reader").first();
|
||||
Element dateElement = chapterElement.select("td").get(4);
|
||||
|
||||
if (urlElement != null) {
|
||||
String fieldUrl = urlElement.attr("href");
|
||||
chapter.setUrl(fieldUrl);
|
||||
chapter.name = urlElement.text().trim();
|
||||
}
|
||||
if (dateElement != null) {
|
||||
chapter.date_upload = parseDateFromElement(dateElement);
|
||||
}
|
||||
return chapter;
|
||||
}
|
||||
|
||||
private long parseDateFromElement(Element dateElement) {
|
||||
String dateAsString = dateElement.text();
|
||||
|
||||
Date date;
|
||||
try {
|
||||
date = new SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString);
|
||||
} catch (ParseException e) {
|
||||
Matcher m = datePattern.matcher(dateAsString);
|
||||
|
||||
if (m.matches()) {
|
||||
String number = m.group(1);
|
||||
int amount = number.contains("A") ? 1 : Integer.parseInt(m.group(1));
|
||||
String unit = m.group(2);
|
||||
|
||||
Calendar cal = Calendar.getInstance();
|
||||
// Not an error
|
||||
cal.add(dateFields.get(unit), -amount);
|
||||
date = cal.getTime();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
|
||||
List<String> pageUrlList = new ArrayList<>();
|
||||
|
||||
Element selectElement = Parser.element(parsedDocument, "#page_select");
|
||||
if (selectElement != null) {
|
||||
for (Element pageUrlElement : selectElement.select("option")) {
|
||||
pageUrlList.add(pageUrlElement.attr("value"));
|
||||
}
|
||||
} else {
|
||||
// For webtoons in one page
|
||||
for (int i = 0; i < parsedDocument.select("div > img").size(); i++) {
|
||||
pageUrlList.add("");
|
||||
}
|
||||
}
|
||||
|
||||
return pageUrlList;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
|
||||
if (!unparsedHtml.contains("Want to see this chapter per page instead?")) {
|
||||
String firstImage = parseHtmlToImageUrl(unparsedHtml);
|
||||
pages.get(0).setImageUrl(firstImage);
|
||||
} else {
|
||||
// For webtoons in one page
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
Elements imageUrls = parsedDocument.select("div > img");
|
||||
for (int i = 0; i < pages.size(); i++) {
|
||||
pages.get(i).setImageUrl(imageUrls.get(i).attr("src"));
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||
int beginIndex = unparsedHtml.indexOf("<img id=\"comic_page\"");
|
||||
int endIndex = unparsedHtml.indexOf("</a>", beginIndex);
|
||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||
|
||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||
Element imageElement = parsedDocument.getElementById("comic_page");
|
||||
return imageElement.attr("src");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable<Boolean> login(String username, String password) {
|
||||
return networkService.getStringResponse(LOGIN_URL, requestHeaders, null)
|
||||
.flatMap(response -> doLogin(response, username, password))
|
||||
.map(this::isAuthenticationSuccessful);
|
||||
}
|
||||
|
||||
private Observable<Response> doLogin(String response, String username, String password) {
|
||||
Document doc = Jsoup.parse(response);
|
||||
Element form = doc.select("#login").first();
|
||||
String postUrl = form.attr("action");
|
||||
|
||||
FormEncodingBuilder formBody = new FormEncodingBuilder();
|
||||
Element authKey = form.select("input[name=auth_key]").first();
|
||||
|
||||
formBody.add(authKey.attr("name"), authKey.attr("value"));
|
||||
formBody.add("ips_username", username);
|
||||
formBody.add("ips_password", password);
|
||||
formBody.add("invisible", "1");
|
||||
formBody.add("rememberMe", "1");
|
||||
|
||||
return networkService.postData(postUrl, formBody.build(), requestHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isAuthenticationSuccessful(Response response) {
|
||||
return response.priorResponse() != null && response.priorResponse().code() == 302;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLogged() {
|
||||
try {
|
||||
for ( HttpCookie cookie : networkService.getCookies().get(new URI(BASE_URL)) ) {
|
||||
if (cookie.getName().equals("pass_hash"))
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (URISyntaxException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable<List<Chapter>> pullChaptersFromNetwork(String mangaUrl) {
|
||||
Observable<List<Chapter>> observable;
|
||||
if (!isLogged()) {
|
||||
observable = login(prefs.getSourceUsername(this), prefs.getSourcePassword(this))
|
||||
.flatMap(result -> super.pullChaptersFromNetwork(mangaUrl));
|
||||
}
|
||||
else {
|
||||
observable = super.pullChaptersFromNetwork(mangaUrl);
|
||||
}
|
||||
return observable;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.squareup.okhttp.FormEncodingBuilder;
|
||||
import com.squareup.okhttp.Headers;
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.util.Parser;
|
||||
import rx.Observable;
|
||||
|
||||
public class Kissmanga extends Source {
|
||||
|
||||
public static final String NAME = "Kissmanga (EN)";
|
||||
public static final String HOST = "kissmanga.com";
|
||||
public static final String IP = "93.174.95.110";
|
||||
public static final String BASE_URL = "http://" + IP;
|
||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/MangaList/MostPopular?page=%s";
|
||||
public static final String SEARCH_URL = BASE_URL + "/AdvanceSearch";
|
||||
|
||||
public Kissmanga(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Headers.Builder headersBuilder() {
|
||||
Headers.Builder builder = super.headersBuilder();
|
||||
builder.add("Host", HOST);
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return SourceManager.KISSMANGA;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return BASE_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialPopularMangasUrl() {
|
||||
return String.format(POPULAR_MANGAS_URL, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialSearchUrl(String query) {
|
||||
return SEARCH_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||
List<Manga> mangaList = new ArrayList<>();
|
||||
|
||||
for (Element currentHtmlBlock : parsedHtml.select("table.listing tr:gt(1)")) {
|
||||
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
|
||||
mangaList.add(manga);
|
||||
}
|
||||
|
||||
return mangaList;
|
||||
}
|
||||
|
||||
private Manga constructPopularMangaFromHtml(Element htmlBlock) {
|
||||
Manga manga = new Manga();
|
||||
manga.source = getId();
|
||||
|
||||
Element urlElement = Parser.element(htmlBlock, "td a:eq(0)");
|
||||
|
||||
if (urlElement != null) {
|
||||
manga.setUrl(urlElement.attr("href"));
|
||||
manga.title = urlElement.text();
|
||||
}
|
||||
|
||||
return manga;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||
String path = Parser.href(parsedHtml, "li > a:contains(› Next)");
|
||||
return path != null ? BASE_URL + path : null;
|
||||
}
|
||||
|
||||
public Observable<MangasPage> searchMangasFromNetwork(MangasPage page, String query) {
|
||||
if (page.page == 1)
|
||||
page.url = getInitialSearchUrl(query);
|
||||
|
||||
FormEncodingBuilder form = new FormEncodingBuilder();
|
||||
form.add("authorArtist", "");
|
||||
form.add("mangaName", query);
|
||||
form.add("status", "");
|
||||
form.add("genres", "");
|
||||
|
||||
return networkService
|
||||
.postData(page.url, form.build(), requestHeaders)
|
||||
.flatMap(networkService::mapResponseToString)
|
||||
.map(Jsoup::parse)
|
||||
.doOnNext(doc -> page.mangas = parseSearchFromHtml(doc))
|
||||
.doOnNext(doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query))
|
||||
.map(response -> page);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||
return parsePopularMangasFromHtml(parsedHtml);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
Element infoElement = parsedDocument.select("div.barContent").first();
|
||||
|
||||
Manga manga = Manga.create(mangaUrl);
|
||||
manga.title = Parser.text(infoElement, "a.bigChar");
|
||||
manga.author = Parser.text(infoElement, "p:has(span:contains(Author:)) > a");
|
||||
manga.genre = Parser.allText(infoElement, "p:has(span:contains(Genres:)) > *:gt(0)");
|
||||
manga.description = Parser.allText(infoElement, "p:has(span:contains(Summary:)) ~ p");
|
||||
manga.status = parseStatus(Parser.text(infoElement, "p:has(span:contains(Status:))"));
|
||||
|
||||
String thumbnail = Parser.src(parsedDocument, ".rightBox:eq(0) img");
|
||||
if (thumbnail != null) {
|
||||
manga.thumbnail_url = Uri.parse(thumbnail).buildUpon().authority(IP).toString();
|
||||
}
|
||||
|
||||
manga.initialized = true;
|
||||
return manga;
|
||||
}
|
||||
|
||||
private int parseStatus(String status) {
|
||||
if (status.contains("Ongoing")) {
|
||||
return Manga.ONGOING;
|
||||
}
|
||||
if (status.contains("Completed")) {
|
||||
return Manga.COMPLETED;
|
||||
}
|
||||
return Manga.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
List<Chapter> chapterList = new ArrayList<>();
|
||||
|
||||
for (Element chapterElement : parsedDocument.select("table.listing tr:gt(1)")) {
|
||||
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
||||
chapterList.add(chapter);
|
||||
}
|
||||
|
||||
return chapterList;
|
||||
}
|
||||
|
||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||
Chapter chapter = Chapter.create();
|
||||
|
||||
Element urlElement = Parser.element(chapterElement, "a");
|
||||
String date = Parser.text(chapterElement, "td:eq(1)");
|
||||
|
||||
if (urlElement != null) {
|
||||
chapter.setUrl(urlElement.attr("href"));
|
||||
chapter.name = urlElement.text();
|
||||
}
|
||||
if (date != null) {
|
||||
try {
|
||||
chapter.date_upload = new SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH).parse(date).getTime();
|
||||
} catch (ParseException e) { /* Ignore */ }
|
||||
}
|
||||
return chapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) {
|
||||
return networkService
|
||||
.postData(getBaseUrl() + overrideChapterUrl(chapterUrl), null, requestHeaders)
|
||||
.flatMap(networkService::mapResponseToString)
|
||||
.flatMap(unparsedHtml -> {
|
||||
List<Page> pages = convertToPages(parseHtmlToPageUrls(unparsedHtml));
|
||||
return !pages.isEmpty() ?
|
||||
Observable.just(parseFirstPage(pages, unparsedHtml)) :
|
||||
Observable.error(new Exception("Page list is empty"));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
List<String> pageUrlList = new ArrayList<>();
|
||||
|
||||
int numImages = parsedDocument.select("#divImage img").size();
|
||||
|
||||
for (int i = 0; i < numImages; i++) {
|
||||
pageUrlList.add("");
|
||||
}
|
||||
return pageUrlList;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Page> parseFirstPage(List<Page> pages, String unparsedHtml) {
|
||||
Pattern p = Pattern.compile("lstImages.push\\(\"(.+?)\"");
|
||||
Matcher m = p.matcher(unparsedHtml);
|
||||
|
||||
int i = 0;
|
||||
while (m.find()) {
|
||||
pages.get(i++).setImageUrl(m.group(1));
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable<Response> getImageProgressResponse(final Page page) {
|
||||
return networkService.getProgressResponse(page.getImageUrl(), null, page);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
import eu.kanade.tachiyomi.util.Parser;
|
||||
|
||||
public class Mangafox extends Source {
|
||||
|
||||
public static final String NAME = "Mangafox (EN)";
|
||||
public static final String BASE_URL = "http://mangafox.me";
|
||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
|
||||
public static final String SEARCH_URL =
|
||||
BASE_URL + "/search.php?name_method=cw&advopts=1&order=az&sort=name&name=%s&page=%s";
|
||||
|
||||
public Mangafox(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return SourceManager.MANGAFOX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return BASE_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialPopularMangasUrl() {
|
||||
return String.format(POPULAR_MANGAS_URL, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialSearchUrl(String query) {
|
||||
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||
List<Manga> mangaList = new ArrayList<>();
|
||||
|
||||
for (Element currentHtmlBlock : parsedHtml.select("div#mangalist > ul.list > li")) {
|
||||
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
|
||||
mangaList.add(currentManga);
|
||||
}
|
||||
return mangaList;
|
||||
}
|
||||
|
||||
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
|
||||
Manga manga = new Manga();
|
||||
manga.source = getId();
|
||||
|
||||
Element urlElement = Parser.element(htmlBlock, "a.title");
|
||||
if (urlElement != null) {
|
||||
manga.setUrl(urlElement.attr("href"));
|
||||
manga.title = urlElement.text();
|
||||
}
|
||||
return manga;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||
Element next = Parser.element(parsedHtml, "a:has(span.next)");
|
||||
return next != null ? String.format(POPULAR_MANGAS_URL, next.attr("href")) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||
List<Manga> mangaList = new ArrayList<>();
|
||||
|
||||
for (Element currentHtmlBlock : parsedHtml.select("table#listing > tbody > tr:gt(0)")) {
|
||||
Manga currentManga = constructSearchMangaFromHtmlBlock(currentHtmlBlock);
|
||||
mangaList.add(currentManga);
|
||||
}
|
||||
return mangaList;
|
||||
}
|
||||
|
||||
private Manga constructSearchMangaFromHtmlBlock(Element htmlBlock) {
|
||||
Manga mangaFromHtmlBlock = new Manga();
|
||||
mangaFromHtmlBlock.source = getId();
|
||||
|
||||
Element urlElement = Parser.element(htmlBlock, "a.series_preview");
|
||||
if (urlElement != null) {
|
||||
mangaFromHtmlBlock.setUrl(urlElement.attr("href"));
|
||||
mangaFromHtmlBlock.title = urlElement.text();
|
||||
}
|
||||
return mangaFromHtmlBlock;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||
Element next = Parser.element(parsedHtml, "a:has(span.next)");
|
||||
return next != null ? BASE_URL + next.attr("href") : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
|
||||
Element infoElement = parsedDocument.select("div#title").first();
|
||||
Element rowElement = infoElement.select("table > tbody > tr:eq(1)").first();
|
||||
Element sideInfoElement = parsedDocument.select("#series_info").first();
|
||||
|
||||
Manga manga = Manga.create(mangaUrl);
|
||||
manga.author = Parser.text(rowElement, "td:eq(1)");
|
||||
manga.artist = Parser.text(rowElement, "td:eq(2)");
|
||||
manga.description = Parser.text(infoElement, "p.summary");
|
||||
manga.genre = Parser.text(rowElement, "td:eq(3)");
|
||||
manga.thumbnail_url = Parser.src(sideInfoElement, "div.cover > img");
|
||||
manga.status = parseStatus(Parser.text(sideInfoElement, ".data"));
|
||||
|
||||
manga.initialized = true;
|
||||
return manga;
|
||||
}
|
||||
|
||||
private int parseStatus(String status) {
|
||||
if (status.contains("Ongoing")) {
|
||||
return Manga.ONGOING;
|
||||
}
|
||||
if (status.contains("Completed")) {
|
||||
return Manga.COMPLETED;
|
||||
}
|
||||
return Manga.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
|
||||
List<Chapter> chapterList = new ArrayList<>();
|
||||
|
||||
for (Element chapterElement : parsedDocument.select("div#chapters li div")) {
|
||||
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
|
||||
chapterList.add(currentChapter);
|
||||
}
|
||||
return chapterList;
|
||||
}
|
||||
|
||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||
Chapter chapter = Chapter.create();
|
||||
|
||||
Element urlElement = chapterElement.select("a.tips").first();
|
||||
Element dateElement = chapterElement.select("span.date").first();
|
||||
|
||||
if (urlElement != null) {
|
||||
chapter.setUrl(urlElement.attr("href"));
|
||||
chapter.name = urlElement.text();
|
||||
}
|
||||
if (dateElement != null) {
|
||||
chapter.date_upload = parseUpdateFromElement(dateElement);
|
||||
}
|
||||
return chapter;
|
||||
}
|
||||
|
||||
private long parseUpdateFromElement(Element updateElement) {
|
||||
String updatedDateAsString = updateElement.text();
|
||||
|
||||
if (updatedDateAsString.contains("Today")) {
|
||||
Calendar today = Calendar.getInstance();
|
||||
today.set(Calendar.HOUR_OF_DAY, 0);
|
||||
today.set(Calendar.MINUTE, 0);
|
||||
today.set(Calendar.SECOND, 0);
|
||||
today.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
try {
|
||||
Date withoutDay = new SimpleDateFormat("h:mm a", Locale.ENGLISH).parse(updatedDateAsString.replace("Today", ""));
|
||||
return today.getTimeInMillis() + withoutDay.getTime();
|
||||
} catch (ParseException e) {
|
||||
return today.getTimeInMillis();
|
||||
}
|
||||
} else if (updatedDateAsString.contains("Yesterday")) {
|
||||
Calendar yesterday = Calendar.getInstance();
|
||||
yesterday.add(Calendar.DATE, -1);
|
||||
yesterday.set(Calendar.HOUR_OF_DAY, 0);
|
||||
yesterday.set(Calendar.MINUTE, 0);
|
||||
yesterday.set(Calendar.SECOND, 0);
|
||||
yesterday.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
try {
|
||||
Date withoutDay = new SimpleDateFormat("h:mm a", Locale.ENGLISH).parse(updatedDateAsString.replace("Yesterday", ""));
|
||||
return yesterday.getTimeInMillis() + withoutDay.getTime();
|
||||
} catch (ParseException e) {
|
||||
return yesterday.getTimeInMillis();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Date specificDate = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(updatedDateAsString);
|
||||
|
||||
return specificDate.getTime();
|
||||
} catch (ParseException e) {
|
||||
// Do Nothing.
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
|
||||
List<String> pageUrlList = new ArrayList<>();
|
||||
|
||||
Elements pageUrlElements = parsedDocument.select("select.m").first().select("option:not([value=0])");
|
||||
String baseUrl = parsedDocument.select("div#series a").first().attr("href").replace("1.html", "");
|
||||
int counter = 1;
|
||||
for (Element pageUrlElement : pageUrlElements) {
|
||||
if(counter < pageUrlElements.size()) {
|
||||
pageUrlList.add(baseUrl + pageUrlElement.attr("value") + ".html");
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
|
||||
return pageUrlList;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
|
||||
Element imageElement = parsedDocument.getElementById("image");
|
||||
return imageElement.attr("src");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
import eu.kanade.tachiyomi.util.Parser;
|
||||
|
||||
public class Mangahere extends Source {
|
||||
|
||||
public static final String NAME = "Mangahere (EN)";
|
||||
public static final String BASE_URL = "http://www.mangahere.co";
|
||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
|
||||
public static final String SEARCH_URL = BASE_URL + "/search.php?name=%s&page=%s";
|
||||
|
||||
public Mangahere(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return SourceManager.MANGAHERE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return BASE_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialPopularMangasUrl() {
|
||||
return String.format(POPULAR_MANGAS_URL, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialSearchUrl(String query) {
|
||||
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||
List<Manga> mangaList = new ArrayList<>();
|
||||
|
||||
for (Element currentHtmlBlock : parsedHtml.select("div.directory_list > ul > li")) {
|
||||
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
|
||||
mangaList.add(currentManga);
|
||||
}
|
||||
return mangaList;
|
||||
}
|
||||
|
||||
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
|
||||
Manga manga = new Manga();
|
||||
manga.source = getId();
|
||||
|
||||
Element urlElement = Parser.element(htmlBlock, "div.title > a");
|
||||
if (urlElement != null) {
|
||||
manga.setUrl(urlElement.attr("href"));
|
||||
manga.title = urlElement.attr("title");
|
||||
}
|
||||
return manga;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||
Element next = Parser.element(parsedHtml, "div.next-page > a.next");
|
||||
return next != null ? String.format(POPULAR_MANGAS_URL, next.attr("href")) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||
List<Manga> mangaList = new ArrayList<>();
|
||||
|
||||
Elements mangaHtmlBlocks = parsedHtml.select("div.result_search > dl");
|
||||
for (Element currentHtmlBlock : mangaHtmlBlocks) {
|
||||
Manga currentManga = constructSearchMangaFromHtmlBlock(currentHtmlBlock);
|
||||
mangaList.add(currentManga);
|
||||
}
|
||||
return mangaList;
|
||||
}
|
||||
|
||||
private Manga constructSearchMangaFromHtmlBlock(Element htmlBlock) {
|
||||
Manga manga = new Manga();
|
||||
manga.source = getId();
|
||||
|
||||
Element urlElement = Parser.element(htmlBlock, "a.manga_info");
|
||||
if (urlElement != null) {
|
||||
manga.setUrl(urlElement.attr("href"));
|
||||
manga.title = urlElement.text();
|
||||
}
|
||||
return manga;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||
Element next = Parser.element(parsedHtml, "div.next-page > a.next");
|
||||
return next != null ? BASE_URL + next.attr("href") : null;
|
||||
}
|
||||
|
||||
private long parseUpdateFromElement(Element updateElement) {
|
||||
String updatedDateAsString = updateElement.text();
|
||||
|
||||
if (updatedDateAsString.contains("Today")) {
|
||||
Calendar today = Calendar.getInstance();
|
||||
today.set(Calendar.HOUR_OF_DAY, 0);
|
||||
today.set(Calendar.MINUTE, 0);
|
||||
today.set(Calendar.SECOND, 0);
|
||||
today.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
try {
|
||||
Date withoutDay = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString.replace("Today", ""));
|
||||
return today.getTimeInMillis() + withoutDay.getTime();
|
||||
} catch (ParseException e) {
|
||||
return today.getTimeInMillis();
|
||||
}
|
||||
} else if (updatedDateAsString.contains("Yesterday")) {
|
||||
Calendar yesterday = Calendar.getInstance();
|
||||
yesterday.add(Calendar.DATE, -1);
|
||||
yesterday.set(Calendar.HOUR_OF_DAY, 0);
|
||||
yesterday.set(Calendar.MINUTE, 0);
|
||||
yesterday.set(Calendar.SECOND, 0);
|
||||
yesterday.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
try {
|
||||
Date withoutDay = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString.replace("Yesterday", ""));
|
||||
return yesterday.getTimeInMillis() + withoutDay.getTime();
|
||||
} catch (ParseException e) {
|
||||
return yesterday.getTimeInMillis();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Date specificDate = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString);
|
||||
|
||||
return specificDate.getTime();
|
||||
} catch (ParseException e) {
|
||||
// Do Nothing.
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||
int beginIndex = unparsedHtml.indexOf("<ul class=\"detail_topText\">");
|
||||
int endIndex = unparsedHtml.indexOf("</ul>", beginIndex);
|
||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||
|
||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||
Element detailElement = parsedDocument.select("ul.detail_topText").first();
|
||||
|
||||
Manga manga = Manga.create(mangaUrl);
|
||||
manga.author = Parser.text(parsedDocument, "a[href^=http://www.mangahere.co/author/]");
|
||||
manga.artist = Parser.text(parsedDocument, "a[href^=http://www.mangahere.co/artist/]");
|
||||
|
||||
String description = Parser.text(detailElement, "#show");
|
||||
if (description != null) {
|
||||
manga.description = description.substring(0, description.length() - "Show less".length());
|
||||
}
|
||||
String genres = Parser.text(detailElement, "li:eq(3)");
|
||||
if (genres != null) {
|
||||
manga.genre = genres.substring("Genre(s):".length());
|
||||
}
|
||||
manga.status = parseStatus(Parser.text(detailElement, "li:eq(6)"));
|
||||
|
||||
beginIndex = unparsedHtml.indexOf("<img");
|
||||
endIndex = unparsedHtml.indexOf("/>", beginIndex);
|
||||
trimmedHtml = unparsedHtml.substring(beginIndex, endIndex + 2);
|
||||
|
||||
parsedDocument = Jsoup.parse(trimmedHtml);
|
||||
manga.thumbnail_url = Parser.src(parsedDocument, "img");
|
||||
|
||||
manga.initialized = true;
|
||||
return manga;
|
||||
}
|
||||
|
||||
private int parseStatus(String status) {
|
||||
if (status.contains("Ongoing")) {
|
||||
return Manga.ONGOING;
|
||||
}
|
||||
if (status.contains("Completed")) {
|
||||
return Manga.COMPLETED;
|
||||
}
|
||||
return Manga.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||
int beginIndex = unparsedHtml.indexOf("<ul>");
|
||||
int endIndex = unparsedHtml.indexOf("</ul>", beginIndex);
|
||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||
|
||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||
|
||||
List<Chapter> chapterList = new ArrayList<>();
|
||||
|
||||
for (Element chapterElement : parsedDocument.getElementsByTag("li")) {
|
||||
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
|
||||
chapterList.add(currentChapter);
|
||||
}
|
||||
return chapterList;
|
||||
}
|
||||
|
||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||
Chapter chapter = Chapter.create();
|
||||
|
||||
Element urlElement = chapterElement.select("a").first();
|
||||
Element dateElement = chapterElement.select("span.right").first();
|
||||
|
||||
if (urlElement != null) {
|
||||
chapter.setUrl(urlElement.attr("href"));
|
||||
chapter.name = urlElement.text();
|
||||
}
|
||||
if (dateElement != null) {
|
||||
chapter.date_upload = parseDateFromElement(dateElement);
|
||||
}
|
||||
return chapter;
|
||||
}
|
||||
|
||||
private long parseDateFromElement(Element dateElement) {
|
||||
String dateAsString = dateElement.text();
|
||||
|
||||
if (dateAsString.contains("Today")) {
|
||||
Calendar today = Calendar.getInstance();
|
||||
today.set(Calendar.HOUR_OF_DAY, 0);
|
||||
today.set(Calendar.MINUTE, 0);
|
||||
today.set(Calendar.SECOND, 0);
|
||||
today.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
try {
|
||||
Date withoutDay = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString.replace("Today", ""));
|
||||
return today.getTimeInMillis() + withoutDay.getTime();
|
||||
} catch (ParseException e) {
|
||||
return today.getTimeInMillis();
|
||||
}
|
||||
} else if (dateAsString.contains("Yesterday")) {
|
||||
Calendar yesterday = Calendar.getInstance();
|
||||
yesterday.add(Calendar.DATE, -1);
|
||||
yesterday.set(Calendar.HOUR_OF_DAY, 0);
|
||||
yesterday.set(Calendar.MINUTE, 0);
|
||||
yesterday.set(Calendar.SECOND, 0);
|
||||
yesterday.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
try {
|
||||
Date withoutDay = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString.replace("Yesterday", ""));
|
||||
return yesterday.getTimeInMillis() + withoutDay.getTime();
|
||||
} catch (ParseException e) {
|
||||
return yesterday.getTimeInMillis();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Date date = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString);
|
||||
|
||||
return date.getTime();
|
||||
} catch (ParseException e) {
|
||||
// Do Nothing.
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||
int beginIndex = unparsedHtml.indexOf("<div class=\"go_page clearfix\">");
|
||||
int endIndex = unparsedHtml.indexOf("</div>", beginIndex);
|
||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||
|
||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||
|
||||
List<String> pageUrlList = new ArrayList<>();
|
||||
|
||||
Elements pageUrlElements = parsedDocument.select("select.wid60").first().getElementsByTag("option");
|
||||
for (Element pageUrlElement : pageUrlElements) {
|
||||
pageUrlList.add(pageUrlElement.attr("value"));
|
||||
}
|
||||
|
||||
return pageUrlList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parseHtmlToImageUrl(String unparsedHtml) {
|
||||
int beginIndex = unparsedHtml.indexOf("<section class=\"read_img\" id=\"viewer\">");
|
||||
int endIndex = unparsedHtml.indexOf("</section>", beginIndex);
|
||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||
|
||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||
|
||||
Element imageElement = parsedDocument.getElementById("image");
|
||||
|
||||
return imageElement.attr("src");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package eu.kanade.tachiyomi.data.sync;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class LibraryUpdateAlarm extends BroadcastReceiver {
|
||||
|
||||
public static final String LIBRARY_UPDATE_ACTION = "eu.kanade.UPDATE_LIBRARY";
|
||||
|
||||
public static void startAlarm(Context context) {
|
||||
startAlarm(context, PreferencesHelper.getLibraryUpdateInterval(context));
|
||||
}
|
||||
|
||||
public static void startAlarm(Context context, int intervalInHours) {
|
||||
stopAlarm(context);
|
||||
if (intervalInHours == 0)
|
||||
return;
|
||||
|
||||
int intervalInMillis = intervalInHours * 60 * 60 * 1000;
|
||||
long nextRun = SystemClock.elapsedRealtime() + intervalInMillis;
|
||||
|
||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
PendingIntent pendingIntent = getPendingIntent(context);
|
||||
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
nextRun, intervalInMillis, pendingIntent);
|
||||
|
||||
Timber.i("Alarm set. Library will update on " + nextRun);
|
||||
}
|
||||
|
||||
public static void stopAlarm(Context context) {
|
||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
PendingIntent pendingIntent = getPendingIntent(context);
|
||||
alarmManager.cancel(pendingIntent);
|
||||
}
|
||||
|
||||
private static PendingIntent getPendingIntent(Context context) {
|
||||
Intent intent = new Intent(context, LibraryUpdateAlarm.class);
|
||||
intent.setAction(LIBRARY_UPDATE_ACTION);
|
||||
return PendingIntent.getBroadcast(context, 0, intent, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction() == null)
|
||||
return;
|
||||
|
||||
if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
|
||||
startAlarm(context);
|
||||
} else if (intent.getAction().equals(LIBRARY_UPDATE_ACTION)) {
|
||||
LibraryUpdateService.start(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package eu.kanade.tachiyomi.data.sync;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.BuildConfig;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity;
|
||||
import eu.kanade.tachiyomi.util.AndroidComponentUtil;
|
||||
import eu.kanade.tachiyomi.util.NetworkUtil;
|
||||
import rx.Observable;
|
||||
import rx.Subscription;
|
||||
import rx.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class LibraryUpdateService extends Service {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject SourceManager sourceManager;
|
||||
@Inject PreferencesHelper preferences;
|
||||
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
private Subscription subscription;
|
||||
|
||||
public static final int UPDATE_NOTIFICATION_ID = 1;
|
||||
|
||||
public static void start(Context context) {
|
||||
if (!isRunning(context)) {
|
||||
context.startService(getStartIntent(context));
|
||||
}
|
||||
}
|
||||
|
||||
private static Intent getStartIntent(Context context) {
|
||||
return new Intent(context, LibraryUpdateService.class);
|
||||
}
|
||||
|
||||
private static boolean isRunning(Context context) {
|
||||
return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
App.get(this).getComponent().inject(this);
|
||||
createAndAcquireWakeLock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (subscription != null)
|
||||
subscription.unsubscribe();
|
||||
// Reset the alarm
|
||||
LibraryUpdateAlarm.startAlarm(this);
|
||||
destroyWakeLock();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, final int startId) {
|
||||
Timber.i("Starting sync...");
|
||||
|
||||
if (!NetworkUtil.isNetworkConnected(this)) {
|
||||
Timber.i("Sync canceled, connection not available");
|
||||
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable.class, true);
|
||||
stopSelf(startId);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
subscription = Observable.fromCallable(() -> db.getFavoriteMangas().executeAsBlocking())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap(this::updateLibrary)
|
||||
.subscribe(next -> {},
|
||||
error -> {
|
||||
showNotification(getString(R.string.notification_update_error), "");
|
||||
stopSelf(startId);
|
||||
}, () -> {
|
||||
Timber.i("Library updated");
|
||||
stopSelf(startId);
|
||||
});
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private Observable<MangaUpdate> updateLibrary(List<Manga> allLibraryMangas) {
|
||||
final AtomicInteger count = new AtomicInteger(0);
|
||||
final List<MangaUpdate> updates = new ArrayList<>();
|
||||
final List<Manga> failedUpdates = new ArrayList<>();
|
||||
|
||||
final List<Manga> mangas = !preferences.updateOnlyNonCompleted() ? allLibraryMangas :
|
||||
Observable.from(allLibraryMangas)
|
||||
.filter(manga -> manga.status != Manga.COMPLETED)
|
||||
.toList().toBlocking().single();
|
||||
|
||||
return Observable.from(mangas)
|
||||
.doOnNext(manga -> showNotification(
|
||||
getString(R.string.notification_update_progress,
|
||||
count.incrementAndGet(), mangas.size()), manga.title))
|
||||
.concatMap(manga -> updateManga(manga)
|
||||
.onErrorReturn(error -> {
|
||||
failedUpdates.add(manga);
|
||||
return Pair.create(0, 0);
|
||||
})
|
||||
// Filter out mangas without new chapters
|
||||
.filter(pair -> pair.first > 0)
|
||||
.map(pair -> new MangaUpdate(manga, pair.first)))
|
||||
.doOnNext(updates::add)
|
||||
.doOnCompleted(() -> showBigNotification(getString(R.string.notification_update_completed),
|
||||
getUpdatedMangas(updates, failedUpdates)));
|
||||
}
|
||||
|
||||
private Observable<Pair<Integer, Integer>> updateManga(Manga manga) {
|
||||
return sourceManager.get(manga.source)
|
||||
.pullChaptersFromNetwork(manga.url)
|
||||
.flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters));
|
||||
}
|
||||
|
||||
private String getUpdatedMangas(List<MangaUpdate> updates, List<Manga> failedUpdates) {
|
||||
final StringBuilder result = new StringBuilder();
|
||||
if (updates.isEmpty()) {
|
||||
result.append(getString(R.string.notification_no_new_chapters)).append("\n");
|
||||
} else {
|
||||
result.append(getString(R.string.notification_new_chapters));
|
||||
|
||||
for (MangaUpdate update : updates) {
|
||||
result.append("\n").append(update.manga.title);
|
||||
}
|
||||
}
|
||||
if (!failedUpdates.isEmpty()) {
|
||||
result.append("\n");
|
||||
result.append(getString(R.string.notification_manga_update_failed));
|
||||
for (Manga manga : failedUpdates) {
|
||||
result.append("\n").append(manga.title);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void createAndAcquireWakeLock() {
|
||||
wakeLock = ((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock");
|
||||
wakeLock.acquire();
|
||||
}
|
||||
|
||||
private void destroyWakeLock() {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
wakeLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void showNotification(String title, String body) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_action_refresh)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body);
|
||||
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
private void showBigNotification(String title, String body) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_action_refresh)
|
||||
.setContentTitle(title)
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(body))
|
||||
.setContentIntent(getNotificationIntent())
|
||||
.setAutoCancel(true);
|
||||
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
private PendingIntent getNotificationIntent() {
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
public static class SyncOnConnectionAvailable extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (NetworkUtil.isNetworkConnected(context)) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.i("Connection is now available, triggering sync...");
|
||||
}
|
||||
AndroidComponentUtil.toggleComponent(context, this.getClass(), false);
|
||||
context.startService(getStartIntent(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class MangaUpdate {
|
||||
public Manga manga;
|
||||
public int newChapters;
|
||||
|
||||
public MangaUpdate(Manga manga, int newChapters) {
|
||||
this.manga = manga;
|
||||
this.newChapters = newChapters;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package eu.kanade.tachiyomi.data.sync;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import rx.subscriptions.CompositeSubscription;
|
||||
|
||||
public class UpdateMangaSyncService extends Service {
|
||||
|
||||
@Inject MangaSyncManager syncManager;
|
||||
@Inject DatabaseHelper db;
|
||||
|
||||
private CompositeSubscription subscriptions;
|
||||
|
||||
private static final String EXTRA_MANGASYNC = "extra_mangasync";
|
||||
|
||||
public static void start(Context context, MangaSync mangaSync) {
|
||||
Intent intent = new Intent(context, UpdateMangaSyncService.class);
|
||||
intent.putExtra(EXTRA_MANGASYNC, mangaSync);
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
App.get(this).getComponent().inject(this);
|
||||
subscriptions = new CompositeSubscription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
MangaSync mangaSync = (MangaSync) intent.getSerializableExtra(EXTRA_MANGASYNC);
|
||||
updateLastChapterRead(mangaSync, startId);
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
subscriptions.unsubscribe();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void updateLastChapterRead(MangaSync mangaSync, int startId) {
|
||||
MangaSyncService sync = syncManager.getSyncService(mangaSync.sync_id);
|
||||
|
||||
subscriptions.add(Observable.defer(() -> sync.update(mangaSync))
|
||||
.flatMap(response -> {
|
||||
if (response.isSuccessful()) {
|
||||
return db.insertMangaSync(mangaSync).createObservable();
|
||||
}
|
||||
return Observable.error(new Exception("Could not update MAL"));
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
stopSelf(startId);
|
||||
}, error -> {
|
||||
stopSelf(startId);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package eu.kanade.tachiyomi.event;
|
||||
|
||||
public class ChapterCountEvent {
|
||||
private int count;
|
||||
|
||||
public ChapterCountEvent(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package eu.kanade.tachiyomi.event;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
|
||||
public class DownloadChaptersEvent {
|
||||
private Manga manga;
|
||||
private List<Chapter> chapters;
|
||||
|
||||
public DownloadChaptersEvent(Manga manga, List<Chapter> chapters) {
|
||||
this.manga = manga;
|
||||
this.chapters = chapters;
|
||||
}
|
||||
|
||||
public Manga getManga() {
|
||||
return manga;
|
||||
}
|
||||
|
||||
public List<Chapter> getChapters() {
|
||||
return chapters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package eu.kanade.tachiyomi.event;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
|
||||
public class LibraryMangasEvent {
|
||||
|
||||
private final Map<Integer, List<Manga>> mangas;
|
||||
|
||||
public LibraryMangasEvent(Map<Integer, List<Manga>> mangas) {
|
||||
this.mangas = mangas;
|
||||
}
|
||||
|
||||
public Map<Integer, List<Manga>> getMangas() {
|
||||
return mangas;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<Manga> getMangasForCategory(Category category) {
|
||||
return mangas.get(category.id);
|
||||
}
|
||||
}
|
||||
30
app/src/main/java/eu/kanade/tachiyomi/event/ReaderEvent.java
Normal file
30
app/src/main/java/eu/kanade/tachiyomi/event/ReaderEvent.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package eu.kanade.tachiyomi.event;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
|
||||
public class ReaderEvent {
|
||||
|
||||
private Source source;
|
||||
private Manga manga;
|
||||
private Chapter chapter;
|
||||
|
||||
public ReaderEvent(Source source, Manga manga, Chapter chapter) {
|
||||
this.source = source;
|
||||
this.manga = manga;
|
||||
this.chapter = chapter;
|
||||
}
|
||||
|
||||
public Source getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public Manga getManga() {
|
||||
return manga;
|
||||
}
|
||||
|
||||
public Chapter getChapter() {
|
||||
return chapter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package eu.kanade.tachiyomi.injection;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* This class allows to inject into objects through a base class,
|
||||
* so we don't have to repeat injection code everywhere.
|
||||
*
|
||||
* The performance drawback is about 0.013 ms per injection on a very slow device,
|
||||
* which is negligible in most cases.
|
||||
*
|
||||
* Example:
|
||||
* <pre>{@code
|
||||
* Component {
|
||||
* void inject(B b);
|
||||
* }
|
||||
*
|
||||
* class A {
|
||||
* void onCreate() {
|
||||
* componentReflectionInjector.inject(this);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class B extends A {
|
||||
* @Inject MyDependency dependency;
|
||||
* }
|
||||
*
|
||||
* new B().onCreate() // dependency will be injected at this point
|
||||
*
|
||||
* class C extends B {
|
||||
*
|
||||
* }
|
||||
*
|
||||
* new C().onCreate() // dependency will be injected at this point as well
|
||||
* }</pre>
|
||||
*
|
||||
* @param <T> a type of dagger 2 component.
|
||||
*/
|
||||
public final class ComponentReflectionInjector<T> {
|
||||
|
||||
private final Class<T> componentClass;
|
||||
private final T component;
|
||||
private final HashMap<Class<?>, Method> methods;
|
||||
|
||||
public ComponentReflectionInjector(Class<T> componentClass, T component) {
|
||||
this.componentClass = componentClass;
|
||||
this.component = component;
|
||||
this.methods = getMethods(componentClass);
|
||||
}
|
||||
|
||||
public T getComponent() {
|
||||
return component;
|
||||
}
|
||||
|
||||
public void inject(Object target) {
|
||||
|
||||
Class targetClass = target.getClass();
|
||||
Method method = methods.get(targetClass);
|
||||
while (method == null && targetClass != null) {
|
||||
targetClass = targetClass.getSuperclass();
|
||||
method = methods.get(targetClass);
|
||||
}
|
||||
|
||||
if (method == null)
|
||||
throw new RuntimeException(String.format("No %s injecting method exists in %s component", target.getClass(), componentClass));
|
||||
|
||||
try {
|
||||
method.invoke(component, target);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final ConcurrentHashMap<Class<?>, HashMap<Class<?>, Method>> cache = new ConcurrentHashMap<>();
|
||||
|
||||
private static HashMap<Class<?>, Method> getMethods(Class componentClass) {
|
||||
HashMap<Class<?>, Method> methods = cache.get(componentClass);
|
||||
if (methods == null) {
|
||||
synchronized (cache) {
|
||||
methods = cache.get(componentClass);
|
||||
if (methods == null) {
|
||||
methods = new HashMap<>();
|
||||
for (Method method : componentClass.getMethods()) {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
if (params.length == 1)
|
||||
methods.put(params[0], method);
|
||||
}
|
||||
cache.put(componentClass, methods);
|
||||
}
|
||||
}
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package eu.kanade.tachiyomi.injection.component;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Component;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService;
|
||||
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.sync.LibraryUpdateService;
|
||||
import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService;
|
||||
import eu.kanade.tachiyomi.injection.module.AppModule;
|
||||
import eu.kanade.tachiyomi.injection.module.DataModule;
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter;
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadPresenter;
|
||||
import eu.kanade.tachiyomi.ui.library.category.CategoryPresenter;
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter;
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaPresenter;
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter;
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter;
|
||||
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListPresenter;
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter;
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsAccountsFragment;
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsActivity;
|
||||
|
||||
@Singleton
|
||||
@Component(
|
||||
modules = {
|
||||
AppModule.class,
|
||||
DataModule.class
|
||||
}
|
||||
)
|
||||
public interface AppComponent {
|
||||
|
||||
void inject(LibraryPresenter libraryPresenter);
|
||||
void inject(MangaPresenter mangaPresenter);
|
||||
void inject(CataloguePresenter cataloguePresenter);
|
||||
void inject(MangaInfoPresenter mangaInfoPresenter);
|
||||
void inject(ChaptersPresenter chaptersPresenter);
|
||||
void inject(ReaderPresenter readerPresenter);
|
||||
void inject(DownloadPresenter downloadPresenter);
|
||||
void inject(MyAnimeListPresenter myAnimeListPresenter);
|
||||
void inject(CategoryPresenter categoryPresenter);
|
||||
|
||||
void inject(ReaderActivity readerActivity);
|
||||
void inject(MangaActivity mangaActivity);
|
||||
void inject(SettingsAccountsFragment settingsAccountsFragment);
|
||||
void inject(SettingsActivity settingsActivity);
|
||||
|
||||
void inject(Source source);
|
||||
|
||||
void inject(MyAnimeList myAnimeList);
|
||||
|
||||
void inject(LibraryUpdateService libraryUpdateService);
|
||||
void inject(DownloadService downloadService);
|
||||
void inject(UpdateMangaSyncService updateMangaSyncService);
|
||||
|
||||
Application application();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package eu.kanade.tachiyomi.injection.module;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
/**
|
||||
* Provide application-level dependencies. Mainly singleton object that can be injected from
|
||||
* anywhere in the app.
|
||||
*/
|
||||
@Module
|
||||
public class AppModule {
|
||||
protected final Application mApplication;
|
||||
|
||||
public AppModule(Application application) {
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Application provideApplication() {
|
||||
return mApplication;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package eu.kanade.tachiyomi.injection.module;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache;
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache;
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager;
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
|
||||
/**
|
||||
* Provide dependencies to the DataManager, mainly Helper classes and Retrofit services.
|
||||
*/
|
||||
@Module
|
||||
public class DataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
PreferencesHelper providePreferencesHelper(Application app) {
|
||||
return new PreferencesHelper(app);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
DatabaseHelper provideDatabaseHelper(Application app) {
|
||||
return new DatabaseHelper(app);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
ChapterCache provideChapterCache(Application app) {
|
||||
return new ChapterCache(app);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
CoverCache provideCoverCache(Application app) {
|
||||
return new CoverCache(app);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
NetworkHelper provideNetworkHelper() {
|
||||
return new NetworkHelper();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
SourceManager provideSourceManager(Application app) {
|
||||
return new SourceManager(app);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
DownloadManager provideDownloadManager(
|
||||
Application app, SourceManager sourceManager, PreferencesHelper preferences) {
|
||||
return new DownloadManager(app, sourceManager, preferences);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MangaSyncManager provideMangaSyncManager(Application app) {
|
||||
return new MangaSyncManager(app);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package eu.kanade.tachiyomi.ui.base.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import icepick.Icepick;
|
||||
|
||||
public class BaseActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
Icepick.restoreInstanceState(this, savedState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
protected void setupToolbar(Toolbar toolbar) {
|
||||
setSupportActionBar(toolbar);
|
||||
if (getSupportActionBar() != null)
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
public void setToolbarTitle(String title) {
|
||||
if (getSupportActionBar() != null)
|
||||
getSupportActionBar().setTitle(title);
|
||||
}
|
||||
|
||||
public void setToolbarTitle(int titleResource) {
|
||||
if (getSupportActionBar() != null)
|
||||
getSupportActionBar().setTitle(getString(titleResource));
|
||||
}
|
||||
|
||||
public void setToolbarSubtitle(String title) {
|
||||
if (getSupportActionBar() != null)
|
||||
getSupportActionBar().setSubtitle(title);
|
||||
}
|
||||
|
||||
public void setToolbarSubtitle(int titleResource) {
|
||||
if (getSupportActionBar() != null)
|
||||
getSupportActionBar().setSubtitle(getString(titleResource));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void registerForStickyEvents() {
|
||||
registerForStickyEvents(0);
|
||||
}
|
||||
|
||||
public void registerForStickyEvents(int priority) {
|
||||
EventBus.getDefault().registerSticky(this, priority);
|
||||
}
|
||||
|
||||
public void registerForEvents() {
|
||||
registerForEvents(0);
|
||||
}
|
||||
|
||||
public void registerForEvents(int priority) {
|
||||
EventBus.getDefault().register(this, priority);
|
||||
}
|
||||
|
||||
public void unregisterForEvents() {
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package eu.kanade.tachiyomi.ui.base.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import nucleus.factory.PresenterFactory;
|
||||
import nucleus.factory.ReflectionPresenterFactory;
|
||||
import nucleus.presenter.Presenter;
|
||||
import nucleus.view.PresenterLifecycleDelegate;
|
||||
import nucleus.view.ViewWithPresenter;
|
||||
|
||||
|
||||
/**
|
||||
* This class is an example of how an activity could controls it's presenter.
|
||||
* You can inherit from this class or copy/paste this class's code to
|
||||
* create your own view implementation.
|
||||
*
|
||||
* @param <P> a type of presenter to return with {@link #getPresenter}.
|
||||
*/
|
||||
public abstract class BaseRxActivity<P extends Presenter> extends BaseActivity implements ViewWithPresenter<P> {
|
||||
|
||||
private static final String PRESENTER_STATE_KEY = "presenter_state";
|
||||
|
||||
private PresenterLifecycleDelegate<P> presenterDelegate =
|
||||
new PresenterLifecycleDelegate<>(ReflectionPresenterFactory.<P>fromViewClass(getClass()));
|
||||
|
||||
/**
|
||||
* Returns a current presenter factory.
|
||||
*/
|
||||
public PresenterFactory<P> getPresenterFactory() {
|
||||
return presenterDelegate.getPresenterFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a presenter factory.
|
||||
* Call this method before onCreate/onFinishInflate to override default {@link ReflectionPresenterFactory} presenter factory.
|
||||
* Use this method for presenter dependency injection.
|
||||
*/
|
||||
@Override
|
||||
public void setPresenterFactory(PresenterFactory<P> presenterFactory) {
|
||||
presenterDelegate.setPresenterFactory(presenterFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a current attached presenter.
|
||||
* This method is guaranteed to return a non-null value between
|
||||
* onResume/onPause and onAttachedToWindow/onDetachedFromWindow calls
|
||||
* if the presenter factory returns a non-null value.
|
||||
*
|
||||
* @return a currently attached presenter or null.
|
||||
*/
|
||||
public P getPresenter() {
|
||||
return presenterDelegate.getPresenter();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
final PresenterFactory<P> superFactory = getPresenterFactory();
|
||||
setPresenterFactory(() -> {
|
||||
P presenter = superFactory.createPresenter();
|
||||
App app = (App) getApplication();
|
||||
app.getComponentReflection().inject(presenter);
|
||||
((BasePresenter)presenter).setContext(app.getApplicationContext());
|
||||
return presenter;
|
||||
});
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null)
|
||||
presenterDelegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBundle(PRESENTER_STATE_KEY, presenterDelegate.onSaveInstanceState());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
presenterDelegate.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
presenterDelegate.onPause(isFinishing());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package eu.kanade.tachiyomi.ui.base.adapter;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
|
||||
public abstract class FlexibleViewHolder extends RecyclerView.ViewHolder
|
||||
implements View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
private final FlexibleAdapter adapter;
|
||||
private final OnListItemClickListener onListItemClickListener;
|
||||
|
||||
public FlexibleViewHolder(View itemView,FlexibleAdapter adapter,
|
||||
OnListItemClickListener onListItemClickListener) {
|
||||
super(itemView);
|
||||
this.adapter = adapter;
|
||||
|
||||
this.onListItemClickListener = onListItemClickListener;
|
||||
|
||||
this.itemView.setOnClickListener(this);
|
||||
this.itemView.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (onListItemClickListener.onListItemClick(getAdapterPosition())) {
|
||||
toggleActivation();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
onListItemClickListener.onListItemLongClick(getAdapterPosition());
|
||||
toggleActivation();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void toggleActivation() {
|
||||
itemView.setActivated(adapter.isSelected(getAdapterPosition()));
|
||||
}
|
||||
|
||||
public interface OnListItemClickListener {
|
||||
boolean onListItemClick(int position);
|
||||
void onListItemLongClick(int position);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Paul Burke
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.kanade.tachiyomi.ui.base.adapter;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
|
||||
/**
|
||||
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
||||
*
|
||||
* @author Paul Burke (ipaulpro)
|
||||
*/
|
||||
public interface ItemTouchHelperAdapter {
|
||||
|
||||
/**
|
||||
* Called when an item has been dragged far enough to trigger a move. This is called every time
|
||||
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
|
||||
* <br/>
|
||||
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
|
||||
* adjusting the underlying data to reflect this move.
|
||||
*
|
||||
* @param fromPosition The start position of the moved item.
|
||||
* @param toPosition Then resolved position of the moved item.
|
||||
*
|
||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||
*/
|
||||
void onItemMove(int fromPosition, int toPosition);
|
||||
|
||||
|
||||
/**
|
||||
* Called when an item has been dismissed by a swipe.<br/>
|
||||
* <br/>
|
||||
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
|
||||
* adjusting the underlying data to reflect this removal.
|
||||
*
|
||||
* @param position The position of the item dismissed.
|
||||
*
|
||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||
*/
|
||||
void onItemDismiss(int position);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package eu.kanade.tachiyomi.ui.base.adapter;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
public interface OnStartDragListener {
|
||||
|
||||
/**
|
||||
* Called when a view is requesting a start of a drag.
|
||||
*
|
||||
* @param viewHolder The holder of the view to drag.
|
||||
*/
|
||||
void onStartDrag(RecyclerView.ViewHolder viewHolder);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package eu.kanade.tachiyomi.ui.base.adapter;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
|
||||
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
|
||||
private final ItemTouchHelperAdapter adapter;
|
||||
|
||||
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
||||
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
||||
return makeMovementFlags(dragFlags, swipeFlags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
|
||||
RecyclerView.ViewHolder target) {
|
||||
adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
adapter.onItemDismiss(viewHolder.getAdapterPosition());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package eu.kanade.tachiyomi.ui.base.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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public List<Fragment> getRegisteredFragments() {
|
||||
ArrayList<Fragment> fragments = new ArrayList<>();
|
||||
for (int i = 0; i < registeredFragments.size(); i++) {
|
||||
fragments.add(registeredFragments.valueAt(i));
|
||||
}
|
||||
return fragments;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package eu.kanade.tachiyomi.ui.base.fab;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.design.widget.CoordinatorLayout;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
|
||||
public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,
|
||||
final View directTargetChild, final View target, final int nestedScrollAxes) {
|
||||
// Ensure we react to vertical scrolling
|
||||
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
|
||||
|| super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,
|
||||
final View target, final int dxConsumed, final int dyConsumed,
|
||||
final int dxUnconsumed, final int dyUnconsumed) {
|
||||
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
|
||||
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
|
||||
// User scrolled down and the FAB is currently visible -> hide the FAB
|
||||
child.hide();
|
||||
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
|
||||
// User scrolled up and the FAB is currently not visible -> show the FAB
|
||||
child.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package eu.kanade.tachiyomi.ui.base.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity;
|
||||
import icepick.Icepick;
|
||||
|
||||
public class BaseFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
Icepick.restoreInstanceState(this, savedState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
public void setToolbarTitle(String title) {
|
||||
getBaseActivity().setToolbarTitle(title);
|
||||
}
|
||||
|
||||
public void setToolbarTitle(int resourceId) {
|
||||
getBaseActivity().setToolbarTitle(getString(resourceId));
|
||||
}
|
||||
|
||||
public BaseActivity getBaseActivity() {
|
||||
return (BaseActivity) getActivity();
|
||||
}
|
||||
|
||||
public void registerForStickyEvents() {
|
||||
registerForStickyEvents(0);
|
||||
}
|
||||
|
||||
public void registerForStickyEvents(int priority) {
|
||||
EventBus.getDefault().registerSticky(this, priority);
|
||||
}
|
||||
|
||||
public void registerForEvents() {
|
||||
registerForEvents(0);
|
||||
}
|
||||
|
||||
public void registerForEvents(int priority) {
|
||||
EventBus.getDefault().register(this, priority);
|
||||
}
|
||||
|
||||
public void unregisterForEvents() {
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package eu.kanade.tachiyomi.ui.base.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import nucleus.factory.PresenterFactory;
|
||||
import nucleus.factory.ReflectionPresenterFactory;
|
||||
import nucleus.presenter.Presenter;
|
||||
import nucleus.view.PresenterLifecycleDelegate;
|
||||
import nucleus.view.ViewWithPresenter;
|
||||
|
||||
/**
|
||||
* This class is an example of how an activity could controls it's presenter.
|
||||
* You can inherit from this class or copy/paste this class's code to
|
||||
* create your own view implementation.
|
||||
*
|
||||
* @param <P> a type of presenter to return with {@link #getPresenter}.
|
||||
*/
|
||||
public abstract class BaseRxFragment<P extends Presenter> extends BaseFragment implements ViewWithPresenter<P> {
|
||||
|
||||
private static final String PRESENTER_STATE_KEY = "presenter_state";
|
||||
private PresenterLifecycleDelegate<P> presenterDelegate =
|
||||
new PresenterLifecycleDelegate<>(ReflectionPresenterFactory.<P>fromViewClass(getClass()));
|
||||
|
||||
/**
|
||||
* Returns a current presenter factory.
|
||||
*/
|
||||
public PresenterFactory<P> getPresenterFactory() {
|
||||
return presenterDelegate.getPresenterFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a presenter factory.
|
||||
* Call this method before onCreate/onFinishInflate to override default {@link ReflectionPresenterFactory} presenter factory.
|
||||
* Use this method for presenter dependency injection.
|
||||
*/
|
||||
@Override
|
||||
public void setPresenterFactory(PresenterFactory<P> presenterFactory) {
|
||||
presenterDelegate.setPresenterFactory(presenterFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a current attached presenter.
|
||||
* This method is guaranteed to return a non-null value between
|
||||
* onResume/onPause and onAttachedToWindow/onDetachedFromWindow calls
|
||||
* if the presenter factory returns a non-null value.
|
||||
*
|
||||
* @return a currently attached presenter or null.
|
||||
*/
|
||||
public P getPresenter() {
|
||||
return presenterDelegate.getPresenter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
final PresenterFactory<P> superFactory = getPresenterFactory();
|
||||
setPresenterFactory(() -> {
|
||||
P presenter = superFactory.createPresenter();
|
||||
App app = (App) getActivity().getApplication();
|
||||
app.getComponentReflection().inject(presenter);
|
||||
((BasePresenter)presenter).setContext(app.getApplicationContext());
|
||||
return presenter;
|
||||
});
|
||||
|
||||
super.onCreate(bundle);
|
||||
if (bundle != null)
|
||||
presenterDelegate.onRestoreInstanceState(bundle.getBundle(PRESENTER_STATE_KEY));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
super.onSaveInstanceState(bundle);
|
||||
bundle.putBundle(PRESENTER_STATE_KEY, presenterDelegate.onSaveInstanceState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
presenterDelegate.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
presenterDelegate.onPause(getActivity().isFinishing());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package eu.kanade.tachiyomi.ui.base.presenter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import icepick.Icepick;
|
||||
import nucleus.view.ViewWithPresenter;
|
||||
|
||||
public class BasePresenter<V extends ViewWithPresenter> extends RxPresenter<V> {
|
||||
|
||||
private Context context;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
Icepick.restoreInstanceState(this, savedState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSave(@NonNull Bundle state) {
|
||||
super.onSave(state);
|
||||
Icepick.saveInstanceState(this, state);
|
||||
}
|
||||
|
||||
public void registerForStickyEvents() {
|
||||
EventBus.getDefault().registerSticky(this);
|
||||
}
|
||||
|
||||
public void registerForEvents() {
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
public void unregisterForEvents() {
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
public void setContext(Context applicationContext) {
|
||||
context = applicationContext;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package eu.kanade.tachiyomi.ui.base.presenter;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.CallSuper;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import nucleus.presenter.Presenter;
|
||||
import nucleus.presenter.delivery.DeliverFirst;
|
||||
import nucleus.presenter.delivery.DeliverLatestCache;
|
||||
import nucleus.presenter.delivery.DeliverReplay;
|
||||
import nucleus.presenter.delivery.Delivery;
|
||||
import rx.Observable;
|
||||
import rx.Subscription;
|
||||
import rx.functions.Action1;
|
||||
import rx.functions.Action2;
|
||||
import rx.functions.Func0;
|
||||
import rx.internal.util.SubscriptionList;
|
||||
import rx.subjects.BehaviorSubject;
|
||||
|
||||
/**
|
||||
* This is an extension of {@link Presenter} which provides RxJava functionality.
|
||||
*
|
||||
* @param <View> a type of view.
|
||||
*/
|
||||
public class RxPresenter<View> extends Presenter<View> {
|
||||
|
||||
private static final String REQUESTED_KEY = RxPresenter.class.getName() + "#requested";
|
||||
|
||||
private final BehaviorSubject<View> views = BehaviorSubject.create();
|
||||
private final SubscriptionList subscriptions = new SubscriptionList();
|
||||
|
||||
private final HashMap<Integer, Func0<Subscription>> restartables = new HashMap<>();
|
||||
private final HashMap<Integer, Subscription> restartableSubscriptions = new HashMap<>();
|
||||
private final ArrayList<Integer> requested = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Returns an {@link rx.Observable} that emits the current attached view or null.
|
||||
* See {@link BehaviorSubject} for more information.
|
||||
*
|
||||
* @return an observable that emits the current attached view or null.
|
||||
*/
|
||||
public Observable<View> view() {
|
||||
return views;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a subscription to automatically unsubscribe it during onDestroy.
|
||||
* See {@link SubscriptionList#add(Subscription) for details.}
|
||||
*
|
||||
* @param subscription a subscription to add.
|
||||
*/
|
||||
public void add(Subscription subscription) {
|
||||
subscriptions.add(subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and unsubscribes a subscription that has been registered with {@link #add} previously.
|
||||
* See {@link SubscriptionList#remove(Subscription)} for details.
|
||||
*
|
||||
* @param subscription a subscription to remove.
|
||||
*/
|
||||
public void remove(Subscription subscription) {
|
||||
subscriptions.remove(subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* A restartable is any RxJava observable that can be started (subscribed) and
|
||||
* should be automatically restarted (re-subscribed) after a process restart if
|
||||
* it was still subscribed at the moment of saving presenter's state.
|
||||
*
|
||||
* Registers a factory. Re-subscribes the restartable after the process restart.
|
||||
*
|
||||
* @param restartableId id of the restartable
|
||||
* @param factory factory of the restartable
|
||||
*/
|
||||
public void restartable(int restartableId, Func0<Subscription> factory) {
|
||||
restartables.put(restartableId, factory);
|
||||
if (requested.contains(restartableId))
|
||||
start(restartableId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the given restartable.
|
||||
*
|
||||
* @param restartableId id of the restartable
|
||||
*/
|
||||
public void start(int restartableId) {
|
||||
stop(restartableId);
|
||||
requested.add(restartableId);
|
||||
restartableSubscriptions.put(restartableId, restartables.get(restartableId).call());
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes a restartable
|
||||
*
|
||||
* @param restartableId id of a restartable.
|
||||
*/
|
||||
public void stop(int restartableId) {
|
||||
requested.remove((Integer) restartableId);
|
||||
Subscription subscription = restartableSubscriptions.get(restartableId);
|
||||
if (subscription != null)
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a restartable is subscribed.
|
||||
*
|
||||
* @param restartableId id of a restartable.
|
||||
* @return True if the restartable is subscribed, false otherwise.
|
||||
*/
|
||||
public boolean isSubscribed(int restartableId) {
|
||||
Subscription s = restartableSubscriptions.get(restartableId);
|
||||
return s != null && !s.isUnsubscribed();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut that can be used instead of combining together
|
||||
* {@link #restartable(int, Func0)},
|
||||
* {@link #deliverFirst()},
|
||||
* {@link #split(Action2, Action2)}.
|
||||
*
|
||||
* @param restartableId an id of the restartable.
|
||||
* @param observableFactory a factory that should return an Observable when the restartable should run.
|
||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
||||
* @param onError a callback that will be called if the source observable emits onError.
|
||||
* @param <T> the type of the observable.
|
||||
*/
|
||||
public <T> void restartableFirst(int restartableId, final Func0<Observable<T>> observableFactory,
|
||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
||||
|
||||
restartable(restartableId, new Func0<Subscription>() {
|
||||
@Override
|
||||
public Subscription call() {
|
||||
return observableFactory.call()
|
||||
.compose(RxPresenter.this.<T>deliverFirst())
|
||||
.subscribe(split(onNext, onError));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut for calling {@link #restartableFirst(int, Func0, Action2, Action2)} with the last parameter = null.
|
||||
*/
|
||||
public <T> void restartableFirst(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
||||
restartableFirst(restartableId, observableFactory, onNext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut that can be used instead of combining together
|
||||
* {@link #restartable(int, Func0)},
|
||||
* {@link #deliverLatestCache()},
|
||||
* {@link #split(Action2, Action2)}.
|
||||
*
|
||||
* @param restartableId an id of the restartable.
|
||||
* @param observableFactory a factory that should return an Observable when the restartable should run.
|
||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
||||
* @param onError a callback that will be called if the source observable emits onError.
|
||||
* @param <T> the type of the observable.
|
||||
*/
|
||||
public <T> void restartableLatestCache(int restartableId, final Func0<Observable<T>> observableFactory,
|
||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
||||
|
||||
restartable(restartableId, new Func0<Subscription>() {
|
||||
@Override
|
||||
public Subscription call() {
|
||||
return observableFactory.call()
|
||||
.compose(RxPresenter.this.<T>deliverLatestCache())
|
||||
.subscribe(split(onNext, onError));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut for calling {@link #restartableLatestCache(int, Func0, Action2, Action2)} with the last parameter = null.
|
||||
*/
|
||||
public <T> void restartableLatestCache(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
||||
restartableLatestCache(restartableId, observableFactory, onNext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut that can be used instead of combining together
|
||||
* {@link #restartable(int, Func0)},
|
||||
* {@link #deliverReplay()},
|
||||
* {@link #split(Action2, Action2)}.
|
||||
*
|
||||
* @param restartableId an id of the restartable.
|
||||
* @param observableFactory a factory that should return an Observable when the restartable should run.
|
||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
||||
* @param onError a callback that will be called if the source observable emits onError.
|
||||
* @param <T> the type of the observable.
|
||||
*/
|
||||
public <T> void restartableReplay(int restartableId, final Func0<Observable<T>> observableFactory,
|
||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
||||
|
||||
restartable(restartableId, new Func0<Subscription>() {
|
||||
@Override
|
||||
public Subscription call() {
|
||||
return observableFactory.call()
|
||||
.compose(RxPresenter.this.<T>deliverReplay())
|
||||
.subscribe(split(onNext, onError));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut for calling {@link #restartableReplay(int, Func0, Action2, Action2)} with the last parameter = null.
|
||||
*/
|
||||
public <T> void restartableReplay(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
||||
restartableReplay(restartableId, observableFactory, onNext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
|
||||
* the source {@link rx.Observable}.
|
||||
*
|
||||
* {@link #deliverLatestCache} keeps the latest onNext value and emits it each time a new view gets attached.
|
||||
* If a new onNext value appears while a view is attached, it will be delivered immediately.
|
||||
*
|
||||
* @param <T> the type of source observable emissions
|
||||
*/
|
||||
public <T> DeliverLatestCache<View, T> deliverLatestCache() {
|
||||
return new DeliverLatestCache<>(views);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
|
||||
* the source {@link rx.Observable}.
|
||||
*
|
||||
* {@link #deliverFirst} delivers only the first onNext value that has been emitted by the source observable.
|
||||
*
|
||||
* @param <T> the type of source observable emissions
|
||||
*/
|
||||
public <T> DeliverFirst<View, T> deliverFirst() {
|
||||
return new DeliverFirst<>(views);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
|
||||
* the source {@link rx.Observable}.
|
||||
*
|
||||
* {@link #deliverReplay} keeps all onNext values and emits them each time a new view gets attached.
|
||||
* If a new onNext value appears while a view is attached, it will be delivered immediately.
|
||||
*
|
||||
* @param <T> the type of source observable emissions
|
||||
*/
|
||||
public <T> DeliverReplay<View, T> deliverReplay() {
|
||||
return new DeliverReplay<>(views);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a method that can be used for manual restartable chain build. It returns an Action1 that splits
|
||||
* a received {@link Delivery} into two {@link Action2} onNext and onError calls.
|
||||
*
|
||||
* @param onNext a method that will be called if the delivery contains an emitted onNext value.
|
||||
* @param onError a method that will be called if the delivery contains an onError throwable.
|
||||
* @param <T> a type on onNext value.
|
||||
* @return an Action1 that splits a received {@link Delivery} into two {@link Action2} onNext and onError calls.
|
||||
*/
|
||||
public <T> Action1<Delivery<View, T>> split(final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
||||
return new Action1<Delivery<View, T>>() {
|
||||
@Override
|
||||
public void call(Delivery<View, T> delivery) {
|
||||
delivery.split(onNext, onError);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut for calling {@link #split(Action2, Action2)} when the second parameter is null.
|
||||
*/
|
||||
public <T> Action1<Delivery<View, T>> split(Action2<View, T> onNext) {
|
||||
return split(onNext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
if (savedState != null)
|
||||
requested.addAll(savedState.getIntegerArrayList(REQUESTED_KEY));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
views.onCompleted();
|
||||
subscriptions.unsubscribe();
|
||||
for (Map.Entry<Integer, Subscription> entry : restartableSubscriptions.entrySet())
|
||||
entry.getValue().unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onSave(Bundle state) {
|
||||
for (int i = requested.size() - 1; i >= 0; i--) {
|
||||
int restartableId = requested.get(i);
|
||||
Subscription subscription = restartableSubscriptions.get(restartableId);
|
||||
if (subscription != null && subscription.isUnsubscribed())
|
||||
requested.remove(i);
|
||||
}
|
||||
state.putIntegerArrayList(REQUESTED_KEY, requested);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onTakeView(View view) {
|
||||
views.onNext(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onDropView() {
|
||||
views.onNext(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Please, use restartableXX and deliverXX methods for pushing data from RxPresenter into View.
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
@Override
|
||||
public View getView() {
|
||||
return super.getView();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
|
||||
public class CatalogueAdapter extends FlexibleAdapter<CatalogueHolder, Manga> {
|
||||
|
||||
private CatalogueFragment fragment;
|
||||
|
||||
public CatalogueAdapter(CatalogueFragment fragment) {
|
||||
this.fragment = fragment;
|
||||
mItems = new ArrayList<>();
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
public void addItems(List<Manga> list) {
|
||||
mItems.addAll(list);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mItems.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mItems.get(position).id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDataSet(String param) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public CatalogueHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = fragment.getActivity().getLayoutInflater();
|
||||
View v = inflater.inflate(R.layout.item_catalogue, parent, false);
|
||||
return new CatalogueHolder(v, this, fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(CatalogueHolder holder, int position) {
|
||||
final Manga manga = getItem(position);
|
||||
holder.onSetValues(manga, fragment.getPresenter());
|
||||
|
||||
//When user scrolls this bind the correct selection status
|
||||
//holder.itemView.setActivated(isSelected(position));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity;
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView;
|
||||
import eu.kanade.tachiyomi.widget.EndlessRecyclerScrollListener;
|
||||
import icepick.State;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.subjects.PublishSubject;
|
||||
|
||||
@RequiresPresenter(CataloguePresenter.class)
|
||||
public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
|
||||
implements FlexibleViewHolder.OnListItemClickListener {
|
||||
|
||||
@Bind(R.id.recycler) AutofitRecyclerView recycler;
|
||||
@Bind(R.id.progress) ProgressBar progress;
|
||||
@Bind(R.id.progress_grid) ProgressBar progressGrid;
|
||||
|
||||
private Toolbar toolbar;
|
||||
private Spinner spinner;
|
||||
private CatalogueAdapter adapter;
|
||||
private EndlessRecyclerScrollListener scrollListener;
|
||||
|
||||
@State String query = "";
|
||||
@State int selectedIndex = -1;
|
||||
private final int SEARCH_TIMEOUT = 1000;
|
||||
|
||||
private PublishSubject<String> queryDebouncerSubject;
|
||||
private Subscription queryDebouncerSubscription;
|
||||
|
||||
public static CatalogueFragment newInstance() {
|
||||
return new CatalogueFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
|
||||
// Inflate the layout for this fragment
|
||||
View view = inflater.inflate(R.layout.fragment_catalogue, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
// Initialize adapter and scroll listener
|
||||
GridLayoutManager layoutManager = (GridLayoutManager) recycler.getLayoutManager();
|
||||
adapter = new CatalogueAdapter(this);
|
||||
scrollListener = new EndlessRecyclerScrollListener(layoutManager, this::requestNextPage);
|
||||
recycler.setHasFixedSize(true);
|
||||
recycler.setAdapter(adapter);
|
||||
recycler.addOnScrollListener(scrollListener);
|
||||
|
||||
// Create toolbar spinner
|
||||
Context themedContext = getBaseActivity().getSupportActionBar() != null ?
|
||||
getBaseActivity().getSupportActionBar().getThemedContext() : getActivity();
|
||||
spinner = new Spinner(themedContext);
|
||||
CatalogueSpinnerAdapter spinnerAdapter = new CatalogueSpinnerAdapter(themedContext,
|
||||
android.R.layout.simple_spinner_item, getPresenter().getEnabledSources());
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
if (savedState == null) selectedIndex = spinnerAdapter.getEmptyIndex();
|
||||
spinner.setAdapter(spinnerAdapter);
|
||||
spinner.setSelection(selectedIndex);
|
||||
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
Source source = spinnerAdapter.getItem(position);
|
||||
// We add an empty source with id -1 that acts as a placeholder to show a hint
|
||||
// that asks to select a source
|
||||
if (source.getId() != -1 && (selectedIndex != position || adapter.isEmpty())) {
|
||||
// Set previous selection if it's not a valid source and notify the user
|
||||
if (!getPresenter().isValidSource(source)) {
|
||||
spinner.setSelection(spinnerAdapter.getEmptyIndex());
|
||||
ToastUtil.showShort(getActivity(), R.string.source_requires_login);
|
||||
} else {
|
||||
selectedIndex = position;
|
||||
showProgressBar();
|
||||
getPresenter().startRequesting(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
});
|
||||
|
||||
setToolbarTitle("");
|
||||
toolbar = ((MainActivity)getActivity()).getToolbar();
|
||||
toolbar.addView(spinner);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.catalogue_list, menu);
|
||||
|
||||
// Initialize search menu
|
||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
||||
final SearchView searchView = (SearchView) searchItem.getActionView();
|
||||
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
searchItem.expandActionView();
|
||||
searchView.setQuery(query, true);
|
||||
searchView.clearFocus();
|
||||
}
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
onSearchEvent(query, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
onSearchEvent(newText, false);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeSearchSubscription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
destroySearchSubscription();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
toolbar.removeView(spinner);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void initializeSearchSubscription() {
|
||||
queryDebouncerSubject = PublishSubject.create();
|
||||
queryDebouncerSubscription = queryDebouncerSubject
|
||||
.debounce(SEARCH_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::restartRequest);
|
||||
}
|
||||
|
||||
private void destroySearchSubscription() {
|
||||
queryDebouncerSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
private void onSearchEvent(String query, boolean now) {
|
||||
// If the query is not debounced, resolve it instantly
|
||||
if (now)
|
||||
restartRequest(query);
|
||||
else if (queryDebouncerSubject != null)
|
||||
queryDebouncerSubject.onNext(query);
|
||||
}
|
||||
|
||||
private void restartRequest(String newQuery) {
|
||||
// If text didn't change, do nothing
|
||||
if (query.equals(newQuery)) return;
|
||||
|
||||
query = newQuery;
|
||||
showProgressBar();
|
||||
recycler.getLayoutManager().scrollToPosition(0);
|
||||
|
||||
getPresenter().restartRequest(query);
|
||||
}
|
||||
|
||||
private void requestNextPage() {
|
||||
if (getPresenter().hasNextPage()) {
|
||||
showGridProgressBar();
|
||||
getPresenter().requestNext();
|
||||
}
|
||||
}
|
||||
|
||||
public void onAddPage(int page, List<Manga> mangas) {
|
||||
hideProgressBar();
|
||||
if (page == 1) {
|
||||
adapter.clear();
|
||||
scrollListener.resetScroll();
|
||||
}
|
||||
adapter.addItems(mangas);
|
||||
}
|
||||
|
||||
public void onAddPageError() {
|
||||
hideProgressBar();
|
||||
}
|
||||
|
||||
public void updateImage(Manga manga) {
|
||||
CatalogueHolder holder = getHolder(manga);
|
||||
if (holder != null) {
|
||||
holder.setImage(manga, getPresenter());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private CatalogueHolder getHolder(Manga manga) {
|
||||
return (CatalogueHolder) recycler.findViewHolderForItemId(manga.id);
|
||||
}
|
||||
|
||||
private void showProgressBar() {
|
||||
progress.setVisibility(ProgressBar.VISIBLE);
|
||||
}
|
||||
|
||||
private void showGridProgressBar() {
|
||||
progressGrid.setVisibility(ProgressBar.VISIBLE);
|
||||
}
|
||||
|
||||
private void hideProgressBar() {
|
||||
progress.setVisibility(ProgressBar.GONE);
|
||||
progressGrid.setVisibility(ProgressBar.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListItemClick(int position) {
|
||||
final Manga selectedManga = adapter.getItem(position);
|
||||
|
||||
Intent intent = MangaActivity.newIntent(getActivity(), selectedManga);
|
||||
intent.putExtra(MangaActivity.MANGA_ONLINE, true);
|
||||
startActivity(intent);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemLongClick(int position) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
|
||||
public class CatalogueHolder extends FlexibleViewHolder {
|
||||
|
||||
@Bind(R.id.title) TextView title;
|
||||
@Bind(R.id.thumbnail) ImageView thumbnail;
|
||||
@Bind(R.id.favorite_sticker) ImageView favoriteSticker;
|
||||
|
||||
public CatalogueHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) {
|
||||
super(view, adapter, listener);
|
||||
ButterKnife.bind(this, view);
|
||||
}
|
||||
|
||||
public void onSetValues(Manga manga, CataloguePresenter presenter) {
|
||||
title.setText(manga.title);
|
||||
favoriteSticker.setVisibility(manga.favorite ? View.VISIBLE : View.GONE);
|
||||
setImage(manga, presenter);
|
||||
}
|
||||
|
||||
public void setImage(Manga manga, CataloguePresenter presenter) {
|
||||
if (manga.thumbnail_url != null) {
|
||||
presenter.coverCache.loadFromNetwork(thumbnail, manga.thumbnail_url,
|
||||
presenter.getSource().getGlideHeaders());
|
||||
} else {
|
||||
thumbnail.setImageResource(android.R.color.transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.RxPager;
|
||||
import icepick.State;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import rx.subjects.PublishSubject;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
|
||||
|
||||
@Inject SourceManager sourceManager;
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject CoverCache coverCache;
|
||||
@Inject PreferencesHelper prefs;
|
||||
|
||||
private Source source;
|
||||
@State int sourceId;
|
||||
|
||||
private String query;
|
||||
|
||||
private int currentPage;
|
||||
private RxPager pager;
|
||||
private MangasPage lastMangasPage;
|
||||
|
||||
private PublishSubject<List<Manga>> mangaDetailSubject;
|
||||
|
||||
private static final int GET_MANGA_LIST = 1;
|
||||
private static final int GET_MANGA_DETAIL = 2;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
mangaDetailSubject = PublishSubject.create();
|
||||
|
||||
restartableReplay(GET_MANGA_LIST,
|
||||
() -> pager.pages().concatMap(page -> getMangasPageObservable(page + 1)),
|
||||
(view, pair) -> view.onAddPage(pair.first, pair.second),
|
||||
(view, error) -> {
|
||||
view.onAddPageError();
|
||||
Timber.e(error.getMessage());
|
||||
});
|
||||
|
||||
restartableLatestCache(GET_MANGA_DETAIL,
|
||||
() -> mangaDetailSubject
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap(Observable::from)
|
||||
.filter(manga -> !manga.initialized)
|
||||
.window(3)
|
||||
.concatMap(pack -> pack.concatMap(this::getMangaDetails))
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread()),
|
||||
CatalogueFragment::updateImage,
|
||||
(view, error) -> Timber.e(error.getMessage()));
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
source = sourceManager.get(sourceId);
|
||||
stop(GET_MANGA_LIST);
|
||||
stop(GET_MANGA_DETAIL);
|
||||
}
|
||||
|
||||
public void startRequesting(Source source) {
|
||||
this.source = source;
|
||||
sourceId = source.getId();
|
||||
restartRequest(null);
|
||||
}
|
||||
|
||||
public void restartRequest(String query) {
|
||||
this.query = query;
|
||||
stop(GET_MANGA_LIST);
|
||||
currentPage = 1;
|
||||
pager = new RxPager();
|
||||
|
||||
start(GET_MANGA_DETAIL);
|
||||
start(GET_MANGA_LIST);
|
||||
}
|
||||
|
||||
public void requestNext() {
|
||||
if (hasNextPage())
|
||||
pager.requestNext(++currentPage);
|
||||
}
|
||||
|
||||
private Observable<Pair<Integer, List<Manga>>> getMangasPageObservable(int page) {
|
||||
MangasPage nextMangasPage = new MangasPage(page);
|
||||
if (page != 1) {
|
||||
nextMangasPage.url = lastMangasPage.nextPageUrl;
|
||||
}
|
||||
|
||||
Observable<MangasPage> obs = !TextUtils.isEmpty(query) ?
|
||||
source.searchMangasFromNetwork(nextMangasPage, query) :
|
||||
source.pullPopularMangasFromNetwork(nextMangasPage);
|
||||
|
||||
return obs.subscribeOn(Schedulers.io())
|
||||
.doOnNext(mangasPage -> lastMangasPage = mangasPage)
|
||||
.flatMap(mangasPage -> Observable.from(mangasPage.mangas))
|
||||
.map(this::networkToLocalManga)
|
||||
.toList()
|
||||
.map(mangas -> Pair.create(page, mangas))
|
||||
.doOnNext(pair -> {
|
||||
if (mangaDetailSubject != null)
|
||||
mangaDetailSubject.onNext(pair.second);
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
private Manga networkToLocalManga(Manga networkManga) {
|
||||
Manga localManga = db.getManga(networkManga.url, source.getId()).executeAsBlocking();
|
||||
if (localManga == null) {
|
||||
PutResult result = db.insertManga(networkManga).executeAsBlocking();
|
||||
networkManga.id = result.insertedId();
|
||||
localManga = networkManga;
|
||||
}
|
||||
return localManga;
|
||||
}
|
||||
|
||||
private Observable<Manga> getMangaDetails(final Manga manga) {
|
||||
return source.pullMangaFromNetwork(manga.url)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap(networkManga -> {
|
||||
manga.copyFrom(networkManga);
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
return Observable.just(manga);
|
||||
})
|
||||
.onErrorResumeNext(error -> Observable.just(manga));
|
||||
}
|
||||
|
||||
public Source getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public boolean hasNextPage() {
|
||||
return lastMangasPage != null && lastMangasPage.nextPageUrl != null;
|
||||
}
|
||||
|
||||
public boolean isValidSource(Source source) {
|
||||
if (!source.isLoginRequired() || source.isLogged())
|
||||
return true;
|
||||
|
||||
return !(prefs.getSourceUsername(source).equals("")
|
||||
|| prefs.getSourcePassword(source).equals(""));
|
||||
}
|
||||
|
||||
public List<Source> getEnabledSources() {
|
||||
// TODO filter by enabled source
|
||||
return sourceManager.getSources();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
|
||||
public class CatalogueSpinnerAdapter extends ArrayAdapter<Source> {
|
||||
|
||||
public CatalogueSpinnerAdapter(Context context, int resource, List<Source> sources) {
|
||||
super(context, resource, sources);
|
||||
sources.add(new SimpleSource());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
|
||||
View v = super.getView(position, convertView, parent);
|
||||
if (position == getCount()) {
|
||||
((TextView)v.findViewById(android.R.id.text1)).setText("");
|
||||
((TextView)v.findViewById(android.R.id.text1)).setHint(getItem(getCount()).getName());
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return super.getCount()-1; // you dont display last item. It is used as hint.
|
||||
}
|
||||
|
||||
public int getEmptyIndex() {
|
||||
return getCount();
|
||||
}
|
||||
|
||||
private class SimpleSource extends Source {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return getContext().getString(R.string.select_source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoginRequired() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialPopularMangasUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialSearchUrl(String query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package eu.kanade.tachiyomi.ui.decoration;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.view.View;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.Canvas;
|
||||
|
||||
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
|
||||
|
||||
private Drawable mDivider;
|
||||
|
||||
public DividerItemDecoration(Context context, AttributeSet attrs) {
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, new int [] { android.R.attr.listDivider });
|
||||
mDivider = a.getDrawable(0);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
public DividerItemDecoration(Drawable divider) { mDivider = divider; }
|
||||
|
||||
@Override
|
||||
public void getItemOffsets (Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
|
||||
super.getItemOffsets(outRect, view, parent, state);
|
||||
if (mDivider == null) return;
|
||||
if (parent.getChildPosition(view) < 1) return;
|
||||
|
||||
if (getOrientation(parent) == LinearLayoutManager.VERTICAL) outRect.top = mDivider.getIntrinsicHeight();
|
||||
else outRect.left = mDivider.getIntrinsicWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
|
||||
if (mDivider == null) { super.onDrawOver(c, parent, state); return; }
|
||||
|
||||
if (getOrientation(parent) == LinearLayoutManager.VERTICAL) {
|
||||
final int left = parent.getPaddingLeft();
|
||||
final int right = parent.getWidth() - parent.getPaddingRight();
|
||||
final int childCount = parent.getChildCount();
|
||||
final int dividerHeight = mDivider.getIntrinsicHeight();
|
||||
|
||||
for (int i=1; i < childCount; i++) {
|
||||
final View child = parent.getChildAt(i);
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
|
||||
final int ty = (int)(child.getTranslationY() + 0.5f);
|
||||
final int top = child.getTop() - params.topMargin + ty;
|
||||
final int bottom = top + dividerHeight;
|
||||
mDivider.setBounds(left, top, right, bottom);
|
||||
mDivider.draw(c);
|
||||
}
|
||||
} else { //horizontal
|
||||
final int top = parent.getPaddingTop();
|
||||
final int bottom = parent.getHeight() - parent.getPaddingBottom();
|
||||
final int childCount = parent.getChildCount();
|
||||
|
||||
for (int i=1; i < childCount; i++) {
|
||||
final View child = parent.getChildAt(i);
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
|
||||
final int size = mDivider.getIntrinsicWidth();
|
||||
final int left = child.getLeft() - params.leftMargin;
|
||||
final int right = left + size;
|
||||
mDivider.setBounds(left, top, right, bottom);
|
||||
mDivider.draw(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getOrientation(RecyclerView parent) {
|
||||
if (parent.getLayoutManager() instanceof LinearLayoutManager) {
|
||||
LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
|
||||
return layoutManager.getOrientation();
|
||||
} else throw new IllegalStateException("DividerItemDecoration can only be used with a LinearLayoutManager.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package eu.kanade.tachiyomi.ui.download;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
|
||||
public class DownloadAdapter extends FlexibleAdapter<DownloadHolder, Download> {
|
||||
|
||||
private Context context;
|
||||
|
||||
public DownloadAdapter(Context context) {
|
||||
this.context = context;
|
||||
mItems = new ArrayList<>();
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.item_download, parent, false);
|
||||
return new DownloadHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(DownloadHolder holder, int position) {
|
||||
final Download download = getItem(position);
|
||||
holder.onSetValues(download);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return getItem(position).chapter.id;
|
||||
}
|
||||
|
||||
public void setItems(List<Download> downloads) {
|
||||
mItems = downloads;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDataSet(String param) {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package eu.kanade.tachiyomi.ui.download;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
import rx.Subscription;
|
||||
|
||||
@RequiresPresenter(DownloadPresenter.class)
|
||||
public class DownloadFragment extends BaseRxFragment<DownloadPresenter> {
|
||||
|
||||
@Bind(R.id.download_list) RecyclerView recyclerView;
|
||||
private DownloadAdapter adapter;
|
||||
|
||||
private MenuItem startButton;
|
||||
private MenuItem stopButton;
|
||||
|
||||
private Subscription queueStatusSubscription;
|
||||
private boolean isRunning;
|
||||
|
||||
public static DownloadFragment newInstance() {
|
||||
return new DownloadFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
// Inflate the layout for this fragment
|
||||
View view = inflater.inflate(R.layout.fragment_download_queue, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
setToolbarTitle(R.string.label_download_queue);
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
recyclerView.setHasFixedSize(true);
|
||||
createAdapter();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.download_queue, menu);
|
||||
startButton = menu.findItem(R.id.start_queue);
|
||||
stopButton = menu.findItem(R.id.stop_queue);
|
||||
|
||||
// Menu seems to be inflated after onResume in fragments, so we initialize them here
|
||||
startButton.setVisible(!isRunning && !getPresenter().downloadManager.getQueue().isEmpty());
|
||||
stopButton.setVisible(isRunning);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.start_queue:
|
||||
DownloadService.start(getActivity());
|
||||
break;
|
||||
case R.id.stop_queue:
|
||||
DownloadService.stop(getActivity());
|
||||
break;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
queueStatusSubscription = getPresenter().downloadManager.getRunningSubject()
|
||||
.subscribe(this::onRunningChange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
queueStatusSubscription.unsubscribe();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
private void onRunningChange(boolean running) {
|
||||
isRunning = running;
|
||||
if (startButton != null)
|
||||
startButton.setVisible(!running && !getPresenter().downloadManager.getQueue().isEmpty());
|
||||
if (stopButton != null)
|
||||
stopButton.setVisible(running);
|
||||
}
|
||||
|
||||
private void createAdapter() {
|
||||
adapter = new DownloadAdapter(getActivity());
|
||||
recyclerView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public void onNextDownloads(List<Download> downloads) {
|
||||
adapter.setItems(downloads);
|
||||
}
|
||||
|
||||
public void updateProgress(Download download) {
|
||||
DownloadHolder holder = getHolder(download);
|
||||
if (holder != null) {
|
||||
holder.setDownloadProgress(download);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateDownloadedPages(Download download) {
|
||||
DownloadHolder holder = getHolder(download);
|
||||
if (holder != null) {
|
||||
holder.setDownloadedPages(download);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private DownloadHolder getHolder(Download download) {
|
||||
return (DownloadHolder) recyclerView.findViewHolderForItemId(download.chapter.id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package eu.kanade.tachiyomi.ui.download;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
|
||||
public class DownloadHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@Bind(R.id.download_title) TextView downloadTitle;
|
||||
@Bind(R.id.download_progress) ProgressBar downloadProgress;
|
||||
@Bind(R.id.download_progress_text) TextView downloadProgressText;
|
||||
|
||||
public DownloadHolder(View view) {
|
||||
super(view);
|
||||
ButterKnife.bind(this, view);
|
||||
}
|
||||
|
||||
public void onSetValues(Download download) {
|
||||
downloadTitle.setText(download.chapter.name);
|
||||
|
||||
if (download.pages == null) {
|
||||
downloadProgress.setProgress(0);
|
||||
downloadProgress.setMax(1);
|
||||
downloadProgressText.setText("");
|
||||
} else {
|
||||
downloadProgress.setMax(download.pages.size() * 100);
|
||||
setDownloadProgress(download);
|
||||
setDownloadedPages(download);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDownloadedPages(Download download) {
|
||||
String progressText = download.downloadedImages + "/" + download.pages.size();
|
||||
downloadProgressText.setText(progressText);
|
||||
}
|
||||
|
||||
public void setDownloadProgress(Download download) {
|
||||
if (downloadProgress.getMax() == 1)
|
||||
downloadProgress.setMax(download.pages.size() * 100);
|
||||
downloadProgress.setProgress(download.totalProgress);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package eu.kanade.tachiyomi.ui.download;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import rx.Observable;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class DownloadPresenter extends BasePresenter<DownloadFragment> {
|
||||
|
||||
@Inject DownloadManager downloadManager;
|
||||
|
||||
private DownloadQueue downloadQueue;
|
||||
private Subscription statusSubscription;
|
||||
private Subscription pageProgressSubscription;
|
||||
private HashMap<Download, Subscription> progressSubscriptions;
|
||||
|
||||
public final static int GET_DOWNLOAD_QUEUE = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
downloadQueue = downloadManager.getQueue();
|
||||
progressSubscriptions = new HashMap<>();
|
||||
|
||||
restartableLatestCache(GET_DOWNLOAD_QUEUE,
|
||||
() -> Observable.just(downloadQueue),
|
||||
DownloadFragment::onNextDownloads,
|
||||
(view, error) -> Timber.e(error.getMessage()));
|
||||
|
||||
if (savedState == null)
|
||||
start(GET_DOWNLOAD_QUEUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTakeView(DownloadFragment view) {
|
||||
super.onTakeView(view);
|
||||
|
||||
add(statusSubscription = downloadQueue.getStatusObservable()
|
||||
.startWith(downloadQueue.getActiveDownloads())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(download -> {
|
||||
processStatus(download, view);
|
||||
}));
|
||||
|
||||
add(pageProgressSubscription = downloadQueue.getProgressObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(view::updateDownloadedPages));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDropView() {
|
||||
destroySubscriptions();
|
||||
super.onDropView();
|
||||
}
|
||||
|
||||
private void processStatus(Download download, DownloadFragment view) {
|
||||
switch (download.getStatus()) {
|
||||
case Download.DOWNLOADING:
|
||||
observeProgress(download, view);
|
||||
// Initial update of the downloaded pages
|
||||
view.updateDownloadedPages(download);
|
||||
break;
|
||||
case Download.DOWNLOADED:
|
||||
unsubscribeProgress(download);
|
||||
view.updateProgress(download);
|
||||
view.updateDownloadedPages(download);
|
||||
break;
|
||||
case Download.ERROR:
|
||||
unsubscribeProgress(download);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void observeProgress(Download download, DownloadFragment view) {
|
||||
Subscription subscription = Observable.interval(50, TimeUnit.MILLISECONDS, Schedulers.newThread())
|
||||
.flatMap(tick -> Observable.from(download.pages)
|
||||
.map(Page::getProgress)
|
||||
.reduce((x, y) -> x + y))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(progress -> {
|
||||
if (download.totalProgress != progress) {
|
||||
download.totalProgress = progress;
|
||||
view.updateProgress(download);
|
||||
}
|
||||
});
|
||||
|
||||
// Avoid leaking subscriptions
|
||||
Subscription oldSubscription = progressSubscriptions.remove(download);
|
||||
if (oldSubscription != null) oldSubscription.unsubscribe();
|
||||
|
||||
progressSubscriptions.put(download, subscription);
|
||||
}
|
||||
|
||||
private void unsubscribeProgress(Download download) {
|
||||
Subscription subscription = progressSubscriptions.remove(download);
|
||||
if (subscription != null)
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
|
||||
private void destroySubscriptions() {
|
||||
for (Subscription subscription : progressSubscriptions.values()) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
progressSubscriptions.clear();
|
||||
|
||||
remove(pageProgressSubscription);
|
||||
remove(statusSubscription);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package eu.kanade.tachiyomi.ui.library;
|
||||
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.SmartFragmentStatePagerAdapter;
|
||||
|
||||
public class LibraryAdapter extends SmartFragmentStatePagerAdapter {
|
||||
|
||||
protected List<Category> categories;
|
||||
|
||||
public LibraryAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
return LibraryCategoryFragment.newInstance(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return categories == null ? 0 : categories.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return categories.get(position).name;
|
||||
}
|
||||
|
||||
public void setCategories(List<Category> categories) {
|
||||
this.categories = categories;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setSelectionMode(int mode) {
|
||||
for (Fragment fragment : getRegisteredFragments()) {
|
||||
((LibraryCategoryFragment) fragment).setMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package eu.kanade.tachiyomi.ui.library;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import rx.Observable;
|
||||
|
||||
public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga>
|
||||
implements Filterable {
|
||||
|
||||
List<Manga> mangas;
|
||||
Filter filter;
|
||||
private LibraryCategoryFragment fragment;
|
||||
|
||||
public LibraryCategoryAdapter(LibraryCategoryFragment fragment) {
|
||||
this.fragment = fragment;
|
||||
mItems = new ArrayList<>();
|
||||
filter = new LibraryFilter();
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
public void setItems(List<Manga> list) {
|
||||
mItems = list;
|
||||
notifyDataSetChanged();
|
||||
|
||||
// TODO needed for filtering?
|
||||
mangas = list;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mItems.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mItems.get(position).id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDataSet(String param) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public LibraryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(fragment.getActivity()).inflate(R.layout.item_catalogue, parent, false);
|
||||
return new LibraryHolder(v, this, fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(LibraryHolder holder, int position) {
|
||||
final LibraryPresenter presenter = ((LibraryFragment) fragment.getParentFragment()).getPresenter();
|
||||
final Manga manga = getItem(position);
|
||||
holder.onSetValues(manga, presenter);
|
||||
|
||||
//When user scrolls this bind the correct selection status
|
||||
holder.itemView.setActivated(isSelected(position));
|
||||
}
|
||||
|
||||
public int getCoverHeight() {
|
||||
return fragment.recycler.getItemWidth() / 9 * 12;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
private class LibraryFilter extends Filter {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence charSequence) {
|
||||
FilterResults results = new FilterResults();
|
||||
String query = charSequence.toString().toLowerCase();
|
||||
|
||||
if (query.length() == 0) {
|
||||
results.values = mangas;
|
||||
results.count = mangas.size();
|
||||
} else {
|
||||
List<Manga> filteredMangas = Observable.from(mangas)
|
||||
.filter(x ->
|
||||
(x.title != null && x.title.toLowerCase().contains(query)) ||
|
||||
(x.author != null && x.author.toLowerCase().contains(query)) ||
|
||||
(x.artist != null && x.artist.toLowerCase().contains(query)))
|
||||
.toList()
|
||||
.toBlocking()
|
||||
.single();
|
||||
results.values = filteredMangas;
|
||||
results.count = filteredMangas.size();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishResults(CharSequence constraint, FilterResults results) {
|
||||
setItems((List<Manga>) results.values);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package eu.kanade.tachiyomi.ui.library;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.f2prateek.rx.preferences.Preference;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.event.LibraryMangasEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView;
|
||||
import icepick.State;
|
||||
import rx.Subscription;
|
||||
|
||||
public class LibraryCategoryFragment extends BaseFragment
|
||||
implements FlexibleViewHolder.OnListItemClickListener {
|
||||
|
||||
@Bind(R.id.library_mangas) AutofitRecyclerView recycler;
|
||||
|
||||
@State int position;
|
||||
private LibraryCategoryAdapter adapter;
|
||||
|
||||
private Subscription numColumnsSubscription;
|
||||
|
||||
public static LibraryCategoryFragment newInstance(int position) {
|
||||
LibraryCategoryFragment fragment = new LibraryCategoryFragment();
|
||||
fragment.position = position;
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
|
||||
// Inflate the layout for this fragment
|
||||
View view = inflater.inflate(R.layout.fragment_library_category, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
adapter = new LibraryCategoryAdapter(this);
|
||||
recycler.setHasFixedSize(true);
|
||||
recycler.setAdapter(adapter);
|
||||
|
||||
if (getLibraryFragment().getActionMode() != null) {
|
||||
setMode(FlexibleAdapter.MODE_MULTI);
|
||||
}
|
||||
|
||||
Preference<Integer> columnsPref = getResources().getConfiguration()
|
||||
.orientation == Configuration.ORIENTATION_PORTRAIT ?
|
||||
getLibraryPresenter().preferences.portraitColumns() :
|
||||
getLibraryPresenter().preferences.landscapeColumns();
|
||||
|
||||
numColumnsSubscription = columnsPref.asObservable()
|
||||
.doOnNext(recycler::setSpanCount)
|
||||
.skip(1)
|
||||
// Set again the adapter to recalculate the covers height
|
||||
.subscribe(count -> recycler.setAdapter(adapter));
|
||||
|
||||
if (savedState != null) {
|
||||
adapter.onRestoreInstanceState(savedState);
|
||||
|
||||
if (adapter.getMode() == FlexibleAdapter.MODE_SINGLE) {
|
||||
adapter.clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
numColumnsSubscription.unsubscribe();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
registerForStickyEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
unregisterForEvents();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
adapter.onSaveInstanceState(outState);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(LibraryMangasEvent event) {
|
||||
List<Category> categories = getLibraryFragment().getAdapter().categories;
|
||||
// When a category is deleted, the index can be greater than the number of categories
|
||||
if (position >= categories.size())
|
||||
return;
|
||||
|
||||
Category category = categories.get(position);
|
||||
List<Manga> mangas = event.getMangasForCategory(category);
|
||||
if (mangas == null) {
|
||||
mangas = new ArrayList<>();
|
||||
}
|
||||
setMangas(mangas);
|
||||
}
|
||||
|
||||
protected void openManga(Manga manga) {
|
||||
getLibraryPresenter().onOpenManga(manga);
|
||||
Intent intent = MangaActivity.newIntent(getActivity(), manga);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void setMangas(List<Manga> mangas) {
|
||||
if (mangas != null) {
|
||||
adapter.setItems(mangas);
|
||||
} else {
|
||||
adapter.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListItemClick(int position) {
|
||||
if (getLibraryFragment().getActionMode() != null && position != -1) {
|
||||
toggleSelection(position);
|
||||
return true;
|
||||
} else {
|
||||
openManga(adapter.getItem(position));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemLongClick(int position) {
|
||||
getLibraryFragment().createActionModeIfNeeded();
|
||||
toggleSelection(position);
|
||||
}
|
||||
|
||||
private void toggleSelection(int position) {
|
||||
LibraryFragment f = getLibraryFragment();
|
||||
|
||||
adapter.toggleSelection(position, false);
|
||||
f.getPresenter().setSelection(adapter.getItem(position), adapter.isSelected(position));
|
||||
|
||||
int count = f.getPresenter().selectedMangas.size();
|
||||
if (count == 0) {
|
||||
f.destroyActionModeIfNeeded();
|
||||
} else {
|
||||
f.setContextTitle(count);
|
||||
f.invalidateActionMode();
|
||||
}
|
||||
}
|
||||
|
||||
public void setMode(int mode) {
|
||||
adapter.setMode(mode);
|
||||
if (mode == FlexibleAdapter.MODE_SINGLE) {
|
||||
adapter.clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private LibraryFragment getLibraryFragment() {
|
||||
return (LibraryFragment) getParentFragment();
|
||||
}
|
||||
|
||||
private LibraryPresenter getLibraryPresenter() {
|
||||
return getLibraryFragment().getPresenter();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package eu.kanade.tachiyomi.ui.library;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.AppBarLayout;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.view.ActionMode;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.sync.LibraryUpdateService;
|
||||
import eu.kanade.tachiyomi.event.LibraryMangasEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||
import eu.kanade.tachiyomi.ui.library.category.CategoryActivity;
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity;
|
||||
import icepick.State;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
|
||||
@RequiresPresenter(LibraryPresenter.class)
|
||||
public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
implements ActionMode.Callback {
|
||||
|
||||
@Bind(R.id.view_pager) ViewPager viewPager;
|
||||
private TabLayout tabs;
|
||||
private AppBarLayout appBar;
|
||||
|
||||
protected LibraryAdapter adapter;
|
||||
|
||||
private ActionMode actionMode;
|
||||
|
||||
@State int activeCategory;
|
||||
|
||||
public static LibraryFragment newInstance() {
|
||||
return new LibraryFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
// Inflate the layout for this fragment
|
||||
View view = inflater.inflate(R.layout.fragment_library, container, false);
|
||||
setToolbarTitle(getString(R.string.label_library));
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
appBar = ((MainActivity) getActivity()).getAppBar();
|
||||
tabs = (TabLayout) inflater.inflate(R.layout.library_tab_layout, appBar, false);
|
||||
appBar.addView(tabs);
|
||||
|
||||
adapter = new LibraryAdapter(getChildFragmentManager());
|
||||
viewPager.setAdapter(adapter);
|
||||
tabs.setupWithViewPager(viewPager);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
appBar.removeView(tabs);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
activeCategory = viewPager.getCurrentItem();
|
||||
super.onSaveInstanceState(bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.library, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_refresh:
|
||||
LibraryUpdateService.start(getActivity());
|
||||
return true;
|
||||
case R.id.action_edit_categories:
|
||||
onEditCategories();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void onEditCategories() {
|
||||
Intent intent = CategoryActivity.newIntent(getActivity());
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void onNextLibraryUpdate(List<Category> categories, Map<Integer, List<Manga>> mangas) {
|
||||
boolean hasMangasInDefaultCategory = mangas.get(0) != null;
|
||||
int activeCat = adapter.categories != null ? viewPager.getCurrentItem() : activeCategory;
|
||||
|
||||
if (hasMangasInDefaultCategory) {
|
||||
setCategoriesWithDefault(categories);
|
||||
} else {
|
||||
setCategories(categories);
|
||||
}
|
||||
// Restore active category
|
||||
viewPager.setCurrentItem(activeCat, false);
|
||||
if (tabs.getTabCount() > 0) {
|
||||
TabLayout.Tab tab = tabs.getTabAt(viewPager.getCurrentItem());
|
||||
if (tab != null) tab.select();
|
||||
}
|
||||
|
||||
// Send the mangas to child fragments after the adapter is updated
|
||||
EventBus.getDefault().postSticky(new LibraryMangasEvent(mangas));
|
||||
}
|
||||
|
||||
private void setCategoriesWithDefault(List<Category> categories) {
|
||||
List<Category> categoriesWithDefault = new ArrayList<>();
|
||||
categoriesWithDefault.add(Category.createDefault());
|
||||
categoriesWithDefault.addAll(categories);
|
||||
|
||||
setCategories(categoriesWithDefault);
|
||||
}
|
||||
|
||||
private void setCategories(List<Category> categories) {
|
||||
adapter.setCategories(categories);
|
||||
tabs.setTabsFromPagerAdapter(adapter);
|
||||
tabs.setVisibility(categories.size() <= 1 ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
public void setContextTitle(int count) {
|
||||
actionMode.setTitle(getString(R.string.label_selected, count));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.library_selection, menu);
|
||||
adapter.setSelectionMode(FlexibleAdapter.MODE_MULTI);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_move_to_category:
|
||||
moveMangasToCategories(getPresenter().selectedMangas);
|
||||
return true;
|
||||
case R.id.action_delete:
|
||||
getPresenter().deleteMangas();
|
||||
destroyActionModeIfNeeded();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
adapter.setSelectionMode(FlexibleAdapter.MODE_SINGLE);
|
||||
getPresenter().selectedMangas.clear();
|
||||
actionMode = null;
|
||||
}
|
||||
|
||||
public void destroyActionModeIfNeeded() {
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void moveMangasToCategories(List<Manga> mangas) {
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.action_move_category)
|
||||
.items(getPresenter().getCategoriesNames())
|
||||
.itemsCallbackMultiChoice(null, (dialog, which, text) -> {
|
||||
getPresenter().moveMangasToCategories(which, mangas);
|
||||
destroyActionModeIfNeeded();
|
||||
return true;
|
||||
})
|
||||
.positiveText(R.string.button_ok)
|
||||
.negativeText(R.string.button_cancel)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ActionMode getActionMode() {
|
||||
return actionMode;
|
||||
}
|
||||
|
||||
public LibraryAdapter getAdapter() {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
public void createActionModeIfNeeded() {
|
||||
if (actionMode == null) {
|
||||
actionMode = getBaseActivity().startSupportActionMode(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void invalidateActionMode() {
|
||||
actionMode.invalidate();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package eu.kanade.tachiyomi.ui.library;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.widget.RelativeLayout.LayoutParams;
|
||||
|
||||
public class LibraryHolder extends FlexibleViewHolder {
|
||||
|
||||
@Bind(R.id.thumbnail) ImageView thumbnail;
|
||||
@Bind(R.id.title) TextView title;
|
||||
@Bind(R.id.unreadText) TextView unreadText;
|
||||
|
||||
public LibraryHolder(View view, LibraryCategoryAdapter adapter, OnListItemClickListener listener) {
|
||||
super(view, adapter, listener);
|
||||
ButterKnife.bind(this, view);
|
||||
thumbnail.setLayoutParams(new LayoutParams(MATCH_PARENT, adapter.getCoverHeight()));
|
||||
}
|
||||
|
||||
public void onSetValues(Manga manga, LibraryPresenter presenter) {
|
||||
title.setText(manga.title);
|
||||
|
||||
if (manga.unread > 0) {
|
||||
unreadText.setVisibility(View.VISIBLE);
|
||||
unreadText.setText(Integer.toString(manga.unread));
|
||||
} else {
|
||||
unreadText.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
loadCover(manga, presenter.sourceManager.get(manga.source), presenter.coverCache);
|
||||
}
|
||||
|
||||
private void loadCover(Manga manga, Source source, CoverCache coverCache) {
|
||||
if (manga.thumbnail_url != null) {
|
||||
coverCache.saveAndLoadFromCache(thumbnail, manga.thumbnail_url, source.getGlideHeaders());
|
||||
} else {
|
||||
thumbnail.setImageResource(android.R.color.transparent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package eu.kanade.tachiyomi.ui.library;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.event.LibraryMangasEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
|
||||
public class LibraryPresenter extends BasePresenter<LibraryFragment> {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject PreferencesHelper preferences;
|
||||
@Inject CoverCache coverCache;
|
||||
@Inject SourceManager sourceManager;
|
||||
|
||||
protected List<Category> categories;
|
||||
protected List<Manga> selectedMangas;
|
||||
|
||||
private static final int GET_LIBRARY = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
selectedMangas = new ArrayList<>();
|
||||
|
||||
restartableLatestCache(GET_LIBRARY,
|
||||
this::getLibraryObservable,
|
||||
(view, pair) -> view.onNextLibraryUpdate(pair.first, pair.second));
|
||||
|
||||
if (savedState == null) {
|
||||
start(GET_LIBRARY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTakeView(LibraryFragment libraryFragment) {
|
||||
super.onTakeView(libraryFragment);
|
||||
if (!isSubscribed(GET_LIBRARY)) {
|
||||
start(GET_LIBRARY);
|
||||
}
|
||||
}
|
||||
|
||||
private Observable<Pair<List<Category>, Map<Integer, List<Manga>>>> getLibraryObservable() {
|
||||
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
|
||||
Pair::create)
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
private Observable<List<Category>> getCategoriesObservable() {
|
||||
return db.getCategories().createObservable()
|
||||
.doOnNext(categories -> this.categories = categories);
|
||||
}
|
||||
|
||||
private Observable<Map<Integer, List<Manga>>> getLibraryMangasObservable() {
|
||||
return db.getLibraryMangas().createObservable()
|
||||
.flatMap(mangas -> Observable.from(mangas)
|
||||
.groupBy(manga -> manga.category)
|
||||
.flatMap(group -> group.toList()
|
||||
.map(list -> Pair.create(group.getKey(), list)))
|
||||
.toMap(pair -> pair.first, pair -> pair.second));
|
||||
}
|
||||
|
||||
public void onOpenManga(Manga manga) {
|
||||
// Avoid further db updates for the library when it's not needed
|
||||
stop(GET_LIBRARY);
|
||||
}
|
||||
|
||||
public void setSelection(Manga manga, boolean selected) {
|
||||
if (selected) {
|
||||
selectedMangas.add(manga);
|
||||
} else {
|
||||
selectedMangas.remove(manga);
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getCategoriesNames() {
|
||||
int count = categories.size();
|
||||
String[] names = new String[count];
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
names[i] = categories.get(i).name;
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
public void deleteMangas() {
|
||||
for (Manga manga : selectedMangas) {
|
||||
manga.favorite = false;
|
||||
}
|
||||
|
||||
db.insertMangas(selectedMangas).executeAsBlocking();
|
||||
}
|
||||
|
||||
public void moveMangasToCategories(Integer[] positions, List<Manga> mangas) {
|
||||
List<Category> categoriesToAdd = new ArrayList<>();
|
||||
for (Integer index : positions) {
|
||||
categoriesToAdd.add(categories.get(index));
|
||||
}
|
||||
|
||||
moveMangasToCategories(categoriesToAdd, mangas);
|
||||
}
|
||||
|
||||
public void moveMangasToCategories(List<Category> categories, List<Manga> mangas) {
|
||||
List<MangaCategory> mc = new ArrayList<>();
|
||||
|
||||
for (Manga manga : mangas) {
|
||||
for (Category cat : categories) {
|
||||
mc.add(MangaCategory.create(manga, cat));
|
||||
}
|
||||
}
|
||||
|
||||
db.setMangaCategories(mc, mangas);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package eu.kanade.tachiyomi.ui.library.category;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.content.res.ResourcesCompat;
|
||||
import android.support.v7.view.ActionMode;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.OnStartDragListener;
|
||||
import eu.kanade.tachiyomi.ui.decoration.DividerItemDecoration;
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
import rx.Observable;
|
||||
|
||||
@RequiresPresenter(CategoryPresenter.class)
|
||||
public class CategoryActivity extends BaseRxActivity<CategoryPresenter> implements
|
||||
ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener, OnStartDragListener {
|
||||
|
||||
@Bind(R.id.toolbar) Toolbar toolbar;
|
||||
@Bind(R.id.categories_list) RecyclerView recycler;
|
||||
@Bind(R.id.fab) FloatingActionButton fab;
|
||||
|
||||
private CategoryAdapter adapter;
|
||||
private ActionMode actionMode;
|
||||
private ItemTouchHelper touchHelper;
|
||||
|
||||
public static Intent newIntent(Context context) {
|
||||
return new Intent(context, CategoryActivity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
setContentView(R.layout.activity_edit_categories);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
setupToolbar(toolbar);
|
||||
|
||||
adapter = new CategoryAdapter(this);
|
||||
recycler.setLayoutManager(new LinearLayoutManager(this));
|
||||
recycler.setHasFixedSize(true);
|
||||
recycler.setAdapter(adapter);
|
||||
recycler.addItemDecoration(new DividerItemDecoration(
|
||||
ResourcesCompat.getDrawable(getResources(), R.drawable.line_divider, null)));
|
||||
|
||||
// Touch helper to drag and reorder categories
|
||||
touchHelper = new ItemTouchHelper(new CategoryItemTouchHelper(adapter));
|
||||
touchHelper.attachToRecyclerView(recycler);
|
||||
|
||||
fab.setOnClickListener(v -> {
|
||||
new MaterialDialog.Builder(this)
|
||||
.title(R.string.action_add_category)
|
||||
.input(R.string.name, 0, false, (dialog, input) -> {
|
||||
getPresenter().createCategory(input.toString());
|
||||
})
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
public void setCategories(List<Category> categories) {
|
||||
destroyActionModeIfNeeded();
|
||||
adapter.setItems(categories);
|
||||
}
|
||||
|
||||
private List<Category> getSelectedCategories() {
|
||||
// Create a blocking copy of the selected categories
|
||||
return Observable.from(adapter.getSelectedItems())
|
||||
.map(adapter::getItem).toList().toBlocking().single();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListItemClick(int position) {
|
||||
if (actionMode != null && position != -1) {
|
||||
toggleSelection(position);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemLongClick(int position) {
|
||||
if (actionMode == null)
|
||||
actionMode = startSupportActionMode(this);
|
||||
|
||||
toggleSelection(position);
|
||||
}
|
||||
|
||||
private void toggleSelection(int position) {
|
||||
adapter.toggleSelection(position, false);
|
||||
|
||||
int count = adapter.getSelectedItemCount();
|
||||
if (count == 0) {
|
||||
actionMode.finish();
|
||||
} else {
|
||||
setContextTitle(count);
|
||||
actionMode.invalidate();
|
||||
MenuItem editItem = actionMode.getMenu().findItem(R.id.action_edit);
|
||||
editItem.setVisible(count == 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void setContextTitle(int count) {
|
||||
actionMode.setTitle(getString(R.string.label_selected, count));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.category_selection, menu);
|
||||
adapter.setMode(LibraryCategoryAdapter.MODE_MULTI);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_delete:
|
||||
deleteCategories(getSelectedCategories());
|
||||
return true;
|
||||
case R.id.action_edit:
|
||||
editCategory(getSelectedCategories().get(0));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
adapter.setMode(LibraryCategoryAdapter.MODE_SINGLE);
|
||||
adapter.clearSelection();
|
||||
actionMode = null;
|
||||
}
|
||||
|
||||
public void destroyActionModeIfNeeded() {
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteCategories(List<Category> categories) {
|
||||
getPresenter().deleteCategories(categories);
|
||||
}
|
||||
|
||||
private void editCategory(Category category) {
|
||||
new MaterialDialog.Builder(this)
|
||||
.title(R.string.action_rename_category)
|
||||
.input(getString(R.string.name), category.name, false, (dialog, input) -> {
|
||||
getPresenter().renameCategory(category, input.toString());
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
|
||||
touchHelper.startDrag(viewHolder);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package eu.kanade.tachiyomi.ui.library.category;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.ItemTouchHelperAdapter;
|
||||
|
||||
public class CategoryAdapter extends FlexibleAdapter<CategoryHolder, Category> implements
|
||||
ItemTouchHelperAdapter {
|
||||
|
||||
private final CategoryActivity activity;
|
||||
private final ColorGenerator generator;
|
||||
|
||||
public CategoryAdapter(CategoryActivity activity) {
|
||||
this.activity = activity;
|
||||
generator = ColorGenerator.DEFAULT;
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
public void setItems(List<Category> items) {
|
||||
mItems = new ArrayList<>(items);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mItems.get(position).id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDataSet(String param) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public CategoryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = activity.getLayoutInflater();
|
||||
View v = inflater.inflate(R.layout.item_edit_categories, parent, false);
|
||||
return new CategoryHolder(v, this, activity, activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(CategoryHolder holder, int position) {
|
||||
final Category category = getItem(position);
|
||||
holder.onSetValues(category, generator);
|
||||
|
||||
//When user scrolls this bind the correct selection status
|
||||
holder.itemView.setActivated(isSelected(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMove(int fromPosition, int toPosition) {
|
||||
if (fromPosition < toPosition) {
|
||||
for (int i = fromPosition; i < toPosition; i++) {
|
||||
Collections.swap(mItems, i, i + 1);
|
||||
}
|
||||
} else {
|
||||
for (int i = fromPosition; i > toPosition; i--) {
|
||||
Collections.swap(mItems, i, i - 1);
|
||||
}
|
||||
}
|
||||
|
||||
activity.getPresenter().reorderCategories(mItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDismiss(int position) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package eu.kanade.tachiyomi.ui.library.category;
|
||||
|
||||
import android.support.v4.view.MotionEventCompat;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.OnStartDragListener;
|
||||
|
||||
public class CategoryHolder extends FlexibleViewHolder {
|
||||
|
||||
private View view;
|
||||
|
||||
@Bind(R.id.image) ImageView image;
|
||||
@Bind(R.id.title) TextView title;
|
||||
@Bind(R.id.reorder) ImageView reorder;
|
||||
|
||||
public CategoryHolder(View view, CategoryAdapter adapter,
|
||||
OnListItemClickListener listener, OnStartDragListener dragListener) {
|
||||
super(view, adapter, listener);
|
||||
ButterKnife.bind(this, view);
|
||||
this.view = view;
|
||||
|
||||
reorder.setOnTouchListener((v, event) -> {
|
||||
if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
|
||||
dragListener.onStartDrag(this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public void onSetValues(Category category, ColorGenerator generator) {
|
||||
title.setText(category.name);
|
||||
image.setImageDrawable(getRound(category.name.substring(0, 1), generator));
|
||||
}
|
||||
|
||||
private TextDrawable getRound(String text, ColorGenerator generator) {
|
||||
return TextDrawable.builder().buildRound(text, generator.getColor(text));
|
||||
}
|
||||
|
||||
@OnClick(R.id.image)
|
||||
void onImageClick() {
|
||||
// Simulate long click on this view to enter selection mode
|
||||
onLongClick(view);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package eu.kanade.tachiyomi.ui.library.category;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.ItemTouchHelperAdapter;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.SimpleItemTouchHelperCallback;
|
||||
|
||||
public class CategoryItemTouchHelper extends SimpleItemTouchHelperCallback {
|
||||
|
||||
public CategoryItemTouchHelper(ItemTouchHelperAdapter adapter) {
|
||||
super(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package eu.kanade.tachiyomi.ui.library.category;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
|
||||
public class CategoryPresenter extends BasePresenter<CategoryActivity> {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
|
||||
private List<Category> categories;
|
||||
|
||||
private static final int GET_CATEGORIES = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
restartableLatestCache(GET_CATEGORIES,
|
||||
() -> db.getCategories().createObservable()
|
||||
.doOnNext(categories -> this.categories = categories)
|
||||
.observeOn(AndroidSchedulers.mainThread()),
|
||||
CategoryActivity::setCategories);
|
||||
|
||||
start(GET_CATEGORIES);
|
||||
}
|
||||
|
||||
public void createCategory(String name) {
|
||||
Category cat = Category.create(name);
|
||||
|
||||
// Set the new item in the last position
|
||||
int max = 0;
|
||||
if (categories != null) {
|
||||
for (Category cat2 : categories) {
|
||||
if (cat2.order > max) {
|
||||
max = cat2.order + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
cat.order = max;
|
||||
|
||||
db.insertCategory(cat).createObservable().subscribe();
|
||||
}
|
||||
|
||||
public void deleteCategories(List<Category> categories) {
|
||||
db.deleteCategories(categories).createObservable().subscribe();
|
||||
}
|
||||
|
||||
public void reorderCategories(List<Category> categories) {
|
||||
for (int i = 0; i < categories.size(); i++) {
|
||||
categories.get(i).order = i;
|
||||
}
|
||||
|
||||
db.insertCategories(categories).createObservable().subscribe();
|
||||
}
|
||||
|
||||
public void renameCategory(Category category, String name) {
|
||||
category.name = name;
|
||||
db.insertCategory(category).createObservable().subscribe();
|
||||
}
|
||||
}
|
||||
179
app/src/main/java/eu/kanade/tachiyomi/ui/main/FragmentStack.java
Normal file
179
app/src/main/java/eu/kanade/tachiyomi/ui/main/FragmentStack.java
Normal file
@@ -0,0 +1,179 @@
|
||||
package eu.kanade.tachiyomi.ui.main;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
|
||||
/**
|
||||
* Why this class is needed.
|
||||
*
|
||||
* FragmentManager does not supply a developer with a fragment stack.
|
||||
* It gives us a fragment *transaction* stack.
|
||||
*
|
||||
* To be sane, we need *fragment* stack.
|
||||
*
|
||||
* This implementation also handles NucleusSupportFragment presenter`s lifecycle correctly.
|
||||
*/
|
||||
public class FragmentStack {
|
||||
|
||||
public interface OnBackPressedHandlingFragment {
|
||||
boolean onBackPressed();
|
||||
}
|
||||
|
||||
public interface OnFragmentRemovedListener {
|
||||
void onFragmentRemoved(Fragment fragment);
|
||||
}
|
||||
|
||||
private Activity activity;
|
||||
private FragmentManager manager;
|
||||
private int containerId;
|
||||
@Nullable private OnFragmentRemovedListener onFragmentRemovedListener;
|
||||
|
||||
public FragmentStack(Activity activity, FragmentManager manager, int containerId, @Nullable OnFragmentRemovedListener onFragmentRemovedListener) {
|
||||
this.activity = activity;
|
||||
this.manager = manager;
|
||||
this.containerId = containerId;
|
||||
this.onFragmentRemovedListener = onFragmentRemovedListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of fragments in the stack.
|
||||
*
|
||||
* @return the number of fragments in the stack.
|
||||
*/
|
||||
public int size() {
|
||||
return getFragments().size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a fragment to the top of the stack.
|
||||
*/
|
||||
public void push(Fragment fragment) {
|
||||
|
||||
Fragment top = peek();
|
||||
if (top != null) {
|
||||
manager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right)
|
||||
.remove(top)
|
||||
.add(containerId, fragment, indexToTag(manager.getBackStackEntryCount() + 1))
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
else {
|
||||
manager.beginTransaction()
|
||||
.add(containerId, fragment, indexToTag(0))
|
||||
.commit();
|
||||
}
|
||||
|
||||
manager.executePendingTransactions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the top item if the stack.
|
||||
* If the fragment implements {@link OnBackPressedHandlingFragment}, calls {@link OnBackPressedHandlingFragment#onBackPressed()} instead.
|
||||
* If {@link OnBackPressedHandlingFragment#onBackPressed()} returns false the fragment gets popped.
|
||||
*
|
||||
* @return true if a fragment has been popped or if {@link OnBackPressedHandlingFragment#onBackPressed()} returned true;
|
||||
*/
|
||||
public boolean back() {
|
||||
Fragment top = peek();
|
||||
if (top instanceof OnBackPressedHandlingFragment) {
|
||||
if (((OnBackPressedHandlingFragment)top).onBackPressed())
|
||||
return true;
|
||||
}
|
||||
return pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the topmost fragment from the stack.
|
||||
* The lowest fragment can't be popped, it can only be replaced.
|
||||
*
|
||||
* @return false if the stack can't pop or true if a top fragment has been popped.
|
||||
*/
|
||||
public boolean pop() {
|
||||
if (manager.getBackStackEntryCount() == 0)
|
||||
return false;
|
||||
Fragment top = peek();
|
||||
manager.popBackStackImmediate();
|
||||
if (onFragmentRemovedListener != null)
|
||||
onFragmentRemovedListener.onFragmentRemoved(top);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces stack contents with just one fragment.
|
||||
*/
|
||||
public void replace(Fragment fragment) {
|
||||
List<Fragment> fragments = getFragments();
|
||||
|
||||
manager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
manager.beginTransaction()
|
||||
.replace(containerId, fragment, indexToTag(0))
|
||||
.commit();
|
||||
manager.executePendingTransactions();
|
||||
|
||||
if (onFragmentRemovedListener != null) {
|
||||
for (Fragment fragment1 : fragments)
|
||||
onFragmentRemovedListener.onFragmentRemoved(fragment1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the topmost fragment in the stack.
|
||||
*/
|
||||
public Fragment peek() {
|
||||
return manager.findFragmentById(containerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a back fragment if the fragment is of given class.
|
||||
* If such fragment does not exist and activity implements the given class then the activity will be returned.
|
||||
*
|
||||
* @param fragment a fragment to search from.
|
||||
* @param callbackType a class of type for callback to search.
|
||||
* @param <T> a type of callback.
|
||||
* @return a back fragment or activity.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T findCallback(Fragment fragment, Class<T> callbackType) {
|
||||
|
||||
Fragment back = getBackFragment(fragment);
|
||||
|
||||
if (back != null && callbackType.isAssignableFrom(back.getClass()))
|
||||
return (T)back;
|
||||
|
||||
if (callbackType.isAssignableFrom(activity.getClass()))
|
||||
return (T)activity;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Fragment getBackFragment(Fragment fragment) {
|
||||
List<Fragment> fragments = getFragments();
|
||||
for (int f = fragments.size() - 1; f >= 0; f--) {
|
||||
if (fragments.get(f) == fragment && f > 0)
|
||||
return fragments.get(f - 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<Fragment> getFragments() {
|
||||
List<Fragment> fragments = new ArrayList<>(manager.getBackStackEntryCount() + 1);
|
||||
for (int i = 0; i < manager.getBackStackEntryCount() + 1; i++) {
|
||||
Fragment fragment = manager.findFragmentByTag(indexToTag(i));
|
||||
if (fragment != null)
|
||||
fragments.add(fragment);
|
||||
}
|
||||
return fragments;
|
||||
}
|
||||
|
||||
private String indexToTag(int index) {
|
||||
return Integer.toString(index);
|
||||
}
|
||||
}
|
||||
180
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.java
Normal file
180
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.java
Normal file
@@ -0,0 +1,180 @@
|
||||
package eu.kanade.tachiyomi.ui.main;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.AppBarLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.widget.DrawerLayout;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.mikepenz.materialdrawer.Drawer;
|
||||
import com.mikepenz.materialdrawer.DrawerBuilder;
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity;
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment;
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadFragment;
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryFragment;
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsActivity;
|
||||
import icepick.State;
|
||||
import nucleus.view.ViewWithPresenter;
|
||||
|
||||
public class MainActivity extends BaseActivity {
|
||||
|
||||
@Bind(R.id.appbar) AppBarLayout appBar;
|
||||
@Bind(R.id.toolbar) Toolbar toolbar;
|
||||
@Bind(R.id.drawer_container) FrameLayout container;
|
||||
|
||||
private Drawer drawer;
|
||||
private FragmentStack fragmentStack;
|
||||
|
||||
@State int selectedItem;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
// Do not let the launcher create a new activity
|
||||
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
setupToolbar(toolbar);
|
||||
|
||||
fragmentStack = new FragmentStack(this, getSupportFragmentManager(), R.id.content_layout,
|
||||
fragment -> {
|
||||
if (fragment instanceof ViewWithPresenter)
|
||||
((ViewWithPresenter)fragment).getPresenter().destroy();
|
||||
});
|
||||
|
||||
drawer = new DrawerBuilder()
|
||||
.withActivity(this)
|
||||
.withRootView(container)
|
||||
.withToolbar(toolbar)
|
||||
.withActionBarDrawerToggleAnimated(true)
|
||||
.withOnDrawerNavigationListener(view -> {
|
||||
if (fragmentStack.size() > 1) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.addDrawerItems(
|
||||
new PrimaryDrawerItem()
|
||||
.withName(R.string.label_library)
|
||||
.withIdentifier(R.id.nav_drawer_library),
|
||||
// new PrimaryDrawerItem()
|
||||
// .withName(R.string.recent_updates_title)
|
||||
// .withIdentifier(R.id.nav_drawer_recent_updates),
|
||||
new PrimaryDrawerItem()
|
||||
.withName(R.string.label_catalogues)
|
||||
.withIdentifier(R.id.nav_drawer_catalogues),
|
||||
new PrimaryDrawerItem()
|
||||
.withName(R.string.label_download_queue)
|
||||
.withIdentifier(R.id.nav_drawer_downloads),
|
||||
new PrimaryDrawerItem()
|
||||
.withName(R.string.label_settings)
|
||||
.withIdentifier(R.id.nav_drawer_settings)
|
||||
.withSelectable(false)
|
||||
)
|
||||
.withSavedInstance(savedState)
|
||||
.withOnDrawerItemClickListener(
|
||||
(view, position, drawerItem) -> {
|
||||
if (drawerItem != null) {
|
||||
int identifier = drawerItem.getIdentifier();
|
||||
switch (identifier) {
|
||||
case R.id.nav_drawer_library:
|
||||
setFragment(LibraryFragment.newInstance());
|
||||
break;
|
||||
case R.id.nav_drawer_recent_updates:
|
||||
break;
|
||||
case R.id.nav_drawer_catalogues:
|
||||
setFragment(CatalogueFragment.newInstance());
|
||||
break;
|
||||
case R.id.nav_drawer_downloads:
|
||||
setFragment(DownloadFragment.newInstance());
|
||||
break;
|
||||
case R.id.nav_drawer_settings:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
)
|
||||
.build();
|
||||
|
||||
if (savedState != null) {
|
||||
// Recover icon state after rotation
|
||||
if (fragmentStack.size() > 1) {
|
||||
showBackArrow();
|
||||
}
|
||||
|
||||
// Set saved selection
|
||||
drawer.setSelection(selectedItem, false);
|
||||
} else {
|
||||
// Set default selection
|
||||
drawer.setSelection(R.id.nav_drawer_library);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
selectedItem = drawer.getCurrentSelection();
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
public void setFragment(Fragment fragment) {
|
||||
fragmentStack.replace(fragment);
|
||||
}
|
||||
|
||||
public void pushFragment(Fragment fragment) {
|
||||
fragmentStack.push(fragment);
|
||||
if (fragmentStack.size() > 1) {
|
||||
showBackArrow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (!fragmentStack.pop()) {
|
||||
super.onBackPressed();
|
||||
} else if (fragmentStack.size() == 1) {
|
||||
showHamburgerIcon();
|
||||
drawer.getActionBarDrawerToggle().syncState();
|
||||
}
|
||||
}
|
||||
|
||||
private void showHamburgerIcon() {
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
drawer.getActionBarDrawerToggle().setDrawerIndicatorEnabled(true);
|
||||
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||
}
|
||||
}
|
||||
|
||||
private void showBackArrow() {
|
||||
if (getSupportActionBar() != null) {
|
||||
drawer.getActionBarDrawerToggle().setDrawerIndicatorEnabled(false);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
}
|
||||
}
|
||||
|
||||
public Toolbar getToolbar() {
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
public AppBarLayout getAppBar() {
|
||||
return appBar;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package eu.kanade.tachiyomi.ui.manga;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity;
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment;
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment;
|
||||
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListFragment;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
|
||||
@RequiresPresenter(MangaPresenter.class)
|
||||
public class MangaActivity extends BaseRxActivity<MangaPresenter> {
|
||||
|
||||
@Bind(R.id.toolbar) Toolbar toolbar;
|
||||
@Bind(R.id.tabs) TabLayout tabs;
|
||||
@Bind(R.id.view_pager) ViewPager view_pager;
|
||||
|
||||
@Inject PreferencesHelper preferences;
|
||||
@Inject MangaSyncManager mangaSyncManager;
|
||||
|
||||
private MangaDetailAdapter adapter;
|
||||
private long manga_id;
|
||||
private boolean is_online;
|
||||
|
||||
public final static String MANGA_ID = "manga_id";
|
||||
public final static String MANGA_ONLINE = "manga_online";
|
||||
|
||||
public static Intent newIntent(Context context, Manga manga) {
|
||||
Intent intent = new Intent(context, MangaActivity.class);
|
||||
intent.putExtra(MANGA_ID, manga.id);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
App.get(this).getComponent().inject(this);
|
||||
setContentView(R.layout.activity_manga);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
setupToolbar(toolbar);
|
||||
|
||||
Intent intent = getIntent();
|
||||
|
||||
manga_id = intent.getLongExtra(MANGA_ID, -1);
|
||||
is_online = intent.getBooleanExtra(MANGA_ONLINE, false);
|
||||
|
||||
setupViewPager();
|
||||
|
||||
if (savedState == null)
|
||||
getPresenter().queryManga(manga_id);
|
||||
}
|
||||
|
||||
private void setupViewPager() {
|
||||
adapter = new MangaDetailAdapter(getSupportFragmentManager(), this);
|
||||
|
||||
view_pager.setAdapter(adapter);
|
||||
tabs.setupWithViewPager(view_pager);
|
||||
|
||||
if (!is_online)
|
||||
view_pager.setCurrentItem(MangaDetailAdapter.CHAPTERS_FRAGMENT);
|
||||
}
|
||||
|
||||
public void setManga(Manga manga) {
|
||||
setToolbarTitle(manga.title);
|
||||
}
|
||||
|
||||
public boolean isCatalogueManga() {
|
||||
return is_online;
|
||||
}
|
||||
|
||||
class MangaDetailAdapter extends FragmentPagerAdapter {
|
||||
|
||||
private int pageCount;
|
||||
private String tabTitles[];
|
||||
|
||||
final static int INFO_FRAGMENT = 0;
|
||||
final static int CHAPTERS_FRAGMENT = 1;
|
||||
final static int MYANIMELIST_FRAGMENT = 2;
|
||||
|
||||
public MangaDetailAdapter(FragmentManager fm, Context context) {
|
||||
super(fm);
|
||||
tabTitles = new String[]{
|
||||
context.getString(R.string.manga_detail_tab),
|
||||
context.getString(R.string.manga_chapters_tab),
|
||||
"MAL"
|
||||
};
|
||||
|
||||
pageCount = 2;
|
||||
if (!is_online && mangaSyncManager.getMyAnimeList().isLogged())
|
||||
pageCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return pageCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
switch (position) {
|
||||
case INFO_FRAGMENT:
|
||||
return MangaInfoFragment.newInstance();
|
||||
case CHAPTERS_FRAGMENT:
|
||||
return ChaptersFragment.newInstance();
|
||||
case MYANIMELIST_FRAGMENT:
|
||||
return MyAnimeListFragment.newInstance();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
// Generate title based on item position
|
||||
return tabTitles[position];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package eu.kanade.tachiyomi.ui.manga;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import icepick.State;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
public class MangaPresenter extends BasePresenter<MangaActivity> {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
|
||||
@State long mangaId;
|
||||
|
||||
private static final int DB_MANGA = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
restartableLatestCache(DB_MANGA, this::getDbMangaObservable, MangaActivity::setManga);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// Avoid new instances receiving wrong manga
|
||||
EventBus.getDefault().removeStickyEvent(Manga.class);
|
||||
}
|
||||
|
||||
private Observable<Manga> getDbMangaObservable() {
|
||||
return db.getManga(mangaId).createObservable()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(manga -> EventBus.getDefault().postSticky(manga));
|
||||
}
|
||||
|
||||
public void queryManga(long mangaId) {
|
||||
this.mangaId = mangaId;
|
||||
start(DB_MANGA);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
|
||||
public class ChaptersAdapter extends FlexibleAdapter<ChaptersHolder, Chapter> {
|
||||
|
||||
private ChaptersFragment fragment;
|
||||
|
||||
public ChaptersAdapter(ChaptersFragment fragment) {
|
||||
this.fragment = fragment;
|
||||
mItems = new ArrayList<>();
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDataSet(String param) {}
|
||||
|
||||
@Override
|
||||
public ChaptersHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(fragment.getActivity()).inflate(R.layout.item_chapter, parent, false);
|
||||
return new ChaptersHolder(v, this, fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ChaptersHolder holder, int position) {
|
||||
final Chapter chapter = getItem(position);
|
||||
holder.onSetValues(fragment.getActivity(), chapter);
|
||||
|
||||
//When user scrolls this bind the correct selection status
|
||||
holder.itemView.setActivated(isSelected(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mItems.get(position).id;
|
||||
}
|
||||
|
||||
public void setItems(List<Chapter> chapters) {
|
||||
mItems = chapters;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public ChaptersFragment getFragment() {
|
||||
return fragment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.view.ActionMode;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||
import eu.kanade.tachiyomi.ui.decoration.DividerItemDecoration;
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
import rx.Observable;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
@RequiresPresenter(ChaptersPresenter.class)
|
||||
public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implements
|
||||
ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener {
|
||||
|
||||
@Bind(R.id.chapter_list) RecyclerView recyclerView;
|
||||
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
|
||||
@Bind(R.id.toolbar_bottom) ViewGroup toolbarBottom;
|
||||
|
||||
@Bind(R.id.action_sort) ImageView sortBtn;
|
||||
@Bind(R.id.action_next_unread) ImageView nextUnreadBtn;
|
||||
@Bind(R.id.action_show_unread) CheckBox readCb;
|
||||
@Bind(R.id.action_show_downloaded) CheckBox downloadedCb;
|
||||
|
||||
private ChaptersAdapter adapter;
|
||||
private LinearLayoutManager linearLayout;
|
||||
private ActionMode actionMode;
|
||||
|
||||
private Subscription downloadProgressSubscription;
|
||||
|
||||
public static ChaptersFragment newInstance() {
|
||||
return new ChaptersFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
// Inflate the layout for this fragment
|
||||
View view = inflater.inflate(R.layout.fragment_manga_chapters, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
// Init RecyclerView and adapter
|
||||
linearLayout = new LinearLayoutManager(getActivity());
|
||||
recyclerView.setLayoutManager(linearLayout);
|
||||
recyclerView.addItemDecoration(new DividerItemDecoration(ContextCompat.getDrawable(getContext(), R.drawable.line_divider)));
|
||||
recyclerView.setHasFixedSize(true);
|
||||
adapter = new ChaptersAdapter(this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
// Set initial values
|
||||
setReadFilter();
|
||||
setDownloadedFilter();
|
||||
setSortIcon();
|
||||
|
||||
// Init listeners
|
||||
swipeRefresh.setOnRefreshListener(this::fetchChapters);
|
||||
readCb.setOnCheckedChangeListener((arg, isChecked) ->
|
||||
getPresenter().setReadFilter(isChecked));
|
||||
downloadedCb.setOnCheckedChangeListener((v, isChecked) ->
|
||||
getPresenter().setDownloadedFilter(isChecked));
|
||||
sortBtn.setOnClickListener(v -> {
|
||||
getPresenter().revertSortOrder();
|
||||
setSortIcon();
|
||||
});
|
||||
nextUnreadBtn.setOnClickListener(v -> {
|
||||
Chapter chapter = getPresenter().getNextUnreadChapter();
|
||||
if (chapter != null) {
|
||||
openChapter(chapter);
|
||||
} else {
|
||||
ToastUtil.showShort(getContext(), R.string.no_next_chapter);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
observeChapterDownloadProgress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
unsubscribeChapterDownloadProgress();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public void onNextChapters(List<Chapter> chapters) {
|
||||
// If the list is empty, fetch chapters from source if the conditions are met
|
||||
// We use presenter chapters instead because they are always unfiltered
|
||||
if (getPresenter().getChapters().isEmpty())
|
||||
initialFetchChapters();
|
||||
|
||||
destroyActionModeIfNeeded();
|
||||
adapter.setItems(chapters);
|
||||
}
|
||||
|
||||
private void initialFetchChapters() {
|
||||
// Only fetch if this view is from the catalog and it hasn't requested previously
|
||||
if (isCatalogueManga() && !getPresenter().hasRequested()) {
|
||||
fetchChapters();
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchChapters() {
|
||||
if (getPresenter().getManga() != null) {
|
||||
swipeRefresh.setRefreshing(true);
|
||||
getPresenter().fetchChaptersFromSource();
|
||||
}
|
||||
}
|
||||
|
||||
public void onFetchChaptersDone() {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchChaptersError() {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
ToastUtil.showShort(getContext(), R.string.fetch_chapters_error);
|
||||
}
|
||||
|
||||
public boolean isCatalogueManga() {
|
||||
return ((MangaActivity) getActivity()).isCatalogueManga();
|
||||
}
|
||||
|
||||
protected void openChapter(Chapter chapter) {
|
||||
getPresenter().onOpenChapter(chapter);
|
||||
Intent intent = ReaderActivity.newIntent(getActivity());
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void observeChapterDownloadProgress() {
|
||||
downloadProgressSubscription = getPresenter().getDownloadProgressObs()
|
||||
.subscribe(this::onDownloadProgressChange,
|
||||
error -> { /* TODO getting a NPE sometimes on 'manga' from presenter */ });
|
||||
}
|
||||
|
||||
private void unsubscribeChapterDownloadProgress() {
|
||||
if (downloadProgressSubscription != null)
|
||||
downloadProgressSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
private void onDownloadProgressChange(Download download) {
|
||||
ChaptersHolder holder = getHolder(download.chapter);
|
||||
if (holder != null)
|
||||
holder.onProgressChange(getContext(), download.downloadedImages, download.pages.size());
|
||||
}
|
||||
|
||||
public void onChapterStatusChange(Chapter chapter) {
|
||||
ChaptersHolder holder = getHolder(chapter);
|
||||
if (holder != null)
|
||||
holder.onStatusChange(chapter.status);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ChaptersHolder getHolder(Chapter chapter) {
|
||||
return (ChaptersHolder) recyclerView.findViewHolderForItemId(chapter.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.chapter_selection, menu);
|
||||
adapter.setMode(ChaptersAdapter.MODE_MULTI);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_select_all:
|
||||
return onSelectAll();
|
||||
case R.id.action_mark_as_read:
|
||||
return onMarkAsRead(getSelectedChapters());
|
||||
case R.id.action_mark_as_unread:
|
||||
return onMarkAsUnread(getSelectedChapters());
|
||||
case R.id.action_download:
|
||||
return onDownload(getSelectedChapters());
|
||||
case R.id.action_delete:
|
||||
return onDelete(getSelectedChapters());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
adapter.setMode(ChaptersAdapter.MODE_SINGLE);
|
||||
adapter.clearSelection();
|
||||
actionMode = null;
|
||||
}
|
||||
|
||||
private Observable<Chapter> getSelectedChapters() {
|
||||
// Create a blocking copy of the selected chapters.
|
||||
// When the action mode is closed the list is cleared. If we use background
|
||||
// threads with this observable, some emissions could be lost.
|
||||
List<Chapter> chapters = Observable.from(adapter.getSelectedItems())
|
||||
.map(adapter::getItem).toList().toBlocking().single();
|
||||
|
||||
return Observable.from(chapters);
|
||||
}
|
||||
|
||||
public void destroyActionModeIfNeeded() {
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean onSelectAll() {
|
||||
adapter.selectAll();
|
||||
setContextTitle(adapter.getSelectedItemCount());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean onMarkAsRead(Observable<Chapter> chapters) {
|
||||
getPresenter().markChaptersRead(chapters, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean onMarkAsUnread(Observable<Chapter> chapters) {
|
||||
getPresenter().markChaptersRead(chapters, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean onDownload(Observable<Chapter> chapters) {
|
||||
DownloadService.start(getActivity());
|
||||
|
||||
Observable<Chapter> observable = chapters
|
||||
.doOnCompleted(adapter::notifyDataSetChanged);
|
||||
|
||||
getPresenter().downloadChapters(observable);
|
||||
destroyActionModeIfNeeded();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean onDelete(Observable<Chapter> chapters) {
|
||||
int size = adapter.getSelectedItemCount();
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.deleting)
|
||||
.progress(false, size, true)
|
||||
.cancelable(false)
|
||||
.show();
|
||||
|
||||
Observable<Chapter> observable = chapters
|
||||
.concatMap(chapter -> {
|
||||
getPresenter().deleteChapter(chapter);
|
||||
return Observable.just(chapter);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(chapter -> {
|
||||
dialog.incrementProgress(1);
|
||||
chapter.status = Download.NOT_DOWNLOADED;
|
||||
})
|
||||
.doOnCompleted(adapter::notifyDataSetChanged)
|
||||
.finallyDo(dialog::dismiss);
|
||||
|
||||
getPresenter().deleteChapters(observable);
|
||||
destroyActionModeIfNeeded();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListItemClick(int position) {
|
||||
if (actionMode != null && adapter.getMode() == ChaptersAdapter.MODE_MULTI) {
|
||||
toggleSelection(position);
|
||||
return true;
|
||||
} else {
|
||||
openChapter(adapter.getItem(position));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemLongClick(int position) {
|
||||
if (actionMode == null)
|
||||
actionMode = getBaseActivity().startSupportActionMode(this);
|
||||
|
||||
toggleSelection(position);
|
||||
}
|
||||
|
||||
private void toggleSelection(int position) {
|
||||
adapter.toggleSelection(position, false);
|
||||
|
||||
int count = adapter.getSelectedItemCount();
|
||||
if (count == 0) {
|
||||
actionMode.finish();
|
||||
} else {
|
||||
setContextTitle(count);
|
||||
actionMode.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void setContextTitle(int count) {
|
||||
actionMode.setTitle(getString(R.string.label_selected, count));
|
||||
}
|
||||
|
||||
public void setSortIcon() {
|
||||
if (sortBtn != null) {
|
||||
boolean aToZ = getPresenter().getSortOrder();
|
||||
sortBtn.setImageResource(!aToZ ? R.drawable.ic_expand_less_white_36dp : R.drawable.ic_expand_more_white_36dp);
|
||||
}
|
||||
}
|
||||
|
||||
public void setReadFilter() {
|
||||
if (readCb != null) {
|
||||
readCb.setChecked(getPresenter().getReadFilter());
|
||||
}
|
||||
}
|
||||
|
||||
public void setDownloadedFilter() {
|
||||
if (downloadedCb != null) {
|
||||
downloadedCb.setChecked(getPresenter().getDownloadedFilter());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.View;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
import rx.Observable;
|
||||
|
||||
public class ChaptersHolder extends FlexibleViewHolder {
|
||||
|
||||
private final ChaptersAdapter adapter;
|
||||
private Chapter item;
|
||||
|
||||
@Bind(R.id.chapter_title) TextView title;
|
||||
@Bind(R.id.download_text) TextView downloadText;
|
||||
@Bind(R.id.chapter_menu) RelativeLayout chapterMenu;
|
||||
@Bind(R.id.chapter_pages) TextView pages;
|
||||
@Bind(R.id.chapter_date) TextView date;
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
|
||||
|
||||
public ChaptersHolder(View view, ChaptersAdapter adapter, OnListItemClickListener listener) {
|
||||
super(view, adapter, listener);
|
||||
this.adapter = adapter;
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
chapterMenu.setOnClickListener(v -> v.post(() -> showPopupMenu(v)));
|
||||
}
|
||||
|
||||
public void onSetValues(Context context, Chapter chapter) {
|
||||
this.item = chapter;
|
||||
title.setText(chapter.name);
|
||||
|
||||
if (chapter.read) {
|
||||
title.setTextColor(ContextCompat.getColor(context, R.color.hint_text));
|
||||
} else {
|
||||
title.setTextColor(ContextCompat.getColor(context, R.color.primary_text));
|
||||
}
|
||||
|
||||
if (!chapter.read && chapter.last_page_read > 0) {
|
||||
pages.setText(context.getString(R.string.chapter_progress, chapter.last_page_read + 1));
|
||||
} else {
|
||||
pages.setText("");
|
||||
}
|
||||
|
||||
onStatusChange(chapter.status);
|
||||
date.setText(sdf.format(new Date(chapter.date_upload)));
|
||||
}
|
||||
|
||||
public void onStatusChange(int status) {
|
||||
switch (status) {
|
||||
case Download.QUEUE:
|
||||
downloadText.setText(R.string.chapter_queued); break;
|
||||
case Download.DOWNLOADING:
|
||||
downloadText.setText(R.string.chapter_downloading); break;
|
||||
case Download.DOWNLOADED:
|
||||
downloadText.setText(R.string.chapter_downloaded); break;
|
||||
case Download.ERROR:
|
||||
downloadText.setText(R.string.chapter_error); break;
|
||||
default:
|
||||
downloadText.setText(""); break;
|
||||
}
|
||||
}
|
||||
|
||||
public void onProgressChange(Context context, int downloaded, int total) {
|
||||
downloadText.setText(context.getString(
|
||||
R.string.chapter_downloading_progress, downloaded, total));
|
||||
}
|
||||
|
||||
private void showPopupMenu(View view) {
|
||||
// Create a PopupMenu, giving it the clicked view for an anchor
|
||||
PopupMenu popup = new PopupMenu(adapter.getFragment().getActivity(), view);
|
||||
|
||||
// Inflate our menu resource into the PopupMenu's Menu
|
||||
popup.getMenuInflater().inflate(R.menu.chapter_single, popup.getMenu());
|
||||
|
||||
// Set a listener so we are notified if a menu item is clicked
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
Observable<Chapter> chapter = Observable.just(item);
|
||||
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.action_mark_as_read:
|
||||
return adapter.getFragment().onMarkAsRead(chapter);
|
||||
case R.id.action_mark_as_unread:
|
||||
return adapter.getFragment().onMarkAsUnread(chapter);
|
||||
case R.id.action_download:
|
||||
return adapter.getFragment().onDownload(chapter);
|
||||
case R.id.action_delete:
|
||||
return adapter.getFragment().onDelete(chapter);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Finally show the PopupMenu
|
||||
popup.show();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.event.ChapterCountEvent;
|
||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
|
||||
import eu.kanade.tachiyomi.event.ReaderEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import icepick.State;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import rx.subjects.PublishSubject;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject SourceManager sourceManager;
|
||||
@Inject PreferencesHelper preferences;
|
||||
@Inject DownloadManager downloadManager;
|
||||
|
||||
private Manga manga;
|
||||
private Source source;
|
||||
private List<Chapter> chapters;
|
||||
private boolean sortOrderAToZ = true;
|
||||
private boolean onlyUnread = true;
|
||||
private boolean onlyDownloaded;
|
||||
@State boolean hasRequested;
|
||||
|
||||
private PublishSubject<List<Chapter>> chaptersSubject;
|
||||
|
||||
private static final int DB_CHAPTERS = 1;
|
||||
private static final int FETCH_CHAPTERS = 2;
|
||||
private static final int CHAPTER_STATUS_CHANGES = 3;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
chaptersSubject = PublishSubject.create();
|
||||
|
||||
restartableLatestCache(DB_CHAPTERS,
|
||||
this::getDbChaptersObs,
|
||||
ChaptersFragment::onNextChapters);
|
||||
|
||||
restartableFirst(FETCH_CHAPTERS,
|
||||
this::getOnlineChaptersObs,
|
||||
(view, result) -> view.onFetchChaptersDone(),
|
||||
(view, error) -> view.onFetchChaptersError());
|
||||
|
||||
restartableLatestCache(CHAPTER_STATUS_CHANGES,
|
||||
this::getChapterStatusObs,
|
||||
(view, download) -> view.onChapterStatusChange(download.chapter),
|
||||
(view, error) -> Timber.e(error.getCause(), error.getMessage()));
|
||||
|
||||
registerForStickyEvents();
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
stop(DB_CHAPTERS);
|
||||
stop(FETCH_CHAPTERS);
|
||||
stop(CHAPTER_STATUS_CHANGES);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterForEvents();
|
||||
EventBus.getDefault().removeStickyEvent(ChapterCountEvent.class);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(Manga manga) {
|
||||
this.manga = manga;
|
||||
|
||||
if (!isSubscribed(DB_CHAPTERS)) {
|
||||
source = sourceManager.get(manga.source);
|
||||
start(DB_CHAPTERS);
|
||||
|
||||
add(db.getChapters(manga).createObservable()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnNext(chapters -> {
|
||||
this.chapters = chapters;
|
||||
EventBus.getDefault().postSticky(new ChapterCountEvent(chapters.size()));
|
||||
for (Chapter chapter : chapters) {
|
||||
setChapterStatus(chapter);
|
||||
}
|
||||
start(CHAPTER_STATUS_CHANGES);
|
||||
})
|
||||
.subscribe(chaptersSubject::onNext));
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchChaptersFromSource() {
|
||||
hasRequested = true;
|
||||
start(FETCH_CHAPTERS);
|
||||
}
|
||||
|
||||
private void refreshChapters() {
|
||||
chaptersSubject.onNext(chapters);
|
||||
}
|
||||
|
||||
private Observable<Pair<Integer, Integer>> getOnlineChaptersObs() {
|
||||
return source.pullChaptersFromNetwork(manga.url)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters))
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
private Observable<List<Chapter>> getDbChaptersObs() {
|
||||
return chaptersSubject.flatMap(this::applyChapterFilters)
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
private Observable<List<Chapter>> applyChapterFilters(List<Chapter> chapters) {
|
||||
Observable<Chapter> observable = Observable.from(chapters)
|
||||
.subscribeOn(Schedulers.io());
|
||||
if (onlyUnread) {
|
||||
observable = observable.filter(chapter -> !chapter.read);
|
||||
}
|
||||
if (onlyDownloaded) {
|
||||
observable = observable.filter(chapter -> chapter.status == Download.DOWNLOADED);
|
||||
}
|
||||
return observable.toSortedList((chapter, chapter2) -> sortOrderAToZ ?
|
||||
Float.compare(chapter2.chapter_number, chapter.chapter_number) :
|
||||
Float.compare(chapter.chapter_number, chapter2.chapter_number));
|
||||
}
|
||||
|
||||
private void setChapterStatus(Chapter chapter) {
|
||||
for (Download download : downloadManager.getQueue()) {
|
||||
if (chapter.id.equals(download.chapter.id)) {
|
||||
chapter.status = download.getStatus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadManager.isChapterDownloaded(source, manga, chapter)) {
|
||||
chapter.status = Download.DOWNLOADED;
|
||||
} else {
|
||||
chapter.status = Download.NOT_DOWNLOADED;
|
||||
}
|
||||
}
|
||||
|
||||
private Observable<Download> getChapterStatusObs() {
|
||||
return downloadManager.getQueue().getStatusObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter(download -> download.manga.id.equals(manga.id))
|
||||
.doOnNext(this::updateChapterStatus);
|
||||
}
|
||||
|
||||
public void updateChapterStatus(Download download) {
|
||||
for (Chapter chapter : chapters) {
|
||||
if (download.chapter.id.equals(chapter.id)) {
|
||||
chapter.status = download.getStatus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (onlyDownloaded && download.getStatus() == Download.DOWNLOADED)
|
||||
refreshChapters();
|
||||
}
|
||||
|
||||
public Observable<Download> getDownloadProgressObs() {
|
||||
return downloadManager.getQueue().getProgressObservable()
|
||||
.filter(download -> download.manga.id.equals(manga.id))
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public void onOpenChapter(Chapter chapter) {
|
||||
EventBus.getDefault().postSticky(new ReaderEvent(source, manga, chapter));
|
||||
}
|
||||
|
||||
public Chapter getNextUnreadChapter() {
|
||||
return db.getNextUnreadChapter(manga).executeAsBlocking();
|
||||
}
|
||||
|
||||
public void markChaptersRead(Observable<Chapter> selectedChapters, boolean read) {
|
||||
add(selectedChapters
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map(chapter -> {
|
||||
chapter.read = read;
|
||||
if (!read) chapter.last_page_read = 0;
|
||||
return chapter;
|
||||
})
|
||||
.toList()
|
||||
.flatMap(chapters -> db.insertChapters(chapters).createObservable())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
public void downloadChapters(Observable<Chapter> selectedChapters) {
|
||||
add(selectedChapters
|
||||
.toList()
|
||||
.subscribe(chapters -> {
|
||||
EventBus.getDefault().postSticky(new DownloadChaptersEvent(manga, chapters));
|
||||
}));
|
||||
}
|
||||
|
||||
public void deleteChapters(Observable<Chapter> selectedChapters) {
|
||||
add(selectedChapters
|
||||
.subscribe(chapter -> {
|
||||
downloadManager.getQueue().remove(chapter);
|
||||
}, error -> {
|
||||
Timber.e(error.getMessage());
|
||||
}, () -> {
|
||||
if (onlyDownloaded)
|
||||
refreshChapters();
|
||||
}));
|
||||
}
|
||||
|
||||
public void deleteChapter(Chapter chapter) {
|
||||
downloadManager.deleteChapter(source, manga, chapter);
|
||||
}
|
||||
|
||||
public void revertSortOrder() {
|
||||
//TODO manga.chapter_order
|
||||
sortOrderAToZ = !sortOrderAToZ;
|
||||
refreshChapters();
|
||||
}
|
||||
|
||||
public void setReadFilter(boolean onlyUnread) {
|
||||
//TODO do we need save filter for manga?
|
||||
this.onlyUnread = onlyUnread;
|
||||
refreshChapters();
|
||||
}
|
||||
|
||||
public void setDownloadedFilter(boolean onlyDownloaded) {
|
||||
this.onlyDownloaded = onlyDownloaded;
|
||||
refreshChapters();
|
||||
}
|
||||
|
||||
public boolean getSortOrder() {
|
||||
return sortOrderAToZ;
|
||||
}
|
||||
|
||||
public boolean getReadFilter() {
|
||||
return onlyUnread;
|
||||
}
|
||||
|
||||
public boolean getDownloadedFilter() {
|
||||
return onlyDownloaded;
|
||||
}
|
||||
|
||||
public Manga getManga() {
|
||||
return manga;
|
||||
}
|
||||
|
||||
public List<Chapter> getChapters() {
|
||||
return chapters;
|
||||
}
|
||||
|
||||
public boolean hasRequested() {
|
||||
return hasRequested;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
|
||||
@RequiresPresenter(MangaInfoPresenter.class)
|
||||
public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
|
||||
|
||||
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
|
||||
|
||||
@Bind(R.id.manga_artist) TextView artist;
|
||||
@Bind(R.id.manga_author) TextView author;
|
||||
@Bind(R.id.manga_chapters) TextView chapterCount;
|
||||
@Bind(R.id.manga_genres) TextView genres;
|
||||
@Bind(R.id.manga_status) TextView status;
|
||||
@Bind(R.id.manga_summary) TextView description;
|
||||
@Bind(R.id.manga_cover) ImageView cover;
|
||||
|
||||
@Bind(R.id.action_favorite) Button favoriteBtn;
|
||||
|
||||
|
||||
public static MangaInfoFragment newInstance() {
|
||||
return new MangaInfoFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
// Inflate the layout for this fragment
|
||||
View view = inflater.inflate(R.layout.fragment_manga_info, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
favoriteBtn.setOnClickListener(v -> {
|
||||
getPresenter().toggleFavorite();
|
||||
});
|
||||
swipeRefresh.setOnRefreshListener(this::fetchMangaFromSource);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public void onNextManga(Manga manga) {
|
||||
if (manga.initialized) {
|
||||
setMangaInfo(manga);
|
||||
} else {
|
||||
// Initialize manga
|
||||
fetchMangaFromSource();
|
||||
}
|
||||
}
|
||||
|
||||
private void setMangaInfo(Manga manga) {
|
||||
artist.setText(manga.artist);
|
||||
author.setText(manga.author);
|
||||
genres.setText(manga.genre);
|
||||
status.setText(manga.getStatus(getActivity()));
|
||||
description.setText(manga.description);
|
||||
|
||||
setFavoriteText(manga.favorite);
|
||||
|
||||
CoverCache coverCache = getPresenter().coverCache;
|
||||
LazyHeaders headers = getPresenter().source.getGlideHeaders();
|
||||
if (manga.thumbnail_url != null && cover.getDrawable() == null) {
|
||||
if (manga.favorite) {
|
||||
coverCache.saveAndLoadFromCache(cover, manga.thumbnail_url, headers);
|
||||
} else {
|
||||
coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setChapterCount(int count) {
|
||||
chapterCount.setText(String.valueOf(count));
|
||||
}
|
||||
|
||||
public void setFavoriteText(boolean isFavorite) {
|
||||
favoriteBtn.setText(!isFavorite ? R.string.add_to_library : R.string.remove_from_library);
|
||||
}
|
||||
|
||||
private void fetchMangaFromSource() {
|
||||
setRefreshing(true);
|
||||
getPresenter().fetchMangaFromSource();
|
||||
}
|
||||
|
||||
public void onFetchMangaDone() {
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchMangaError() {
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private void setRefreshing(boolean value) {
|
||||
swipeRefresh.setRefreshing(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.event.ChapterCountEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject SourceManager sourceManager;
|
||||
@Inject CoverCache coverCache;
|
||||
|
||||
private Manga manga;
|
||||
protected Source source;
|
||||
private int count = -1;
|
||||
|
||||
private boolean isFetching;
|
||||
|
||||
private static final int GET_MANGA = 1;
|
||||
private static final int GET_CHAPTER_COUNT = 2;
|
||||
private static final int FETCH_MANGA_INFO = 3;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
restartableLatestCache(GET_MANGA,
|
||||
() -> Observable.just(manga),
|
||||
MangaInfoFragment::onNextManga);
|
||||
|
||||
restartableLatestCache(GET_CHAPTER_COUNT,
|
||||
() -> Observable.just(count),
|
||||
MangaInfoFragment::setChapterCount);
|
||||
|
||||
restartableFirst(FETCH_MANGA_INFO,
|
||||
this::fetchMangaObs,
|
||||
(view, manga) -> view.onFetchMangaDone(),
|
||||
(view, error) -> view.onFetchMangaError());
|
||||
|
||||
registerForStickyEvents();
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
stop(GET_MANGA);
|
||||
stop(GET_CHAPTER_COUNT);
|
||||
stop(FETCH_MANGA_INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterForEvents();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(Manga manga) {
|
||||
this.manga = manga;
|
||||
source = sourceManager.get(manga.source);
|
||||
start(GET_MANGA);
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(ChapterCountEvent event) {
|
||||
if (count != event.getCount()) {
|
||||
count = event.getCount();
|
||||
start(GET_CHAPTER_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchMangaFromSource() {
|
||||
if (!isFetching) {
|
||||
isFetching = true;
|
||||
start(FETCH_MANGA_INFO);
|
||||
}
|
||||
}
|
||||
|
||||
private Observable<Manga> fetchMangaObs() {
|
||||
return source.pullMangaFromNetwork(manga.url)
|
||||
.flatMap(networkManga -> {
|
||||
manga.copyFrom(networkManga);
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
return Observable.just(manga);
|
||||
})
|
||||
.finallyDo(() -> isFetching = false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public void toggleFavorite() {
|
||||
manga.favorite = !manga.favorite;
|
||||
onMangaFavoriteChange(manga.favorite);
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
}
|
||||
|
||||
private void onMangaFavoriteChange(boolean isFavorite) {
|
||||
if (isFavorite) {
|
||||
coverCache.save(manga.thumbnail_url, source.getGlideHeaders());
|
||||
} else {
|
||||
coverCache.delete(manga.thumbnail_url);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.subjects.PublishSubject;
|
||||
import uk.co.ribot.easyadapter.EasyAdapter;
|
||||
import uk.co.ribot.easyadapter.ItemViewHolder;
|
||||
import uk.co.ribot.easyadapter.PositionInfo;
|
||||
import uk.co.ribot.easyadapter.annotations.LayoutId;
|
||||
import uk.co.ribot.easyadapter.annotations.ViewId;
|
||||
|
||||
public class MyAnimeListDialogFragment extends DialogFragment {
|
||||
|
||||
@Bind(R.id.myanimelist_search_field) EditText searchText;
|
||||
@Bind(R.id.myanimelist_search_results) ListView searchResults;
|
||||
@Bind(R.id.progress) ProgressBar progressBar;
|
||||
|
||||
private EasyAdapter<MangaSync> adapter;
|
||||
private MangaSync selectedItem;
|
||||
|
||||
private Subscription searchSubscription;
|
||||
|
||||
public static MyAnimeListDialogFragment newInstance() {
|
||||
return new MyAnimeListDialogFragment();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedState) {
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.customView(R.layout.dialog_myanimelist_search, false)
|
||||
.positiveText(R.string.button_ok)
|
||||
.negativeText(R.string.button_cancel)
|
||||
.onPositive((dialog1, which) -> onPositiveButtonClick())
|
||||
.build();
|
||||
|
||||
ButterKnife.bind(this, dialog.getView());
|
||||
|
||||
// Create adapter
|
||||
adapter = new EasyAdapter<>(getActivity(), ResultViewHolder.class);
|
||||
searchResults.setAdapter(adapter);
|
||||
|
||||
// Set listeners
|
||||
searchResults.setOnItemClickListener((parent, viewList, position, id) ->
|
||||
selectedItem = adapter.getItem(position));
|
||||
|
||||
// Do an initial search based on the manga's title
|
||||
if (savedState == null) {
|
||||
String title = getPresenter().manga.title;
|
||||
searchText.append(title);
|
||||
search(title);
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
PublishSubject<String> querySubject = PublishSubject.create();
|
||||
searchText.addTextChangedListener(new SimpleTextChangeListener() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
querySubject.onNext(s.toString());
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to text changes
|
||||
searchSubscription = querySubject.debounce(1, TimeUnit.SECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::search);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (searchSubscription != null) {
|
||||
searchSubscription.unsubscribe();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
private void onPositiveButtonClick() {
|
||||
if (adapter != null && selectedItem != null) {
|
||||
getPresenter().registerManga(selectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void search(String query) {
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
searchResults.setVisibility(View.GONE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
getPresenter().searchManga(query);
|
||||
}
|
||||
}
|
||||
|
||||
public void onSearchResults(List<MangaSync> results) {
|
||||
selectedItem = null;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
searchResults.setVisibility(View.VISIBLE);
|
||||
adapter.setItems(results);
|
||||
}
|
||||
|
||||
public void onSearchResultsError() {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
searchResults.setVisibility(View.VISIBLE);
|
||||
adapter.getItems().clear();
|
||||
}
|
||||
|
||||
public MyAnimeListFragment getMALFragment() {
|
||||
return (MyAnimeListFragment) getParentFragment();
|
||||
}
|
||||
|
||||
public MyAnimeListPresenter getPresenter() {
|
||||
return getMALFragment().getPresenter();
|
||||
}
|
||||
|
||||
@LayoutId(R.layout.dialog_myanimelist_search_item)
|
||||
public static class ResultViewHolder extends ItemViewHolder<MangaSync> {
|
||||
|
||||
@ViewId(R.id.myanimelist_result_title) TextView title;
|
||||
|
||||
public ResultViewHolder(View view) {
|
||||
super(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetValues(MangaSync chapter, PositionInfo positionInfo) {
|
||||
title.setText(chapter.title);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SimpleTextChangeListener implements TextWatcher {
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
|
||||
@RequiresPresenter(MyAnimeListPresenter.class)
|
||||
public class MyAnimeListFragment extends BaseRxFragment<MyAnimeListPresenter> {
|
||||
|
||||
@Bind(R.id.myanimelist_title) TextView title;
|
||||
@Bind(R.id.myanimelist_chapters) TextView chapters;
|
||||
@Bind(R.id.myanimelist_score) TextView score;
|
||||
@Bind(R.id.myanimelist_status) TextView status;
|
||||
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
|
||||
|
||||
private MyAnimeListDialogFragment dialog;
|
||||
|
||||
private DecimalFormat decimalFormat = new DecimalFormat("#.##");
|
||||
|
||||
private final static String SEARCH_FRAGMENT_TAG = "mal_search";
|
||||
|
||||
public static MyAnimeListFragment newInstance() {
|
||||
return new MyAnimeListFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_myanimelist, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
swipeRefresh.setEnabled(false);
|
||||
swipeRefresh.setOnRefreshListener(() -> getPresenter().refresh());
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setMangaSync(MangaSync mangaSync) {
|
||||
swipeRefresh.setEnabled(mangaSync != null);
|
||||
if (mangaSync != null) {
|
||||
title.setText(mangaSync.title);
|
||||
chapters.setText(mangaSync.last_chapter_read + "/" +
|
||||
(mangaSync.total_chapters > 0 ? mangaSync.total_chapters : "-"));
|
||||
score.setText(mangaSync.score == 0 ? "-" : decimalFormat.format(mangaSync.score));
|
||||
status.setText(getPresenter().myAnimeList.getStatus(mangaSync.status));
|
||||
}
|
||||
}
|
||||
|
||||
public void onRefreshDone() {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onRefreshError() {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void setSearchResults(List<MangaSync> results) {
|
||||
findSearchFragmentIfNeeded();
|
||||
|
||||
if (dialog != null) {
|
||||
dialog.onSearchResults(results);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSearchResultsError() {
|
||||
findSearchFragmentIfNeeded();
|
||||
|
||||
if (dialog != null) {
|
||||
dialog.onSearchResultsError();
|
||||
}
|
||||
}
|
||||
|
||||
private void findSearchFragmentIfNeeded() {
|
||||
if (dialog == null) {
|
||||
dialog = (MyAnimeListDialogFragment) getChildFragmentManager()
|
||||
.findFragmentByTag(SEARCH_FRAGMENT_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.myanimelist_title_layout)
|
||||
void onTitleClick() {
|
||||
if (dialog == null)
|
||||
dialog = MyAnimeListDialogFragment.newInstance();
|
||||
|
||||
getPresenter().restartSearch();
|
||||
dialog.show(getChildFragmentManager(), SEARCH_FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
@OnClick(R.id.myanimelist_status_layout)
|
||||
void onStatusClick() {
|
||||
if (getPresenter().mangaSync == null)
|
||||
return;
|
||||
|
||||
Context ctx = getActivity();
|
||||
new MaterialDialog.Builder(ctx)
|
||||
.title(R.string.status)
|
||||
.items(getPresenter().getAllStatus(ctx))
|
||||
.itemsCallbackSingleChoice(getPresenter().getIndexFromStatus(),
|
||||
(materialDialog, view, i, charSequence) -> {
|
||||
getPresenter().setStatus(i);
|
||||
status.setText("...");
|
||||
return true;
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@OnClick(R.id.myanimelist_chapters_layout)
|
||||
void onChaptersClick() {
|
||||
if (getPresenter().mangaSync == null)
|
||||
return;
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.chapters)
|
||||
.customView(R.layout.dialog_myanimelist_chapters, false)
|
||||
.positiveText(R.string.button_ok)
|
||||
.negativeText(R.string.button_cancel)
|
||||
.onPositive((materialDialog, dialogAction) -> {
|
||||
View view = materialDialog.getCustomView();
|
||||
if (view != null) {
|
||||
NumberPicker np = (NumberPicker) view.findViewById(R.id.chapters_picker);
|
||||
getPresenter().setLastChapterRead(np.getValue());
|
||||
chapters.setText("...");
|
||||
}
|
||||
})
|
||||
.show();
|
||||
|
||||
View view = dialog.getCustomView();
|
||||
if (view != null) {
|
||||
NumberPicker np = (NumberPicker) view.findViewById(R.id.chapters_picker);
|
||||
// Set initial value
|
||||
np.setValue(getPresenter().mangaSync.last_chapter_read);
|
||||
// Don't allow to go from 0 to 9999
|
||||
np.setWrapSelectorWheel(false);
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.myanimelist_score_layout)
|
||||
void onScoreClick() {
|
||||
if (getPresenter().mangaSync == null)
|
||||
return;
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.score)
|
||||
.customView(R.layout.dialog_myanimelist_score, false)
|
||||
.positiveText(R.string.button_ok)
|
||||
.negativeText(R.string.button_cancel)
|
||||
.onPositive((materialDialog, dialogAction) -> {
|
||||
View view = materialDialog.getCustomView();
|
||||
if (view != null) {
|
||||
NumberPicker np = (NumberPicker) view.findViewById(R.id.score_picker);
|
||||
getPresenter().setScore(np.getValue());
|
||||
score.setText("...");
|
||||
}
|
||||
})
|
||||
.show();
|
||||
|
||||
View view = dialog.getCustomView();
|
||||
if (view != null) {
|
||||
NumberPicker np = (NumberPicker) view.findViewById(R.id.score_picker);
|
||||
// Set initial value
|
||||
np.setValue((int) getPresenter().mangaSync.score);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject MangaSyncManager syncManager;
|
||||
|
||||
protected MyAnimeList myAnimeList;
|
||||
protected Manga manga;
|
||||
protected MangaSync mangaSync;
|
||||
|
||||
private String query;
|
||||
|
||||
private static final int GET_MANGA_SYNC = 1;
|
||||
private static final int GET_SEARCH_RESULTS = 2;
|
||||
private static final int REFRESH = 3;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
myAnimeList = syncManager.getMyAnimeList();
|
||||
|
||||
restartableLatestCache(GET_MANGA_SYNC,
|
||||
() -> db.getMangaSync(manga, myAnimeList).createObservable()
|
||||
.doOnNext(mangaSync -> this.mangaSync = mangaSync)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread()),
|
||||
MyAnimeListFragment::setMangaSync);
|
||||
|
||||
restartableLatestCache(GET_SEARCH_RESULTS,
|
||||
() -> myAnimeList.search(query)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread()),
|
||||
(view, results) -> {
|
||||
view.setSearchResults(results);
|
||||
}, (view, error) -> {
|
||||
Timber.e(error.getMessage());
|
||||
view.setSearchResultsError();
|
||||
});
|
||||
|
||||
restartableFirst(REFRESH,
|
||||
() -> myAnimeList.getList()
|
||||
.flatMap(myList -> {
|
||||
for (MangaSync myManga : myList) {
|
||||
if (myManga.remote_id == mangaSync.remote_id) {
|
||||
mangaSync.copyPersonalFrom(myManga);
|
||||
mangaSync.total_chapters = myManga.total_chapters;
|
||||
return Observable.just(mangaSync);
|
||||
}
|
||||
}
|
||||
return Observable.error(new Exception("Could not find manga"));
|
||||
})
|
||||
.flatMap(myManga -> db.insertMangaSync(myManga).createObservable())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread()),
|
||||
(view, result) -> view.onRefreshDone(),
|
||||
(view, error) -> view.onRefreshError());
|
||||
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
stop(GET_MANGA_SYNC);
|
||||
stop(GET_SEARCH_RESULTS);
|
||||
stop(REFRESH);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTakeView(MyAnimeListFragment view) {
|
||||
super.onTakeView(view);
|
||||
registerForStickyEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDropView() {
|
||||
unregisterForEvents();
|
||||
super.onDropView();
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(Manga manga) {
|
||||
this.manga = manga;
|
||||
start(GET_MANGA_SYNC);
|
||||
}
|
||||
|
||||
private void updateRemote() {
|
||||
add(myAnimeList.update(mangaSync)
|
||||
.flatMap(response -> db.insertMangaSync(mangaSync).createObservable())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(next -> {},
|
||||
error -> {
|
||||
Timber.e(error.getMessage());
|
||||
// Restart on error to set old values
|
||||
start(GET_MANGA_SYNC);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
public void searchManga(String query) {
|
||||
if (TextUtils.isEmpty(query) || query.equals(this.query))
|
||||
return;
|
||||
|
||||
this.query = query;
|
||||
start(GET_SEARCH_RESULTS);
|
||||
}
|
||||
|
||||
public void restartSearch() {
|
||||
this.query = null;
|
||||
stop(GET_SEARCH_RESULTS);
|
||||
}
|
||||
|
||||
public void registerManga(MangaSync manga) {
|
||||
manga.manga_id = this.manga.id;
|
||||
add(myAnimeList.bind(manga)
|
||||
.flatMap(response -> {
|
||||
if (response.isSuccessful()) {
|
||||
return db.insertMangaSync(manga).createObservable();
|
||||
}
|
||||
return Observable.error(new Exception("Could not bind manga"));
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(manga2 -> {},
|
||||
error -> ToastUtil.showShort(getContext(), error.getMessage())));
|
||||
}
|
||||
|
||||
public String[] getAllStatus(Context context) {
|
||||
return new String[] {
|
||||
context.getString(R.string.reading),
|
||||
context.getString(R.string.completed),
|
||||
context.getString(R.string.on_hold),
|
||||
context.getString(R.string.dropped),
|
||||
context.getString(R.string.plan_to_read)
|
||||
};
|
||||
}
|
||||
|
||||
public int getIndexFromStatus() {
|
||||
return mangaSync.status == 6 ? 4 : mangaSync.status - 1;
|
||||
}
|
||||
|
||||
public void setStatus(int index) {
|
||||
mangaSync.status = index == 4 ? 6 : index + 1;
|
||||
updateRemote();
|
||||
}
|
||||
|
||||
public void setScore(int score) {
|
||||
mangaSync.score = score;
|
||||
updateRemote();
|
||||
}
|
||||
|
||||
public void setLastChapterRead(int chapterNumber) {
|
||||
mangaSync.last_chapter_read = chapterNumber;
|
||||
updateRemote();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (mangaSync != null) {
|
||||
start(REFRESH);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package eu.kanade.tachiyomi.ui.reader;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonReader;
|
||||
import eu.kanade.tachiyomi.util.GLUtil;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import icepick.Icepick;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
import rx.Subscription;
|
||||
import rx.subscriptions.CompositeSubscription;
|
||||
|
||||
@RequiresPresenter(ReaderPresenter.class)
|
||||
public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
||||
|
||||
@Bind(R.id.page_number) TextView pageNumber;
|
||||
@Bind(R.id.toolbar) Toolbar toolbar;
|
||||
|
||||
@Inject PreferencesHelper preferences;
|
||||
|
||||
private BaseReader viewer;
|
||||
private ReaderMenu readerMenu;
|
||||
|
||||
private int uiFlags;
|
||||
private int readerTheme;
|
||||
protected CompositeSubscription subscriptions;
|
||||
private Subscription customBrightnessSubscription;
|
||||
|
||||
private int maxBitmapSize;
|
||||
|
||||
public static final int LEFT_TO_RIGHT = 1;
|
||||
public static final int RIGHT_TO_LEFT = 2;
|
||||
public static final int VERTICAL = 3;
|
||||
public static final int WEBTOON = 4;
|
||||
|
||||
public static final int BLACK_THEME = 1;
|
||||
|
||||
public static Intent newIntent(Context context) {
|
||||
return new Intent(context, ReaderActivity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
App.get(this).getComponent().inject(this);
|
||||
setContentView(R.layout.activity_reader);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
setupToolbar(toolbar);
|
||||
subscriptions = new CompositeSubscription();
|
||||
|
||||
readerMenu = new ReaderMenu(this);
|
||||
Icepick.restoreInstanceState(readerMenu, savedState);
|
||||
if (savedState != null && readerMenu.showing)
|
||||
readerMenu.show(false);
|
||||
|
||||
initializeSettings();
|
||||
|
||||
maxBitmapSize = GLUtil.getMaxTextureSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
setSystemUiVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
if (viewer != null)
|
||||
getPresenter().setCurrentPage(viewer.getCurrentPage());
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
subscriptions.unsubscribe();
|
||||
viewer = null;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
return readerMenu.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
return readerMenu.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
Icepick.saveInstanceState(readerMenu, outState);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (viewer != null)
|
||||
getPresenter().setCurrentPage(viewer.getCurrentPage());
|
||||
getPresenter().onChapterLeft();
|
||||
|
||||
int chapterToUpdate = getPresenter().getMangaSyncChapterToUpdate();
|
||||
|
||||
if (chapterToUpdate > 0) {
|
||||
if (getPresenter().prefs.askUpdateMangaSync()) {
|
||||
new MaterialDialog.Builder(this)
|
||||
.content(getString(R.string.confirm_update_manga_sync, chapterToUpdate))
|
||||
.positiveText(R.string.button_yes)
|
||||
.negativeText(R.string.button_no)
|
||||
.onPositive((dialog, which) -> {
|
||||
getPresenter().updateMangaSyncLastChapterRead();
|
||||
})
|
||||
.onAny((dialog1, which1) -> {
|
||||
finish();
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
getPresenter().updateMangaSyncLastChapterRead();
|
||||
finish();
|
||||
}
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
public void onChapterError() {
|
||||
finish();
|
||||
ToastUtil.showShort(this, R.string.page_list_error);
|
||||
}
|
||||
|
||||
public void onChapterReady(List<Page> pages, Manga manga, Chapter chapter, int currentPage) {
|
||||
if (viewer == null) {
|
||||
viewer = createViewer(manga);
|
||||
getSupportFragmentManager().beginTransaction().replace(R.id.reader, viewer).commit();
|
||||
}
|
||||
viewer.onPageListReady(pages, currentPage);
|
||||
readerMenu.onChapterReady(pages.size(), manga, chapter, currentPage);
|
||||
}
|
||||
|
||||
public void onAdjacentChapters(Chapter previous, Chapter next) {
|
||||
readerMenu.onAdjacentChapters(previous, next);
|
||||
}
|
||||
|
||||
private BaseReader createViewer(Manga manga) {
|
||||
int mangaViewer = manga.viewer == 0 ? preferences.getDefaultViewer() : manga.viewer;
|
||||
|
||||
switch (mangaViewer) {
|
||||
case LEFT_TO_RIGHT: default:
|
||||
return new LeftToRightReader();
|
||||
case RIGHT_TO_LEFT:
|
||||
return new RightToLeftReader();
|
||||
case VERTICAL:
|
||||
return new VerticalReader();
|
||||
case WEBTOON:
|
||||
return new WebtoonReader();
|
||||
}
|
||||
}
|
||||
|
||||
public void onPageChanged(int currentPageIndex, int totalPages) {
|
||||
String page = (currentPageIndex + 1) + "/" + totalPages;
|
||||
pageNumber.setText(page);
|
||||
readerMenu.onPageChanged(currentPageIndex);
|
||||
}
|
||||
|
||||
public void setSelectedPage(int pageIndex) {
|
||||
viewer.setSelectedPage(pageIndex);
|
||||
}
|
||||
|
||||
public void onCenterSingleTap() {
|
||||
readerMenu.toggle();
|
||||
}
|
||||
|
||||
public void requestNextChapter() {
|
||||
getPresenter().setCurrentPage(viewer != null ? viewer.getCurrentPage() : 0);
|
||||
if (!getPresenter().loadNextChapter()) {
|
||||
ToastUtil.showShort(this, R.string.no_next_chapter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void requestPreviousChapter() {
|
||||
getPresenter().setCurrentPage(viewer != null ? viewer.getCurrentPage() : 0);
|
||||
if (!getPresenter().loadPreviousChapter()) {
|
||||
ToastUtil.showShort(this, R.string.no_previous_chapter);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSettings() {
|
||||
subscriptions.add(preferences.showPageNumber()
|
||||
.asObservable()
|
||||
.subscribe(this::setPageNumberVisibility));
|
||||
|
||||
subscriptions.add(preferences.lockOrientation()
|
||||
.asObservable()
|
||||
.subscribe(this::setOrientation));
|
||||
|
||||
subscriptions.add(preferences.hideStatusBar()
|
||||
.asObservable()
|
||||
.subscribe(this::setStatusBarVisibility));
|
||||
|
||||
subscriptions.add(preferences.keepScreenOn()
|
||||
.asObservable()
|
||||
.subscribe(this::setKeepScreenOn));
|
||||
|
||||
subscriptions.add(preferences.customBrightness()
|
||||
.asObservable()
|
||||
.subscribe(this::setCustomBrightness));
|
||||
|
||||
subscriptions.add(preferences.readerTheme()
|
||||
.asObservable()
|
||||
.distinctUntilChanged()
|
||||
.subscribe(this::applyTheme));
|
||||
}
|
||||
|
||||
private void setOrientation(boolean locked) {
|
||||
if (locked) {
|
||||
int orientation;
|
||||
int rotation = ((WindowManager) getSystemService(
|
||||
Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
|
||||
switch (rotation) {
|
||||
case Surface.ROTATION_0:
|
||||
orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
break;
|
||||
case Surface.ROTATION_90:
|
||||
orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
|
||||
break;
|
||||
default:
|
||||
orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
|
||||
break;
|
||||
}
|
||||
setRequestedOrientation(orientation);
|
||||
} else {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPageNumberVisibility(boolean visible) {
|
||||
pageNumber.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
private void setKeepScreenOn(boolean enabled) {
|
||||
if (enabled) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCustomBrightness(boolean enabled) {
|
||||
if (enabled) {
|
||||
subscriptions.add(customBrightnessSubscription = preferences.customBrightnessValue()
|
||||
.asObservable()
|
||||
.subscribe(this::setCustomBrightnessValue));
|
||||
} else {
|
||||
if (customBrightnessSubscription != null)
|
||||
subscriptions.remove(customBrightnessSubscription);
|
||||
setCustomBrightnessValue(-1);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCustomBrightnessValue(float value) {
|
||||
WindowManager.LayoutParams layout = getWindow().getAttributes();
|
||||
layout.screenBrightness = value;
|
||||
getWindow().setAttributes(layout);
|
||||
}
|
||||
|
||||
private void setStatusBarVisibility(boolean hidden) {
|
||||
createUiHideFlags(hidden);
|
||||
setSystemUiVisibility();
|
||||
}
|
||||
|
||||
private void createUiHideFlags(boolean statusBarHidden) {
|
||||
uiFlags = 0;
|
||||
uiFlags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
|
||||
if (statusBarHidden)
|
||||
uiFlags |= View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
uiFlags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
}
|
||||
|
||||
public void setSystemUiVisibility() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(uiFlags);
|
||||
}
|
||||
|
||||
protected void setMangaDefaultViewer(int viewer) {
|
||||
getPresenter().updateMangaViewer(viewer);
|
||||
recreate();
|
||||
}
|
||||
|
||||
private void applyTheme(int theme) {
|
||||
readerTheme = theme;
|
||||
View rootView = getWindow().getDecorView().getRootView();
|
||||
if (theme == BLACK_THEME) {
|
||||
rootView.setBackgroundColor(Color.BLACK);
|
||||
pageNumber.setTextColor(ContextCompat.getColor(this, R.color.light_grey));
|
||||
pageNumber.setBackgroundColor(ContextCompat.getColor(this, R.color.page_number_background_black));
|
||||
} else {
|
||||
rootView.setBackgroundColor(Color.WHITE);
|
||||
pageNumber.setTextColor(ContextCompat.getColor(this, R.color.primary_text));
|
||||
pageNumber.setBackgroundColor(ContextCompat.getColor(this, R.color.page_number_background));
|
||||
}
|
||||
}
|
||||
|
||||
public int getReaderTheme() {
|
||||
return readerTheme;
|
||||
}
|
||||
|
||||
public PreferencesHelper getPreferences() {
|
||||
return preferences;
|
||||
}
|
||||
|
||||
public BaseReader getViewer() {
|
||||
return viewer;
|
||||
}
|
||||
|
||||
public int getMaxBitmapSize() {
|
||||
return maxBitmapSize;
|
||||
}
|
||||
|
||||
}
|
||||
402
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderMenu.java
Normal file
402
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderMenu.java
Normal file
@@ -0,0 +1,402 @@
|
||||
package eu.kanade.tachiyomi.ui.reader;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
|
||||
import icepick.State;
|
||||
import rx.Subscription;
|
||||
|
||||
public class ReaderMenu {
|
||||
|
||||
@Bind(R.id.reader_menu) RelativeLayout menu;
|
||||
@Bind(R.id.reader_menu_bottom) LinearLayout bottomMenu;
|
||||
@Bind(R.id.toolbar) Toolbar toolbar;
|
||||
@Bind(R.id.current_page) TextView currentPage;
|
||||
@Bind(R.id.page_seeker) SeekBar seekBar;
|
||||
@Bind(R.id.total_pages) TextView totalPages;
|
||||
@Bind(R.id.lock_orientation) ImageButton lockOrientation;
|
||||
@Bind(R.id.reader_selector) ImageButton readerSelector;
|
||||
@Bind(R.id.reader_extra_settings) ImageButton extraSettings;
|
||||
@Bind(R.id.reader_brightness) ImageButton brightnessSettings;
|
||||
|
||||
private MenuItem nextChapterBtn;
|
||||
private MenuItem prevChapterBtn;
|
||||
private Chapter prevChapter;
|
||||
private Chapter nextChapter;
|
||||
|
||||
private ReaderActivity activity;
|
||||
private PreferencesHelper preferences;
|
||||
|
||||
@State boolean showing;
|
||||
private PopupWindow settingsPopup;
|
||||
private PopupWindow brightnessPopup;
|
||||
private boolean inverted;
|
||||
|
||||
private DecimalFormat decimalFormat;
|
||||
|
||||
public ReaderMenu(ReaderActivity activity) {
|
||||
this.activity = activity;
|
||||
this.preferences = activity.getPreferences();
|
||||
ButterKnife.bind(this, activity);
|
||||
|
||||
// Intercept all image events in this layout
|
||||
bottomMenu.setOnTouchListener((v, event) -> true);
|
||||
|
||||
seekBar.setOnSeekBarChangeListener(new PageSeekBarChangeListener());
|
||||
decimalFormat = new DecimalFormat("#.##");
|
||||
inverted = false;
|
||||
|
||||
initializeOptions();
|
||||
}
|
||||
|
||||
public void add(Subscription subscription) {
|
||||
activity.subscriptions.add(subscription);
|
||||
}
|
||||
|
||||
public void toggle() {
|
||||
if (showing)
|
||||
hide();
|
||||
else
|
||||
show(true);
|
||||
}
|
||||
|
||||
public void show(boolean animate) {
|
||||
menu.setVisibility(View.VISIBLE);
|
||||
|
||||
if (animate) {
|
||||
Animation toolbarAnimation = AnimationUtils.loadAnimation(activity, R.anim.enter_from_top);
|
||||
toolbar.startAnimation(toolbarAnimation);
|
||||
|
||||
Animation bottomMenuAnimation = AnimationUtils.loadAnimation(activity, R.anim.enter_from_bottom);
|
||||
bottomMenu.startAnimation(bottomMenuAnimation);
|
||||
}
|
||||
|
||||
showing = true;
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
Animation toolbarAnimation = AnimationUtils.loadAnimation(activity, R.anim.exit_to_top);
|
||||
toolbarAnimation.setAnimationListener(new HideMenuAnimationListener());
|
||||
toolbar.startAnimation(toolbarAnimation);
|
||||
|
||||
Animation bottomMenuAnimation = AnimationUtils.loadAnimation(activity, R.anim.exit_to_bottom);
|
||||
bottomMenu.startAnimation(bottomMenuAnimation);
|
||||
|
||||
settingsPopup.dismiss();
|
||||
brightnessPopup.dismiss();
|
||||
|
||||
showing = false;
|
||||
}
|
||||
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
activity.getMenuInflater().inflate(R.menu.reader, menu);
|
||||
nextChapterBtn = menu.findItem(R.id.action_next_chapter);
|
||||
prevChapterBtn = menu.findItem(R.id.action_previous_chapter);
|
||||
setAdjacentChaptersVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item == prevChapterBtn) {
|
||||
activity.requestPreviousChapter();
|
||||
} else if (item == nextChapterBtn) {
|
||||
activity.requestNextChapter();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onChapterReady(int numPages, Manga manga, Chapter chapter, int currentPageIndex) {
|
||||
if (manga.viewer == ReaderActivity.RIGHT_TO_LEFT && !inverted) {
|
||||
// Invert the seekbar and textview fields for the right to left reader
|
||||
seekBar.setRotation(180);
|
||||
TextView aux = currentPage;
|
||||
currentPage = totalPages;
|
||||
totalPages = aux;
|
||||
// Don't invert again on chapter change
|
||||
inverted = true;
|
||||
}
|
||||
|
||||
// Set initial values
|
||||
totalPages.setText("" + numPages);
|
||||
currentPage.setText("" + (currentPageIndex + 1));
|
||||
seekBar.setProgress(currentPageIndex);
|
||||
seekBar.setMax(numPages - 1);
|
||||
|
||||
activity.setToolbarTitle(manga.title);
|
||||
activity.setToolbarSubtitle(chapter.chapter_number != -1 ?
|
||||
activity.getString(R.string.chapter_subtitle,
|
||||
decimalFormat.format(chapter.chapter_number)) :
|
||||
chapter.name);
|
||||
|
||||
}
|
||||
|
||||
public void onPageChanged(int pageIndex) {
|
||||
currentPage.setText("" + (pageIndex + 1));
|
||||
seekBar.setProgress(pageIndex);
|
||||
}
|
||||
|
||||
public void onAdjacentChapters(Chapter previous, Chapter next) {
|
||||
prevChapter = previous;
|
||||
nextChapter = next;
|
||||
setAdjacentChaptersVisibility();
|
||||
}
|
||||
|
||||
private void setAdjacentChaptersVisibility() {
|
||||
if (prevChapterBtn != null) prevChapterBtn.setVisible(prevChapter != null);
|
||||
if (nextChapterBtn != null) nextChapterBtn.setVisible(nextChapter != null);
|
||||
}
|
||||
|
||||
private void initializeOptions() {
|
||||
// Orientation changes
|
||||
add(preferences.lockOrientation().asObservable()
|
||||
.subscribe(locked -> {
|
||||
int resourceId = !locked ? R.drawable.ic_screen_rotation :
|
||||
activity.getResources().getConfiguration().orientation == 1 ?
|
||||
R.drawable.ic_screen_lock_portrait :
|
||||
R.drawable.ic_screen_lock_landscape;
|
||||
|
||||
lockOrientation.setImageResource(resourceId);
|
||||
}));
|
||||
|
||||
lockOrientation.setOnClickListener(v ->
|
||||
preferences.lockOrientation().set(!preferences.lockOrientation().get()));
|
||||
|
||||
// Reader selector
|
||||
readerSelector.setOnClickListener(v -> {
|
||||
final Manga manga = activity.getPresenter().getManga();
|
||||
showImmersiveDialog(new MaterialDialog.Builder(activity)
|
||||
.items(R.array.viewers_selector)
|
||||
.itemsCallbackSingleChoice(manga.viewer,
|
||||
(d, itemView, which, text) -> {
|
||||
activity.setMangaDefaultViewer(which);
|
||||
return true;
|
||||
})
|
||||
.build());
|
||||
});
|
||||
|
||||
// Extra settings menu
|
||||
final View popupView = activity.getLayoutInflater().inflate(R.layout.reader_popup, null);
|
||||
settingsPopup = new SettingsPopupWindow(popupView);
|
||||
|
||||
extraSettings.setOnClickListener(v -> {
|
||||
if (!settingsPopup.isShowing())
|
||||
settingsPopup.showAtLocation(extraSettings,
|
||||
Gravity.BOTTOM | Gravity.RIGHT, 0, bottomMenu.getHeight());
|
||||
else
|
||||
settingsPopup.dismiss();
|
||||
});
|
||||
|
||||
// Brightness popup
|
||||
final View brightnessView = activity.getLayoutInflater().inflate(R.layout.reader_brightness, null);
|
||||
brightnessPopup = new BrightnessPopupWindow(brightnessView);
|
||||
|
||||
brightnessSettings.setOnClickListener(v -> {
|
||||
if (!brightnessPopup.isShowing())
|
||||
brightnessPopup.showAtLocation(brightnessSettings,
|
||||
Gravity.BOTTOM | Gravity.LEFT, 0, bottomMenu.getHeight());
|
||||
else
|
||||
brightnessPopup.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
private void showImmersiveDialog(Dialog dialog) {
|
||||
// Hack to not leave immersive mode
|
||||
dialog.getWindow().setFlags(LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
dialog.getWindow().getDecorView().setSystemUiVisibility(
|
||||
activity.getWindow().getDecorView().getSystemUiVisibility());
|
||||
dialog.show();
|
||||
dialog.getWindow().clearFlags(LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
WindowManager wm = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
|
||||
wm.updateViewLayout(activity.getWindow().getDecorView(), activity.getWindow().getAttributes());
|
||||
}
|
||||
|
||||
class SettingsPopupWindow extends PopupWindow {
|
||||
|
||||
@Bind(R.id.enable_transitions) CheckBox enableTransitions;
|
||||
@Bind(R.id.show_page_number) CheckBox showPageNumber;
|
||||
@Bind(R.id.hide_status_bar) CheckBox hideStatusBar;
|
||||
@Bind(R.id.keep_screen_on) CheckBox keepScreenOn;
|
||||
@Bind(R.id.reader_theme) CheckBox readerTheme;
|
||||
@Bind(R.id.image_decoder) TextView imageDecoder;
|
||||
@Bind(R.id.image_decoder_initial) TextView imageDecoderInitial;
|
||||
|
||||
public SettingsPopupWindow(View view) {
|
||||
super(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
setAnimationStyle(R.style.reader_settings_popup_animation);
|
||||
ButterKnife.bind(this, view);
|
||||
initializePopupMenu();
|
||||
}
|
||||
|
||||
private void initializePopupMenu() {
|
||||
// Load values from preferences
|
||||
enableTransitions.setChecked(preferences.enableTransitions().get());
|
||||
showPageNumber.setChecked(preferences.showPageNumber().get());
|
||||
hideStatusBar.setChecked(preferences.hideStatusBar().get());
|
||||
keepScreenOn.setChecked(preferences.keepScreenOn().get());
|
||||
readerTheme.setChecked(preferences.readerTheme().get() == 1);
|
||||
setDecoderInitial(preferences.imageDecoder().get());
|
||||
|
||||
// Add a listener to change the corresponding setting
|
||||
enableTransitions.setOnCheckedChangeListener((view, isChecked) ->
|
||||
preferences.enableTransitions().set(isChecked));
|
||||
|
||||
showPageNumber.setOnCheckedChangeListener((view, isChecked) ->
|
||||
preferences.showPageNumber().set(isChecked));
|
||||
|
||||
hideStatusBar.setOnCheckedChangeListener((view, isChecked) ->
|
||||
preferences.hideStatusBar().set(isChecked));
|
||||
|
||||
keepScreenOn.setOnCheckedChangeListener((view, isChecked) ->
|
||||
preferences.keepScreenOn().set(isChecked));
|
||||
|
||||
readerTheme.setOnCheckedChangeListener((view, isChecked) ->
|
||||
preferences.readerTheme().set(isChecked ? 1 : 0));
|
||||
|
||||
imageDecoder.setOnClickListener(v -> {
|
||||
showImmersiveDialog(new MaterialDialog.Builder(activity)
|
||||
.title(R.string.pref_image_decoder)
|
||||
.items(R.array.image_decoders)
|
||||
.itemsCallbackSingleChoice(preferences.imageDecoder().get(),
|
||||
(dialog, itemView, which, text) -> {
|
||||
preferences.imageDecoder().set(which);
|
||||
setDecoderInitial(which);
|
||||
return true;
|
||||
})
|
||||
.build());
|
||||
});
|
||||
}
|
||||
|
||||
private void setDecoderInitial(int decoder) {
|
||||
String initial;
|
||||
switch (decoder) {
|
||||
case BaseReader.SKIA_DECODER:
|
||||
initial = "S";
|
||||
break;
|
||||
case BaseReader.RAPID_DECODER:
|
||||
initial = "R";
|
||||
break;
|
||||
default:
|
||||
initial = "";
|
||||
break;
|
||||
}
|
||||
imageDecoderInitial.setText(initial);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class BrightnessPopupWindow extends PopupWindow {
|
||||
|
||||
@Bind(R.id.custom_brightness) CheckBox customBrightness;
|
||||
@Bind(R.id.brightness_seekbar) SeekBar brightnessSeekbar;
|
||||
|
||||
public BrightnessPopupWindow(View view) {
|
||||
super(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
setAnimationStyle(R.style.reader_brightness_popup_animation);
|
||||
ButterKnife.bind(this, view);
|
||||
initializePopupMenu();
|
||||
}
|
||||
|
||||
private void initializePopupMenu() {
|
||||
add(preferences.customBrightness()
|
||||
.asObservable()
|
||||
.subscribe(isEnabled -> {
|
||||
customBrightness.setChecked(isEnabled);
|
||||
brightnessSeekbar.setEnabled(isEnabled);
|
||||
}));
|
||||
|
||||
customBrightness.setOnCheckedChangeListener((view, isChecked) ->
|
||||
preferences.customBrightness().set(isChecked));
|
||||
|
||||
brightnessSeekbar.setMax(100);
|
||||
brightnessSeekbar.setProgress(Math.round(
|
||||
preferences.customBrightnessValue().get() * brightnessSeekbar.getMax()));
|
||||
brightnessSeekbar.setOnSeekBarChangeListener(new BrightnessSeekBarChangeListener());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PageSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
activity.setSelectedPage(progress);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
}
|
||||
|
||||
class BrightnessSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
preferences.customBrightnessValue().set((float) progress / seekBar.getMax());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class HideMenuAnimationListener implements Animation.AnimationListener {
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
menu.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package eu.kanade.tachiyomi.ui.reader;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager;
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService;
|
||||
import eu.kanade.tachiyomi.event.ReaderEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import icepick.State;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import rx.subjects.PublishSubject;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
|
||||
@Inject PreferencesHelper prefs;
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject DownloadManager downloadManager;
|
||||
@Inject MangaSyncManager syncManager;
|
||||
@Inject SourceManager sourceManager;
|
||||
|
||||
@State Manga manga;
|
||||
@State Chapter chapter;
|
||||
@State int sourceId;
|
||||
@State boolean isDownloaded;
|
||||
@State int currentPage;
|
||||
private Source source;
|
||||
private Chapter nextChapter;
|
||||
private Chapter previousChapter;
|
||||
private List<Page> pageList;
|
||||
private List<Page> nextChapterPageList;
|
||||
private List<MangaSync> mangaSyncList;
|
||||
|
||||
private PublishSubject<Page> retryPageSubject;
|
||||
|
||||
private static final int GET_PAGE_LIST = 1;
|
||||
private static final int GET_PAGE_IMAGES = 2;
|
||||
private static final int GET_ADJACENT_CHAPTERS = 3;
|
||||
private static final int RETRY_IMAGES = 4;
|
||||
private static final int PRELOAD_NEXT_CHAPTER = 5;
|
||||
private static final int GET_MANGA_SYNC = 6;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
retryPageSubject = PublishSubject.create();
|
||||
|
||||
restartableLatestCache(PRELOAD_NEXT_CHAPTER,
|
||||
this::getPreloadNextChapterObservable,
|
||||
(view, pages) -> {},
|
||||
(view, error) -> Timber.e("An error occurred while preloading a chapter"));
|
||||
|
||||
restartableLatestCache(GET_PAGE_IMAGES,
|
||||
this::getPageImagesObservable,
|
||||
(view, page) -> {},
|
||||
(view, error) -> Timber.e("An error occurred while downloading an image"));
|
||||
|
||||
restartableLatestCache(GET_ADJACENT_CHAPTERS,
|
||||
this::getAdjacentChaptersObservable,
|
||||
(view, pair) -> view.onAdjacentChapters(pair.first, pair.second),
|
||||
(view, error) -> Timber.e("An error occurred while getting adjacent chapters"));
|
||||
|
||||
restartableLatestCache(RETRY_IMAGES,
|
||||
this::getRetryPageObservable,
|
||||
(view, page) -> {},
|
||||
(view, error) -> Timber.e("An error occurred while downloading an image"));
|
||||
|
||||
restartableLatestCache(GET_PAGE_LIST,
|
||||
() -> getPageListObservable()
|
||||
.doOnNext(pages -> pageList = pages)
|
||||
.doOnCompleted(() -> {
|
||||
start(GET_ADJACENT_CHAPTERS);
|
||||
start(GET_PAGE_IMAGES);
|
||||
start(RETRY_IMAGES);
|
||||
}),
|
||||
(view, pages) -> view.onChapterReady(pages, manga, chapter, currentPage),
|
||||
(view, error) -> view.onChapterError());
|
||||
|
||||
restartableFirst(GET_MANGA_SYNC, this::getMangaSyncObservable,
|
||||
(view, mangaSync) -> {},
|
||||
(view, error) -> {});
|
||||
|
||||
registerForStickyEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterForEvents();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSave(@NonNull Bundle state) {
|
||||
onChapterLeft();
|
||||
super.onSave(state);
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
source = sourceManager.get(sourceId);
|
||||
|
||||
// These are started by GET_PAGE_LIST, so we don't let them restart itselves
|
||||
stop(GET_PAGE_IMAGES);
|
||||
stop(GET_ADJACENT_CHAPTERS);
|
||||
stop(RETRY_IMAGES);
|
||||
stop(PRELOAD_NEXT_CHAPTER);
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(ReaderEvent event) {
|
||||
EventBus.getDefault().removeStickyEvent(event);
|
||||
manga = event.getManga();
|
||||
source = event.getSource();
|
||||
sourceId = source.getId();
|
||||
loadChapter(event.getChapter());
|
||||
if (prefs.autoUpdateMangaSync()) {
|
||||
start(GET_MANGA_SYNC);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the page list of a chapter
|
||||
private Observable<List<Page>> getPageListObservable() {
|
||||
return isDownloaded ?
|
||||
// Fetch the page list from disk
|
||||
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)) :
|
||||
// Fetch the page list from cache or fallback to network
|
||||
source.getCachedPageListOrPullFromNetwork(chapter.url)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
// Get the chapter images from network or disk
|
||||
private Observable<Page> getPageImagesObservable() {
|
||||
Observable<Page> pageObservable;
|
||||
|
||||
if (!isDownloaded) {
|
||||
pageObservable = source.getAllImageUrlsFromPageList(pageList)
|
||||
.flatMap(source::getCachedImage, 2);
|
||||
} else {
|
||||
File chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter);
|
||||
pageObservable = Observable.from(pageList)
|
||||
.flatMap(page -> downloadManager.getDownloadedImage(page, chapterDir));
|
||||
}
|
||||
return pageObservable.subscribeOn(Schedulers.io())
|
||||
.doOnCompleted(this::preloadNextChapter);
|
||||
}
|
||||
|
||||
private Observable<Pair<Chapter, Chapter>> getAdjacentChaptersObservable() {
|
||||
return Observable.zip(
|
||||
db.getPreviousChapter(chapter).createObservable().take(1),
|
||||
db.getNextChapter(chapter).createObservable().take(1),
|
||||
Pair::create)
|
||||
.doOnNext(pair -> {
|
||||
previousChapter = pair.first;
|
||||
nextChapter = pair.second;
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
// Listen for retry page events
|
||||
private Observable<Page> getRetryPageObservable() {
|
||||
return retryPageSubject
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap(page -> page.getImageUrl() == null ?
|
||||
source.getImageUrlFromPage(page) :
|
||||
Observable.just(page))
|
||||
.flatMap(source::getCachedImage)
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
// Preload the first pages of the next chapter
|
||||
private Observable<Page> getPreloadNextChapterObservable() {
|
||||
return source.getCachedPageListOrPullFromNetwork(nextChapter.url)
|
||||
.flatMap(pages -> {
|
||||
nextChapterPageList = pages;
|
||||
// Preload at most 5 pages
|
||||
int pagesToPreload = Math.min(pages.size(), 5);
|
||||
return Observable.from(pages).take(pagesToPreload);
|
||||
})
|
||||
.concatMap(page -> page.getImageUrl() == null ?
|
||||
source.getImageUrlFromPage(page) :
|
||||
Observable.just(page))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnCompleted(this::stopPreloadingNextChapter);
|
||||
}
|
||||
|
||||
private Observable<List<MangaSync>> getMangaSyncObservable() {
|
||||
return db.getMangasSync(manga).createObservable()
|
||||
.doOnNext(mangaSync -> this.mangaSyncList = mangaSync);
|
||||
}
|
||||
|
||||
// Loads the given chapter
|
||||
private void loadChapter(Chapter chapter) {
|
||||
// Before loading the chapter, stop preloading (if it's working) and save current progress
|
||||
stopPreloadingNextChapter();
|
||||
|
||||
this.chapter = chapter;
|
||||
isDownloaded = isChapterDownloaded(chapter);
|
||||
|
||||
// If the chapter is partially read, set the starting page to the last the user read
|
||||
if (!chapter.read && chapter.last_page_read != 0)
|
||||
currentPage = chapter.last_page_read;
|
||||
else
|
||||
currentPage = 0;
|
||||
|
||||
// Reset next and previous chapter. They have to be fetched again
|
||||
nextChapter = null;
|
||||
previousChapter = null;
|
||||
nextChapterPageList = null;
|
||||
|
||||
start(GET_PAGE_LIST);
|
||||
}
|
||||
|
||||
// Check whether the given chapter is downloaded
|
||||
public boolean isChapterDownloaded(Chapter chapter) {
|
||||
return downloadManager.isChapterDownloaded(source, manga, chapter);
|
||||
}
|
||||
|
||||
public void retryPage(Page page) {
|
||||
page.setStatus(Page.QUEUE);
|
||||
retryPageSubject.onNext(page);
|
||||
}
|
||||
|
||||
// Called before loading another chapter or leaving the reader. It allows to do operations
|
||||
// over the chapter read like saving progress
|
||||
public void onChapterLeft() {
|
||||
if (pageList == null)
|
||||
return;
|
||||
|
||||
// Cache current page list progress for online chapters to allow a faster reopen
|
||||
if (!isDownloaded)
|
||||
source.savePageList(chapter.url, pageList);
|
||||
|
||||
// Save current progress of the chapter. Mark as read if the chapter is finished
|
||||
chapter.last_page_read = currentPage;
|
||||
if (isChapterFinished()) {
|
||||
chapter.read = true;
|
||||
}
|
||||
db.insertChapter(chapter).createObservable().subscribe();
|
||||
}
|
||||
|
||||
// Check whether the chapter has been read
|
||||
private boolean isChapterFinished() {
|
||||
return !chapter.read && currentPage == pageList.size() - 1;
|
||||
}
|
||||
|
||||
public int getMangaSyncChapterToUpdate() {
|
||||
if (pageList == null || mangaSyncList == null || mangaSyncList.isEmpty())
|
||||
return 0;
|
||||
|
||||
int lastChapterReadLocal = 0;
|
||||
// If the current chapter has been read, we check with this one
|
||||
if (chapter.read)
|
||||
lastChapterReadLocal = (int) Math.floor(chapter.chapter_number);
|
||||
// If not, we check if the previous chapter has been read
|
||||
else if (previousChapter != null && previousChapter.read)
|
||||
lastChapterReadLocal = (int) Math.floor(previousChapter.chapter_number);
|
||||
|
||||
// We know the chapter we have to check, but we don't know yet if an update is required.
|
||||
// This boolean is used to return 0 if no update is required
|
||||
boolean hasToUpdate = false;
|
||||
|
||||
for (MangaSync mangaSync : mangaSyncList) {
|
||||
if (lastChapterReadLocal > mangaSync.last_chapter_read) {
|
||||
mangaSync.last_chapter_read = lastChapterReadLocal;
|
||||
mangaSync.update = true;
|
||||
hasToUpdate = true;
|
||||
}
|
||||
}
|
||||
return hasToUpdate ? lastChapterReadLocal : 0;
|
||||
}
|
||||
|
||||
public void updateMangaSyncLastChapterRead() {
|
||||
for (MangaSync mangaSync : mangaSyncList) {
|
||||
MangaSyncService service = syncManager.getSyncService(mangaSync.sync_id);
|
||||
if (service.isLogged() && mangaSync.update) {
|
||||
UpdateMangaSyncService.start(getContext(), mangaSync);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCurrentPage(int currentPage) {
|
||||
this.currentPage = currentPage;
|
||||
}
|
||||
|
||||
public boolean loadNextChapter() {
|
||||
if (hasNextChapter()) {
|
||||
onChapterLeft();
|
||||
loadChapter(nextChapter);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean loadPreviousChapter() {
|
||||
if (hasPreviousChapter()) {
|
||||
onChapterLeft();
|
||||
loadChapter(previousChapter);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasNextChapter() {
|
||||
return nextChapter != null;
|
||||
}
|
||||
|
||||
public boolean hasPreviousChapter() {
|
||||
return previousChapter != null;
|
||||
}
|
||||
|
||||
private void preloadNextChapter() {
|
||||
if (hasNextChapter() && !isChapterDownloaded(nextChapter)) {
|
||||
start(PRELOAD_NEXT_CHAPTER);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopPreloadingNextChapter() {
|
||||
if (isSubscribed(PRELOAD_NEXT_CHAPTER)) {
|
||||
stop(PRELOAD_NEXT_CHAPTER);
|
||||
if (nextChapterPageList != null)
|
||||
source.savePageList(nextChapter.url, nextChapterPageList);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateMangaViewer(int viewer) {
|
||||
manga.viewer = viewer;
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
}
|
||||
|
||||
public Manga getManga() {
|
||||
return manga;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.base;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder;
|
||||
import com.davemorrissey.labs.subscaleview.decoder.RapidImageRegionDecoder;
|
||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
||||
|
||||
public abstract class BaseReader extends BaseFragment {
|
||||
|
||||
protected int currentPage;
|
||||
protected List<Page> pages;
|
||||
protected Class<? extends ImageRegionDecoder> regionDecoderClass;
|
||||
|
||||
public static final int RAPID_DECODER = 0;
|
||||
public static final int SKIA_DECODER = 1;
|
||||
|
||||
public void updatePageNumber() {
|
||||
getReaderActivity().onPageChanged(getCurrentPage(), getTotalPages());
|
||||
}
|
||||
|
||||
public int getCurrentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
public int getPageForPosition(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
public int getPositionForPage(int page) {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void onPageChanged(int position) {
|
||||
currentPage = getPageForPosition(position);
|
||||
updatePageNumber();
|
||||
}
|
||||
|
||||
public int getTotalPages() {
|
||||
return pages == null ? 0 : pages.size();
|
||||
}
|
||||
|
||||
public abstract void setSelectedPage(int pageNumber);
|
||||
public abstract void onPageListReady(List<Page> pages, int currentPage);
|
||||
public abstract boolean onImageTouch(MotionEvent motionEvent);
|
||||
|
||||
public void setRegionDecoderClass(int value) {
|
||||
switch (value) {
|
||||
case RAPID_DECODER:
|
||||
default:
|
||||
regionDecoderClass = RapidImageRegionDecoder.class;
|
||||
break;
|
||||
case SKIA_DECODER:
|
||||
regionDecoderClass = SkiaImageRegionDecoder.class;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Class<? extends ImageRegionDecoder> getRegionDecoderClass() {
|
||||
return regionDecoderClass;
|
||||
}
|
||||
|
||||
public ReaderActivity getReaderActivity() {
|
||||
return (ReaderActivity) getActivity();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user