From 8b52fea602686db7544b907ab69a4e07af32a7bd Mon Sep 17 00:00:00 2001 From: NoodleMage Date: Thu, 28 Jan 2016 19:54:04 +0100 Subject: [PATCH] Can now manually set cover pictures. #79 Forgot to add IOHandler Removed FAB library now use the internal one. Changed getTimestamp to modification date. Rewrote IOHandler. Fixed Drive Bug. More bug fixes. Tested working for API 16 and 23 Fixed merge bugs --- app/build.gradle | 3 + .../tachiyomi/data/cache/CoverCache.java | 6 +- .../injection/component/AppComponent.java | 1 - .../eu/kanade/tachiyomi/io/IOHandler.java | 110 ++++++++++++++++++ .../ui/manga/info/MangaInfoFragment.java | 81 +++++++++++-- .../ui/manga/info/MangaInfoPresenter.java | 71 +++++++++-- .../main/res/layout/fragment_manga_info.xml | 86 ++++++++------ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 4 + 9 files changed, 311 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/io/IOHandler.java 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