diff --git a/app/build.gradle b/app/build.gradle index 97749683e7..5152b88405 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -130,12 +130,15 @@ dependencies { compile('com.mikepenz:materialdrawer:4.6.4@aar') { transitive = true } + + //Google material icons SVG. compile 'com.mikepenz:google-material-typeface:2.1.0.1.original@aar' compile('com.github.afollestad.material-dialogs:core:0.8.5.3@aar') { transitive = true } + testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:2.3.0' testCompile "org.mockito:mockito-core:$MOCKITO_VERSION" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.java b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.java index b7fca9556e..90fc078620 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.java +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.java @@ -11,6 +11,7 @@ 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 com.bumptech.glide.signature.StringSignature; import java.io.File; import java.io.FileInputStream; @@ -119,7 +120,7 @@ public class CoverCache { * @param source the cover image. * @throws IOException exception returned */ - private void copyToLocalCache(String thumbnailUrl, File source) throws IOException { + public void copyToLocalCache(String thumbnailUrl, File source) throws IOException { // Create cache directory if needed. createCacheDir(); @@ -200,11 +201,12 @@ public class CoverCache { * @param imageView imageView where picture should be displayed. * @param file file to load. Must exist!. */ - private void loadFromCache(ImageView imageView, File file) { + public void loadFromCache(ImageView imageView, File file) { Glide.with(context) .load(file) .diskCacheStrategy(DiskCacheStrategy.RESULT) .centerCrop() + .signature(new StringSignature(String.valueOf(file.lastModified()))) .into(imageView); } diff --git a/app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.java b/app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.java index c2d2faed6e..4746f0463a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.java +++ b/app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.java @@ -59,7 +59,6 @@ public interface AppComponent { void inject(LibraryUpdateService libraryUpdateService); void inject(DownloadService downloadService); void inject(UpdateMangaSyncService updateMangaSyncService); - Application application(); } diff --git a/app/src/main/java/eu/kanade/tachiyomi/io/IOHandler.java b/app/src/main/java/eu/kanade/tachiyomi/io/IOHandler.java new file mode 100644 index 0000000000..6ed4f83afe --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/io/IOHandler.java @@ -0,0 +1,110 @@ +package eu.kanade.tachiyomi.io; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.MediaStore; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +public class IOHandler { + /** + * Get full filepath of build in Android File picker. + * If Google Drive (or other Cloud service) throw exception and download before loading + */ + public static String getFilePath(Uri uri, ContentResolver resolver, Context context) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + String filePath = ""; + String wholeID = DocumentsContract.getDocumentId(uri); + + //Ugly work around. In sdk version Kitkat or higher external getDocumentId request will have no content:// + if (wholeID.split(":").length == 1) + throw new IllegalArgumentException(); + + // Split at colon, use second item in the array + String id = wholeID.split(":")[1]; + + String[] column = {MediaStore.Images.Media.DATA}; + + // where id is equal to + String sel = MediaStore.Images.Media._ID + "=?"; + + Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + column, sel, new String[]{id}, null); + + int columnIndex = cursor != null ? cursor.getColumnIndex(column[0]) : 0; + + if (cursor != null ? cursor.moveToFirst() : false) { + filePath = cursor.getString(columnIndex); + } + cursor.close(); + return filePath; + } else { + String[] fields = {MediaStore.Images.Media.DATA}; + + Cursor cursor = resolver.query(uri, fields, null, null, null); + + if (cursor == null) + return null; + + cursor.moveToFirst(); + String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); + cursor.close(); + + return path; + } + } catch (IllegalArgumentException e) { + //This exception is thrown when Google Drive. Try to download file + return downloadMediaAndReturnPath(uri, resolver, context); + } + } + + private static String getTempFilename(Context context) throws IOException { + File outputDir = context.getCacheDir(); + File outputFile = File.createTempFile("temp_cover", "0", outputDir); + return outputFile.getAbsolutePath(); + } + + private static String downloadMediaAndReturnPath(Uri uri, ContentResolver resolver, Context context) { + if (uri == null) return null; + FileInputStream input = null; + FileOutputStream output = null; + try { + ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r"); + FileDescriptor fd = pfd != null ? pfd.getFileDescriptor() : null; + input = new FileInputStream(fd); + + String tempFilename = getTempFilename(context); + output = new FileOutputStream(tempFilename); + + int read; + byte[] bytes = new byte[4096]; + while ((read = input.read(bytes)) != -1) { + output.write(bytes, 0, read); + } + return tempFilename; + } catch (IOException ignored) { + } finally { + if (input != null) try { + input.close(); + } catch (Exception ignored) { + } + if (output != null) try { + output.close(); + } catch (Exception ignored) { + } + } + return null; + + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.java index 033e8e7ce8..ffa9e1ee5e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.java @@ -1,6 +1,11 @@ package eu.kanade.tachiyomi.ui.manga.info; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.v4.content.ContextCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.view.LayoutInflater; import android.view.View; @@ -10,6 +15,11 @@ import android.widget.ImageView; import android.widget.TextView; import com.bumptech.glide.load.model.LazyHeaders; +import com.mikepenz.google_material_typeface_library.GoogleMaterial; +import com.mikepenz.iconics.IconicsDrawable; + +import java.io.File; +import java.io.IOException; import butterknife.Bind; import butterknife.ButterKnife; @@ -17,14 +27,16 @@ 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.io.IOHandler; import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment; +import eu.kanade.tachiyomi.util.ToastUtil; import nucleus.factory.RequiresPresenter; @RequiresPresenter(MangaInfoPresenter.class) public class MangaInfoFragment extends BaseRxFragment { + private static final int REQUEST_IMAGE_OPEN = 101; @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; @@ -33,9 +45,8 @@ public class MangaInfoFragment extends BaseRxFragment { @Bind(R.id.manga_source) TextView source; @Bind(R.id.manga_summary) TextView description; @Bind(R.id.manga_cover) ImageView cover; - @Bind(R.id.action_favorite) Button favoriteBtn; - + @Bind(R.id.fab_edit) FloatingActionButton fabEdit; public static MangaInfoFragment newInstance() { return new MangaInfoFragment(); @@ -54,9 +65,20 @@ public class MangaInfoFragment extends BaseRxFragment { View view = inflater.inflate(R.layout.fragment_manga_info, container, false); ButterKnife.bind(this, view); - favoriteBtn.setOnClickListener(v -> { - getPresenter().toggleFavorite(); - }); + //Create edit drawable with size 24dp (google guidelines) + IconicsDrawable edit = new IconicsDrawable(this.getContext()) + .icon(GoogleMaterial.Icon.gmd_edit) + .color(ContextCompat.getColor(this.getContext(), R.color.white)) + .sizeDp(24); + + // Update image of fab buttons + fabEdit.setImageDrawable(edit); + + // Set listener. + fabEdit.setOnClickListener(v -> MangaInfoFragment.this.selectImage()); + + favoriteBtn.setOnClickListener(v -> getPresenter().toggleFavorite()); + swipeRefresh.setOnRefreshListener(this::fetchMangaFromSource); return view; @@ -71,6 +93,12 @@ public class MangaInfoFragment extends BaseRxFragment { } } + /** + * Set the info of the manga + * + * @param manga manga object containing information about manga + * @param mangaSource the source of the manga + */ private void setMangaInfo(Manga manga, Source mangaSource) { artist.setText(manga.artist); author.setText(manga.author); @@ -99,7 +127,7 @@ public class MangaInfoFragment extends BaseRxFragment { chapterCount.setText(String.valueOf(count)); } - public void setFavoriteText(boolean isFavorite) { + private void setFavoriteText(boolean isFavorite) { favoriteBtn.setText(!isFavorite ? R.string.add_to_library : R.string.remove_from_library); } @@ -108,6 +136,45 @@ public class MangaInfoFragment extends BaseRxFragment { getPresenter().fetchMangaFromSource(); } + private void selectImage() { + if (getPresenter().getManga().favorite) { + + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_GET_CONTENT); + startActivityForResult(Intent.createChooser(intent, + getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN); + } else { + ToastUtil.showShort(getContext(), R.string.notification_first_add_to_library); + } + + } + + + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + if (requestCode == REQUEST_IMAGE_OPEN) { + // Get the file's content URI from the incoming Intent + Uri selectedImageUri = data.getData(); + + // Convert to absolute path to prevent FileNotFoundException + String result = IOHandler.getFilePath(selectedImageUri, this.getContext().getContentResolver(), this.getContext()); + + // Get file from filepath + File picture = new File(result != null ? result : ""); + + + try { + // Update cover to selected file + getPresenter().editCoverWithLocalFile(picture, cover); + + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + public void onFetchMangaDone() { setRefreshing(false); } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.java index 6f9d9ef1bf..a947279d61 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.java @@ -1,6 +1,10 @@ package eu.kanade.tachiyomi.ui.manga.info; import android.os.Bundle; +import android.widget.ImageView; + +import java.io.File; +import java.io.IOException; import javax.inject.Inject; @@ -19,17 +23,42 @@ import rx.schedulers.Schedulers; public class MangaInfoPresenter extends BasePresenter { - @Inject DatabaseHelper db; - @Inject SourceManager sourceManager; - @Inject CoverCache coverCache; - - protected Source source; - private Manga manga; - private int count = -1; - + /** + * The id of the restartable. + */ private static final int GET_MANGA = 1; + /** + * The id of the restartable. + */ private static final int GET_CHAPTER_COUNT = 2; + /** + * The id of the restartable. + */ private static final int FETCH_MANGA_INFO = 3; + /** + * Source information + */ + protected Source source; + /** + * Used to connect to database + */ + @Inject DatabaseHelper db; + /** + * Used to connect to different manga sources + */ + @Inject SourceManager sourceManager; + /** + * Used to connect to cache + */ + @Inject CoverCache coverCache; + /** + * Selected manga information + */ + private Manga manga; + /** + * Count of chapters + */ + private int count = -1; @Override protected void onCreate(Bundle savedState) { @@ -39,22 +68,29 @@ public class MangaInfoPresenter extends BasePresenter { onProcessRestart(); } + // Update manga cache restartableLatestCache(GET_MANGA, () -> Observable.just(manga), (view, manga) -> view.onNextManga(manga, source)); + // Update chapter count restartableLatestCache(GET_CHAPTER_COUNT, () -> Observable.just(count), MangaInfoFragment::setChapterCount); + // Fetch manga info from source restartableFirst(FETCH_MANGA_INFO, this::fetchMangaObs, (view, manga) -> view.onFetchMangaDone(), (view, error) -> view.onFetchMangaError()); + // onEventMainThread receives an event thanks to this line. registerForStickyEvents(); } + /** + * Called when savedState not null + */ private void onProcessRestart() { stop(GET_MANGA); stop(GET_CHAPTER_COUNT); @@ -82,6 +118,9 @@ public class MangaInfoPresenter extends BasePresenter { } } + /** + * Fetch manga info from source + */ public void fetchMangaFromSource() { if (isUnsubscribed(FETCH_MANGA_INFO)) { start(FETCH_MANGA_INFO); @@ -107,6 +146,16 @@ public class MangaInfoPresenter extends BasePresenter { refreshManga(); } + /** + * Update cover with local file + */ + public void editCoverWithLocalFile(File file, ImageView imageView) throws IOException { + if (manga.favorite) { + coverCache.copyToLocalCache(manga.thumbnail_url, file); + coverCache.loadFromCache(imageView, file); + } + } + private void onMangaFavoriteChange(boolean isFavorite) { if (isFavorite) { coverCache.save(manga.thumbnail_url, source.getGlideHeaders()); @@ -115,8 +164,12 @@ public class MangaInfoPresenter extends BasePresenter { } } + public Manga getManga() { + return manga; + } + // Used to refresh the view - private void refreshManga() { + protected void refreshManga() { start(GET_MANGA); } diff --git a/app/src/main/res/layout/fragment_manga_info.xml b/app/src/main/res/layout/fragment_manga_info.xml index f5eaeabf15..9bf55d8dc8 100644 --- a/app/src/main/res/layout/fragment_manga_info.xml +++ b/app/src/main/res/layout/fragment_manga_info.xml @@ -1,10 +1,11 @@ - + + Select cover image