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
This commit is contained in:
NoodleMage 2016-01-28 19:54:04 +01:00
parent c03495be94
commit 8b52fea602
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,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>

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>