瀏覽代碼

Merge pull request #91 from NoodleMage/change_cover

Can now manually set cover pictures. #79
inorichi 9 年之前
父節點
當前提交
38bb0b61d4

+ 3 - 0
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"

+ 4 - 2
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);
     }
 

+ 0 - 1
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();
 
 }

+ 110 - 0
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;
+
+    }
+
+}

+ 74 - 7
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<MangaInfoPresenter> {
 
+    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<MangaInfoPresenter> {
     @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<MangaInfoPresenter> {
         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<MangaInfoPresenter> {
         }
     }
 
+    /**
+     * 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<MangaInfoPresenter> {
         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<MangaInfoPresenter> {
         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);
     }

+ 60 - 7
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,18 +23,43 @@ import rx.schedulers.Schedulers;
 
 public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
 
+    /**
+     * 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;
-    
-    protected Source source;
+    /**
+     * Selected manga information
+     */
     private Manga manga;
+    /**
+     * Count of chapters
+     */
     private int count = -1;
 
-    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);
@@ -39,22 +68,29 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
             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<MangaInfoFragment> {
         }
     }
 
+    /**
+     * 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<MangaInfoFragment> {
         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<MangaInfoFragment> {
         }
     }
 
+    public Manga getManga() {
+        return manga;
+    }
+
     // Used to refresh the view
-    private void refreshManga() {
+    protected void refreshManga() {
         start(GET_MANGA);
     }
 

+ 53 - 33
app/src/main/res/layout/fragment_manga_info.xml

@@ -1,10 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment">
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:app="http://schemas.android.com/apk/res-auto"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="vertical"
+                tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment">
 
 
     <!-- It seems I have to wrap everything in SwipeRefreshLayout because it always take the entire height
@@ -16,8 +17,8 @@
     <android.support.v4.widget.SwipeRefreshLayout
         android:id="@+id/swipe_refresh"
         android:layout_width="match_parent"
-        android:orientation="vertical"
-        android:layout_height="match_parent">
+        android:layout_height="match_parent"
+        android:orientation="vertical">
 
         <ScrollView
             android:layout_width="match_parent"
@@ -50,7 +51,7 @@
                             android:focusable="false"
                             android:focusableInTouchMode="false"
                             android:scaleType="fitXY"
-                            android:visibility="visible" />
+                            android:visibility="visible"/>
 
                     </RelativeLayout>
 
@@ -72,7 +73,7 @@
                             android:layout_marginTop="5dp"
                             android:focusable="false"
                             android:focusableInTouchMode="false"
-                            android:text="@string/author" />
+                            android:text="@string/author"/>
 
                         <TextView
                             android:id="@+id/manga_author"
@@ -85,7 +86,7 @@
                             android:focusable="false"
                             android:focusableInTouchMode="false"
                             android:maxLines="1"
-                            android:singleLine="true" />
+                            android:singleLine="true"/>
 
                         <TextView
                             android:id="@+id/manga_artist_label"
@@ -97,7 +98,7 @@
                             android:layout_below="@id/manga_author_label"
                             android:focusable="false"
                             android:focusableInTouchMode="false"
-                            android:text="@string/artist" />
+                            android:text="@string/artist"/>
 
                         <TextView
                             android:id="@+id/manga_artist"
@@ -110,7 +111,7 @@
                             android:focusable="false"
                             android:focusableInTouchMode="false"
                             android:maxLines="1"
-                            android:singleLine="true" />
+                            android:singleLine="true"/>
 
                         <TextView
                             android:id="@+id/manga_chapters_label"
@@ -121,7 +122,7 @@
                             android:layout_below="@id/manga_artist_label"
                             android:focusable="false"
                             android:focusableInTouchMode="false"
-                            android:text="@string/chapters" />
+                            android:text="@string/chapters"/>
 
                         <TextView
                             android:id="@+id/manga_chapters"
@@ -134,7 +135,7 @@
                             android:focusable="false"
                             android:focusableInTouchMode="false"
                             android:maxLines="1"
-                            android:singleLine="true" />
+                            android:singleLine="true"/>
 
                         <TextView
                             android:id="@+id/manga_status_label"
@@ -146,7 +147,7 @@
                             android:layout_below="@id/manga_chapters_label"
                             android:focusable="false"
                             android:focusableInTouchMode="false"
-                            android:text="@string/status" />
+                            android:text="@string/status"/>
 
                         <TextView
                             android:id="@+id/manga_status"
@@ -159,7 +160,7 @@
                             android:focusable="false"
                             android:focusableInTouchMode="false"
                             android:maxLines="1"
-                            android:singleLine="true" />
+                            android:singleLine="true"/>
 
                         <TextView
                             android:id="@+id/manga_source_label"
@@ -170,7 +171,7 @@
                             android:layout_below="@id/manga_status_label"
                             android:focusable="false"
                             android:focusableInTouchMode="false"
-                            android:text="@string/source" />
+                            android:text="@string/source"/>
 
                         <TextView
                             android:id="@+id/manga_source"
@@ -183,7 +184,7 @@
                             android:focusable="false"
                             android:focusableInTouchMode="false"
                             android:maxLines="1"
-                            android:singleLine="true" />
+                            android:singleLine="true"/>
 
                         <TextView
                             android:id="@+id/manga_genres_label"
@@ -194,7 +195,7 @@
                             android:layout_below="@id/manga_source_label"
                             android:focusable="false"
                             android:focusableInTouchMode="false"
-                            android:text="@string/genres" />
+                            android:text="@string/genres"/>
 
                         <TextView
                             android:id="@+id/manga_genres"
@@ -204,7 +205,7 @@
                             android:layout_below="@id/manga_genres_label"
                             android:focusable="false"
                             android:focusableInTouchMode="false"
-                            android:singleLine="false" />
+                            android:singleLine="false"/>
 
 
                     </RelativeLayout>
@@ -221,7 +222,7 @@
                         android:id="@+id/action_favorite"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
-                        android:text="@string/add_to_library" />
+                        android:text="@string/add_to_library"/>
                 </LinearLayout>
 
                 <LinearLayout
@@ -238,24 +239,43 @@
                         android:focusable="false"
                         android:focusableInTouchMode="false"
                         android:singleLine="false"
-                        android:text="@string/description" />
+                        android:text="@string/description"/>
 
 
-                        <TextView
-                            android:id="@+id/manga_summary"
-                            style="@style/manga_detail_text"
-                            android:layout_width="match_parent"
-                            android:layout_height="wrap_content"
-                            android:focusable="false"
-                            android:focusableInTouchMode="false"
-                            android:singleLine="false" />
-                    
+                    <TextView
+                        android:id="@+id/manga_summary"
+                        style="@style/manga_detail_text"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:focusable="false"
+                        android:focusableInTouchMode="false"
+                        android:singleLine="false"/>
+
                 </LinearLayout>
 
+
             </LinearLayout>
 
         </ScrollView>
 
     </android.support.v4.widget.SwipeRefreshLayout>
 
-</LinearLayout>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentRight="true"
+        android:layout_margin="10dp"
+        android:gravity="bottom">
+
+        <android.support.design.widget.FloatingActionButton
+            android:id="@+id/fab_edit"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="bottom|right"
+            android:layout_margin="@dimen/fab_margin"
+            app:backgroundTint="@color/colorPrimary"
+            app:layout_behavior="eu.kanade.tachiyomi.ui.base.fab.ScrollAwareFABBehavior"/>
+
+    </LinearLayout>
+</RelativeLayout>

+ 1 - 0
app/src/main/res/values/colors.xml

@@ -11,6 +11,7 @@
     <color name="primary">@color/colorPrimary</color>
     <color name="primary_dark">@color/colorPrimaryDark</color>
     <color name="primary_light">@color/colorPrimaryLight</color>
+    <color name="color_ripple">#E9F1FF</color>
 
     <color name="divider">@color/md_light_dividers</color>
 

+ 4 - 0
app/src/main/res/values/strings.xml

@@ -205,5 +205,9 @@
     <string name="notification_no_new_chapters">No new chapters found</string>
     <string name="notification_new_chapters">New chapters found for:</string>
     <string name="notification_manga_update_failed">Failed to update manga:</string>
+    <string name="notification_first_add_to_library">Please add the manga to your library before doing this</string>
+
+    <!-- File Picker Titles -->
+    <string name="file_select_cover">Select cover image</string>
 
 </resources>