Merge pull request #91 from NoodleMage/change_cover

Can now manually set cover pictures. #79
This commit is contained in:
inorichi 2016-01-30 00:12:54 +01:00
commit 38bb0b61d4
9 changed files with 311 additions and 52 deletions

View File

@ -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"

View File

@ -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);
}

View File

@ -59,7 +59,6 @@ public interface AppComponent {
void inject(LibraryUpdateService libraryUpdateService);
void inject(DownloadService downloadService);
void inject(UpdateMangaSyncService updateMangaSyncService);
Application application();
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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<MangaInfoFragment> {
@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<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);
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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"
@ -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,7 +239,7 @@
android:focusable="false"
android:focusableInTouchMode="false"
android:singleLine="false"
android:text="@string/description" />
android:text="@string/description"/>
<TextView
@ -248,14 +249,33 @@
android:layout_height="wrap_content"
android:focusable="false"
android:focusableInTouchMode="false"
android:singleLine="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>

View File

@ -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>

View File

@ -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>