Initial MAL support

This commit is contained in:
inorichi 2015-11-25 16:08:24 +01:00
parent da7d5886da
commit ee7d76e775
33 changed files with 1037 additions and 168 deletions

View File

@ -1,6 +1,4 @@
apply plugin: 'com.android.application'
// This does not break the build when Android Studio is missing the JRebel for Android plugin.
apply plugin: 'com.zeroturnaround.jrebel.android'
apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'me.tatarka.retrolambda'
@ -65,8 +63,8 @@ dependencies {
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
compile 'com.squareup.okhttp:okhttp-urlconnection:2.5.0'
compile 'com.squareup.okhttp:okhttp:2.5.0'
compile 'com.squareup.okhttp:okhttp-urlconnection:2.6.0'
compile 'com.squareup.okhttp:okhttp:2.6.0'
compile 'com.squareup.okio:okio:1.6.0'
compile 'com.google.code.gson:gson:2.4'
compile 'com.jakewharton:disklrucache:2.0.2'

View File

@ -0,0 +1,16 @@
package eu.kanade.mangafeed.data.chaptersync;
import rx.Observable;
public abstract class BaseChapterSync {
// Name of the chapter sync service to display
public abstract String getName();
// Id of the sync service (must be declared and obtained from ChapterSyncManager to avoid conflicts)
public abstract int getId();
public abstract Observable<Boolean> login(String username, String password);
public abstract boolean isLogged();
}

View File

@ -0,0 +1,29 @@
package eu.kanade.mangafeed.data.chaptersync;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
public class ChapterSyncManager {
private List<BaseChapterSync> services;
private MyAnimeList myAnimeList;
public static final int MYANIMELIST = 1;
public ChapterSyncManager(Context context) {
services = new ArrayList<>();
myAnimeList = new MyAnimeList(context);
services.add(myAnimeList);
}
public MyAnimeList getMyAnimeList() {
return myAnimeList;
}
public List<BaseChapterSync> getChapterSyncServices() {
return services;
}
}

View File

@ -0,0 +1,163 @@
package eu.kanade.mangafeed.data.chaptersync;
import android.content.Context;
import android.net.Uri;
import android.util.Xml;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Response;
import org.jsoup.Jsoup;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import javax.inject.Inject;
import eu.kanade.mangafeed.App;
import eu.kanade.mangafeed.data.database.models.ChapterSync;
import eu.kanade.mangafeed.data.network.NetworkHelper;
import eu.kanade.mangafeed.data.preference.PreferencesHelper;
import rx.Observable;
public class MyAnimeList extends BaseChapterSync {
@Inject PreferencesHelper preferences;
@Inject NetworkHelper networkService;
private Headers headers;
public static final String BASE_URL = "http://myanimelist.net";
private static final String ENTRY = "entry";
private static final String CHAPTER = "chapter";
public MyAnimeList(Context context) {
App.get(context).getComponent().inject(this);
String username = preferences.getChapterSyncUsername(this);
String password = preferences.getChapterSyncPassword(this);
if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password);
}
}
@Override
public String getName() {
return "MyAnimeList";
}
@Override
public int getId() {
return ChapterSyncManager.MYANIMELIST;
}
public String getLoginUrl() {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/account/verify_credentials.xml")
.toString();
}
public Observable<Boolean> login(String username, String password) {
createHeaders(username, password);
return networkService.getResponse(getLoginUrl(), headers, null)
.map(response -> response.code() == 200);
}
@Override
public boolean isLogged() {
return !preferences.getChapterSyncUsername(this).isEmpty()
&& !preferences.getChapterSyncPassword(this).isEmpty();
}
public String getSearchUrl(String query) {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/manga/search.xml")
.appendQueryParameter("q", query)
.toString();
}
public Observable<List<ChapterSync>> search(String query) {
return networkService.getStringResponse(getSearchUrl(query), headers, null)
.map(Jsoup::parse)
.flatMap(doc -> Observable.from(doc.select("entry")))
.map(entry -> {
ChapterSync chapter = ChapterSync.create(this);
chapter.title = entry.select("title").first().text();
chapter.remote_id = Long.parseLong(entry.select("id").first().text());
return chapter;
})
.toList();
}
public String getListUrl(String username) {
return Uri.parse(BASE_URL).buildUpon()
.appendPath("malappinfo.php")
.appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga")
.toString();
}
public Observable<List<ChapterSync>> getList(String username) {
return networkService.getStringResponse(getListUrl(username), headers, null)
.map(Jsoup::parse)
.flatMap(doc -> Observable.from(doc.select("manga")))
.map(entry -> {
ChapterSync chapter = ChapterSync.create(this);
chapter.title = entry.select("series_title").first().text();
chapter.remote_id = Long.parseLong(
entry.select("series_mangadb_id").first().text());
chapter.last_chapter_read = Integer.parseInt(
entry.select("my_read_chapters").first().text());
return chapter;
})
.toList();
}
public String getUpdateUrl(ChapterSync chapter) {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath(chapter.remote_id + ".xml")
.toString();
}
public Observable<Response> update(ChapterSync chapter) {
XmlSerializer xml = Xml.newSerializer();
StringWriter writer = new StringWriter();
try {
xml.setOutput(writer);
xml.startDocument("UTF-8", false);
xml.startTag("", ENTRY);
xml.startTag("", CHAPTER);
xml.text(chapter.last_chapter_read + "");
xml.endTag("", CHAPTER);
xml.endTag("", ENTRY);
xml.endDocument();
} catch (IOException e) {
return Observable.error(e);
}
FormEncodingBuilder form = new FormEncodingBuilder();
form.add("data", writer.toString());
return networkService.postData(getUpdateUrl(chapter), form.build(), headers);
}
public void createHeaders(String username, String password) {
Headers.Builder builder = new Headers.Builder();
builder.add("Authorization", Credentials.basic(username, password));
// builder.add("User-Agent", "");
setHeaders(builder.build());
}
public void setHeaders(Headers headers) {
this.headers = headers;
}
}

View File

@ -16,14 +16,20 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery;
import java.util.List;
import eu.kanade.mangafeed.data.chaptersync.BaseChapterSync;
import eu.kanade.mangafeed.data.database.models.Chapter;
import eu.kanade.mangafeed.data.database.models.ChapterStorIOSQLiteDeleteResolver;
import eu.kanade.mangafeed.data.database.models.ChapterStorIOSQLiteGetResolver;
import eu.kanade.mangafeed.data.database.models.ChapterStorIOSQLitePutResolver;
import eu.kanade.mangafeed.data.database.models.ChapterSync;
import eu.kanade.mangafeed.data.database.models.ChapterSyncStorIOSQLiteDeleteResolver;
import eu.kanade.mangafeed.data.database.models.ChapterSyncStorIOSQLiteGetResolver;
import eu.kanade.mangafeed.data.database.models.ChapterSyncStorIOSQLitePutResolver;
import eu.kanade.mangafeed.data.database.models.Manga;
import eu.kanade.mangafeed.data.database.models.MangaStorIOSQLiteDeleteResolver;
import eu.kanade.mangafeed.data.database.models.MangaStorIOSQLitePutResolver;
import eu.kanade.mangafeed.data.database.resolvers.MangaWithUnreadGetResolver;
import eu.kanade.mangafeed.data.database.tables.ChapterSyncTable;
import eu.kanade.mangafeed.data.database.tables.ChapterTable;
import eu.kanade.mangafeed.data.database.tables.MangaTable;
import eu.kanade.mangafeed.util.ChapterRecognition;
@ -48,6 +54,11 @@ public class DatabaseHelper {
.getResolver(new ChapterStorIOSQLiteGetResolver())
.deleteResolver(new ChapterStorIOSQLiteDeleteResolver())
.build())
.addTypeMapping(ChapterSync.class, SQLiteTypeMapping.<ChapterSync>builder()
.putResolver(new ChapterSyncStorIOSQLitePutResolver())
.getResolver(new ChapterSyncStorIOSQLiteGetResolver())
.deleteResolver(new ChapterSyncStorIOSQLiteDeleteResolver())
.build())
.build();
}
@ -263,4 +274,31 @@ public class DatabaseHelper {
.objects(chapters)
.prepare();
}
// Chapter sync related queries
public PreparedGetListOfObjects<ChapterSync> getChapterSync(Manga manga, BaseChapterSync sync) {
return db.get()
.listOfObjects(ChapterSync.class)
.withQuery(Query.builder()
.table(ChapterSyncTable.TABLE)
.where(ChapterSyncTable.COLUMN_MANGA_ID + "=? AND " +
ChapterSyncTable.COLUMN_SYNC_ID + "=?")
.whereArgs(manga.id, sync.getId())
.build())
.prepare();
}
public PreparedPutObject<ChapterSync> insertChapterSync(ChapterSync chapter) {
return db.put()
.object(chapter)
.prepare();
}
public PreparedDeleteObject<ChapterSync> deleteChapterSync(ChapterSync chapter) {
return db.delete()
.object(chapter)
.prepare();
}
}

View File

@ -5,13 +5,14 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import eu.kanade.mangafeed.data.database.tables.ChapterSyncTable;
import eu.kanade.mangafeed.data.database.tables.ChapterTable;
import eu.kanade.mangafeed.data.database.tables.MangaTable;
public class DbOpenHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "mangafeed.db";
public static final int DATABASE_VERSION = 1;
public static final int DATABASE_VERSION = 2;
public DbOpenHelper(@NonNull Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
@ -25,7 +26,8 @@ public class DbOpenHelper extends SQLiteOpenHelper {
@Override
public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) {
// no impl
if (oldVersion == 1)
db.execSQL(ChapterSyncTable.getCreateTableQuery());
}
@Override

View File

@ -0,0 +1,35 @@
package eu.kanade.mangafeed.data.database.models;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import eu.kanade.mangafeed.data.chaptersync.BaseChapterSync;
import eu.kanade.mangafeed.data.database.tables.ChapterSyncTable;
@StorIOSQLiteType(table = ChapterSyncTable.TABLE)
public class ChapterSync {
@StorIOSQLiteColumn(name = ChapterSyncTable.COLUMN_ID, key = true)
public long id;
@StorIOSQLiteColumn(name = ChapterSyncTable.COLUMN_MANGA_ID)
public long manga_id;
@StorIOSQLiteColumn(name = ChapterSyncTable.COLUMN_SYNC_ID)
public long sync_id;
@StorIOSQLiteColumn(name = ChapterSyncTable.COLUMN_REMOTE_ID)
public long remote_id;
@StorIOSQLiteColumn(name = ChapterSyncTable.COLUMN_TITLE)
public String title;
@StorIOSQLiteColumn(name = ChapterSyncTable.COLUMN_LAST_CHAPTER_READ)
public int last_chapter_read;
public static ChapterSync create(BaseChapterSync sync) {
ChapterSync chapter = new ChapterSync();
chapter.sync_id = sync.getId();
return chapter;
}
}

View File

@ -0,0 +1,35 @@
package eu.kanade.mangafeed.data.database.tables;
import android.support.annotation.NonNull;
public class ChapterSyncTable {
public static final String TABLE = "chapter_sync";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_MANGA_ID = "manga_id";
public static final String COLUMN_SYNC_ID = "sync_id";
public static final String COLUMN_REMOTE_ID = "remote_id";
public static final String COLUMN_TITLE = "title";
public static final String COLUMN_LAST_CHAPTER_READ = "last_chapter_read";
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
+ COLUMN_SYNC_ID + " INTEGER NOT NULL, "
+ COLUMN_REMOTE_ID + " INTEGER NOT NULL, "
+ COLUMN_TITLE + " TEXT NOT NULL, "
+ COLUMN_LAST_CHAPTER_READ + " INTEGER NOT NULL, "
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
+ "ON DELETE CASCADE"
+ ");";
}
}

View File

@ -8,6 +8,7 @@ import com.f2prateek.rx.preferences.Preference;
import com.f2prateek.rx.preferences.RxSharedPreferences;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.chaptersync.BaseChapterSync;
import eu.kanade.mangafeed.data.source.base.Source;
import eu.kanade.mangafeed.util.DiskUtils;
import rx.Observable;
@ -20,6 +21,8 @@ public class PreferencesHelper {
private static final String SOURCE_ACCOUNT_USERNAME = "pref_source_username_";
private static final String SOURCE_ACCOUNT_PASSWORD = "pref_source_password_";
private static final String CHAPTERSYNC_ACCOUNT_USERNAME = "pref_chaptersync_username_";
private static final String CHAPTERSYNC_ACCOUNT_PASSWORD = "pref_chaptersync_password_";
public PreferencesHelper(Context context) {
this.context = context;
@ -84,6 +87,21 @@ public class PreferencesHelper {
.apply();
}
public String getChapterSyncUsername(BaseChapterSync sync) {
return prefs.getString(CHAPTERSYNC_ACCOUNT_USERNAME + sync.getId(), "");
}
public String getChapterSyncPassword(BaseChapterSync sync) {
return prefs.getString(CHAPTERSYNC_ACCOUNT_PASSWORD + sync.getId(), "");
}
public void setChapterSyncCredentials(BaseChapterSync sync, String username, String password) {
prefs.edit()
.putString(CHAPTERSYNC_ACCOUNT_USERNAME + sync.getId(), username)
.putString(CHAPTERSYNC_ACCOUNT_PASSWORD + sync.getId(), password)
.apply();
}
public String getDownloadsDirectory() {
return prefs.getString(getKey(R.string.pref_download_directory_key),
DiskUtils.getStorageDirectories(context)[0]);

View File

@ -5,6 +5,7 @@ import android.app.Application;
import javax.inject.Singleton;
import dagger.Component;
import eu.kanade.mangafeed.data.chaptersync.MyAnimeList;
import eu.kanade.mangafeed.data.download.DownloadService;
import eu.kanade.mangafeed.data.sync.LibraryUpdateService;
import eu.kanade.mangafeed.injection.module.AppModule;
@ -12,9 +13,11 @@ import eu.kanade.mangafeed.injection.module.DataModule;
import eu.kanade.mangafeed.ui.catalogue.CataloguePresenter;
import eu.kanade.mangafeed.ui.download.DownloadPresenter;
import eu.kanade.mangafeed.ui.library.LibraryPresenter;
import eu.kanade.mangafeed.ui.manga.MangaActivity;
import eu.kanade.mangafeed.ui.manga.MangaPresenter;
import eu.kanade.mangafeed.ui.manga.chapter.ChaptersPresenter;
import eu.kanade.mangafeed.ui.manga.info.MangaInfoPresenter;
import eu.kanade.mangafeed.ui.manga.myanimelist.MyAnimeListPresenter;
import eu.kanade.mangafeed.ui.reader.ReaderPresenter;
import eu.kanade.mangafeed.ui.catalogue.SourcePresenter;
import eu.kanade.mangafeed.data.source.base.Source;
@ -39,13 +42,17 @@ public interface AppComponent {
void inject(ChaptersPresenter chaptersPresenter);
void inject(ReaderPresenter readerPresenter);
void inject(DownloadPresenter downloadPresenter);
void inject(MyAnimeListPresenter myAnimeListPresenter);
void inject(ReaderActivity readerActivity);
void inject(MangaActivity mangaActivity);
void inject(SettingsAccountsFragment settingsAccountsFragment);
void inject(SettingsDownloadsFragment settingsDownloadsFragment);
void inject(Source source);
void inject(MyAnimeList myAnimeList);
void inject(LibraryUpdateService libraryUpdateService);
void inject(DownloadService downloadService);

View File

@ -7,6 +7,7 @@ import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import eu.kanade.mangafeed.data.cache.CacheManager;
import eu.kanade.mangafeed.data.chaptersync.ChapterSyncManager;
import eu.kanade.mangafeed.data.database.DatabaseHelper;
import eu.kanade.mangafeed.data.download.DownloadManager;
import eu.kanade.mangafeed.data.network.NetworkHelper;
@ -56,4 +57,10 @@ public class DataModule {
return new DownloadManager(app, sourceManager, preferences);
}
@Provides
@Singleton
ChapterSyncManager provideChapterSyncManager(Application app) {
return new ChapterSyncManager(app);
}
}

View File

@ -11,13 +11,19 @@ import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.Toolbar;
import javax.inject.Inject;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.mangafeed.App;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.chaptersync.ChapterSyncManager;
import eu.kanade.mangafeed.data.database.models.Manga;
import eu.kanade.mangafeed.data.preference.PreferencesHelper;
import eu.kanade.mangafeed.ui.base.activity.BaseRxActivity;
import eu.kanade.mangafeed.ui.manga.chapter.ChaptersFragment;
import eu.kanade.mangafeed.ui.manga.info.MangaInfoFragment;
import eu.kanade.mangafeed.ui.manga.myanimelist.MyAnimeListFragment;
import nucleus.factory.RequiresPresenter;
@RequiresPresenter(MangaPresenter.class)
@ -27,6 +33,9 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
@Bind(R.id.tabs) TabLayout tabs;
@Bind(R.id.view_pager) ViewPager view_pager;
@Inject PreferencesHelper preferences;
@Inject ChapterSyncManager chapterSyncManager;
private MangaDetailAdapter adapter;
private long manga_id;
private boolean is_online;
@ -43,6 +52,7 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
App.get(this).getComponent().inject(this);
setContentView(R.layout.activity_manga_detail);
ButterKnife.bind(this);
@ -88,25 +98,31 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
class MangaDetailAdapter extends FragmentPagerAdapter {
final int PAGE_COUNT = 2;
private String tab_titles[];
private int pageCount;
private String tabTitles[];
private Context context;
final static int INFO_FRAGMENT = 0;
final static int CHAPTERS_FRAGMENT = 1;
final static int MYANIMELIST_FRAGMENT = 2;
public MangaDetailAdapter(FragmentManager fm, Context context) {
super(fm);
this.context = context;
tab_titles = new String[]{
tabTitles = new String[]{
context.getString(R.string.manga_detail_tab),
context.getString(R.string.manga_chapters_tab)
context.getString(R.string.manga_chapters_tab),
"MAL"
};
pageCount = 2;
if (chapterSyncManager.getMyAnimeList().isLogged())
pageCount++;
}
@Override
public int getCount() {
return PAGE_COUNT;
return pageCount;
}
@Override
@ -116,7 +132,8 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
return MangaInfoFragment.newInstance();
case CHAPTERS_FRAGMENT:
return ChaptersFragment.newInstance();
case MYANIMELIST_FRAGMENT:
return MyAnimeListFragment.newInstance();
default:
return null;
}
@ -125,7 +142,7 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
@Override
public CharSequence getPageTitle(int position) {
// Generate title based on item position
return tab_titles[position];
return tabTitles[position];
}
}

View File

@ -2,10 +2,6 @@ package eu.kanade.mangafeed.ui.manga.info;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
@ -52,28 +48,12 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_manga_info, container, false);
ButterKnife.bind(this, view);
favoriteBtn.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
favoriteBtn.setOnClickListener(v -> {
getPresenter().toggleFavorite();
return true;
}
return false;
});
getPresenter().initFavoriteText();
return view;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}
public void setMangaInfo(Manga manga) {

View File

@ -0,0 +1,99 @@
package eu.kanade.mangafeed.ui.manga.myanimelist;
import android.app.Dialog;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.database.models.ChapterSync;
import uk.co.ribot.easyadapter.EasyAdapter;
import uk.co.ribot.easyadapter.ItemViewHolder;
import uk.co.ribot.easyadapter.PositionInfo;
import uk.co.ribot.easyadapter.annotations.LayoutId;
import uk.co.ribot.easyadapter.annotations.ViewId;
public class MyAnimeListDialogFragment extends DialogFragment {
@Bind(R.id.myanimelist_search_field) EditText searchText;
@Bind(R.id.myanimelist_search_button) Button searchButton;
@Bind(R.id.myanimelist_search_results) ListView searchResults;
private EasyAdapter<ChapterSync> adapter;
private MyAnimeListFragment fragment;
private ChapterSync selectedItem;
public static MyAnimeListDialogFragment newInstance(MyAnimeListFragment parentFragment) {
MyAnimeListDialogFragment dialog = new MyAnimeListDialogFragment();
dialog.setParentFragment(parentFragment);
return dialog;
}
@Override
public Dialog onCreateDialog(Bundle savedState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
LayoutInflater inflater = getActivity().getLayoutInflater();
View view = inflater.inflate(R.layout.dialog_myanimelist_search, null);
ButterKnife.bind(this, view);
builder.setView(view)
.setPositiveButton(R.string.button_ok, (dialog, which) -> onPositiveButtonClick())
.setNegativeButton(R.string.button_cancel, (dialog, which) -> {});
searchButton.setOnClickListener(v ->
fragment.getPresenter().searchManga(searchText.getText().toString()));
searchResults.setOnItemClickListener((parent, viewList, position, id) ->
selectedItem = adapter.getItem(position));
adapter = new EasyAdapter<>(getActivity(), ResultViewHolder.class);
searchResults.setAdapter(adapter);
return builder.create();
}
private void onPositiveButtonClick() {
if (adapter != null && selectedItem != null) {
fragment.getPresenter().registerManga(selectedItem);
}
}
public void setResults(List<ChapterSync> results) {
selectedItem = null;
adapter.setItems(results);
}
public void setParentFragment(MyAnimeListFragment fragment) {
this.fragment = fragment;
}
@LayoutId(R.layout.dialog_myanimelist_search_item)
public static class ResultViewHolder extends ItemViewHolder<ChapterSync> {
@ViewId(R.id.myanimelist_result_title) TextView title;
public ResultViewHolder(View view) {
super(view);
}
@Override
public void onSetValues(ChapterSync chapter, PositionInfo positionInfo) {
title.setText(chapter.title);
}
}
}

View File

@ -0,0 +1,85 @@
package eu.kanade.mangafeed.ui.manga.myanimelist;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.database.models.ChapterSync;
import eu.kanade.mangafeed.ui.base.fragment.BaseRxFragment;
import nucleus.factory.RequiresPresenter;
@RequiresPresenter(MyAnimeListPresenter.class)
public class MyAnimeListFragment extends BaseRxFragment<MyAnimeListPresenter> {
@Bind(R.id.myanimelist_title) TextView title;
@Bind(R.id.myanimelist_last_chapter_read) EditText lastChapterRead;
@Bind(R.id.update_button) Button updateButton;
private MyAnimeListDialogFragment dialog;
public static MyAnimeListFragment newInstance() {
return new MyAnimeListFragment();
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_myanimelist, container, false);
ButterKnife.bind(this, view);
updateButton.setOnClickListener(v -> getPresenter().updateLastChapter(
Integer.parseInt(lastChapterRead.getText().toString())));
return view;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.myanimelist, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.myanimelist_edit:
showSearchDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
public void setChapterSync(ChapterSync chapterSync) {
title.setText(chapterSync.title);
lastChapterRead.setText(chapterSync.last_chapter_read + "");
}
private void showSearchDialog() {
if (dialog == null)
dialog = MyAnimeListDialogFragment.newInstance(this);
dialog.show(getActivity().getSupportFragmentManager(), "search");
}
public void onSearchResults(List<ChapterSync> results) {
if (dialog != null)
dialog.setResults(results);
}
}

View File

@ -0,0 +1,105 @@
package eu.kanade.mangafeed.ui.manga.myanimelist;
import android.os.Bundle;
import javax.inject.Inject;
import eu.kanade.mangafeed.data.chaptersync.ChapterSyncManager;
import eu.kanade.mangafeed.data.chaptersync.MyAnimeList;
import eu.kanade.mangafeed.data.database.DatabaseHelper;
import eu.kanade.mangafeed.data.database.models.ChapterSync;
import eu.kanade.mangafeed.data.database.models.Manga;
import eu.kanade.mangafeed.ui.base.presenter.BasePresenter;
import eu.kanade.mangafeed.util.EventBusHook;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import timber.log.Timber;
public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
@Inject DatabaseHelper db;
@Inject ChapterSyncManager syncManager;
private MyAnimeList myAnimeList;
private Manga manga;
private ChapterSync chapterSync;
private String query;
private Subscription updateSubscription;
private static final int GET_CHAPTER_SYNC = 1;
private static final int GET_SEARCH_RESULTS = 2;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
myAnimeList = syncManager.getMyAnimeList();
restartableLatestCache(GET_CHAPTER_SYNC,
() -> db.getChapterSync(manga, myAnimeList).createObservable()
.flatMap(Observable::from)
.doOnNext(chapterSync -> this.chapterSync = chapterSync)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()),
MyAnimeListFragment::setChapterSync);
restartableLatestCache(GET_SEARCH_RESULTS,
() -> myAnimeList.search(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()),
(view, results) -> {
view.onSearchResults(results);
}, (view, error) -> {
Timber.e(error.getMessage());
});
}
@Override
protected void onTakeView(MyAnimeListFragment view) {
super.onTakeView(view);
registerForStickyEvents();
}
@Override
protected void onDropView() {
unregisterForEvents();
super.onDropView();
}
@EventBusHook
public void onEventMainThread(Manga manga) {
this.manga = manga;
start(GET_CHAPTER_SYNC);
}
public void updateLastChapter(int chapterNumber) {
if (updateSubscription != null)
remove(updateSubscription);
chapterSync.last_chapter_read = chapterNumber;
add(updateSubscription = myAnimeList.update(chapterSync)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> {},
error -> {
Timber.e(error.getMessage());
}
));
}
public void searchManga(String query) {
this.query = query;
start(GET_SEARCH_RESULTS);
}
public void registerManga(ChapterSync selectedManga) {
selectedManga.manga_id = manga.id;
db.insertChapterSync(selectedManga).executeAsBlocking();
}
}

View File

@ -1,124 +0,0 @@
package eu.kanade.mangafeed.ui.setting;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.preference.DialogPreference;
import android.text.method.PasswordTransformationMethod;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import com.dd.processbutton.iml.ActionProcessButton;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.preference.PreferencesHelper;
import eu.kanade.mangafeed.data.source.base.Source;
import eu.kanade.mangafeed.util.ToastUtil;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class LoginDialogPreference extends DialogPreference {
@Bind(R.id.accounts_login) TextView title;
@Bind(R.id.username) EditText username;
@Bind(R.id.password) EditText password;
@Bind(R.id.show_password) CheckBox showPassword;
@Bind(R.id.login) ActionProcessButton loginBtn;
private PreferencesHelper preferences;
private Source source;
private AlertDialog dialog;
private Subscription requestSubscription;
private Context context;
public LoginDialogPreference(Context context, PreferencesHelper preferences, Source source) {
super(context, null);
this.context = context;
this.preferences = preferences;
this.source = source;
setDialogLayoutResource(R.layout.pref_account_login);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
// Hide positive button
builder.setPositiveButton("", this);
}
@Override
protected void onBindDialogView(View view) {
ButterKnife.bind(this, view);
title.setText(getContext().getString(R.string.accounts_login_title, source.getName()));
username.setText(preferences.getSourceUsername(source));
password.setText(preferences.getSourcePassword(source));
showPassword.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked)
password.setTransformationMethod(null);
else
password.setTransformationMethod(new PasswordTransformationMethod());
});
loginBtn.setMode(ActionProcessButton.Mode.ENDLESS);
loginBtn.setOnClickListener(click -> checkLogin());
super.onBindDialogView(view);
}
@Override
public void showDialog(Bundle state) {
super.showDialog(state);
dialog = ((AlertDialog) getDialog());
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (requestSubscription != null)
requestSubscription.unsubscribe();
if(!positiveResult)
return;
preferences.setSourceCredentials(source,
username.getText().toString(),
password.getText().toString());
}
private void checkLogin() {
if (requestSubscription != null)
requestSubscription.unsubscribe();
if (username.getText().length() == 0 || password.getText().length() == 0)
return;
loginBtn.setProgress(1);
requestSubscription = source
.login(username.getText().toString(), password.getText().toString())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(logged -> {
if (logged) {
// Simulate a positive button click and dismiss the dialog
onClick(dialog, DialogInterface.BUTTON_POSITIVE);
dialog.dismiss();
ToastUtil.showShort(context, R.string.login_success);
} else {
loginBtn.setProgress(-1);
}
}, throwable -> {
loginBtn.setProgress(-1);
loginBtn.setText(R.string.unknown_error);
});
}
}

View File

@ -1,6 +1,7 @@
package eu.kanade.mangafeed.ui.setting;
import android.os.Bundle;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
@ -10,16 +11,21 @@ import javax.inject.Inject;
import eu.kanade.mangafeed.App;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.chaptersync.BaseChapterSync;
import eu.kanade.mangafeed.data.chaptersync.ChapterSyncManager;
import eu.kanade.mangafeed.data.preference.PreferencesHelper;
import eu.kanade.mangafeed.data.source.SourceManager;
import eu.kanade.mangafeed.data.source.base.Source;
import eu.kanade.mangafeed.ui.base.activity.BaseActivity;
import eu.kanade.mangafeed.ui.setting.dialog.ChapterSyncLoginDialog;
import eu.kanade.mangafeed.ui.setting.dialog.SourceLoginDialog;
import rx.Observable;
public class SettingsAccountsFragment extends PreferenceFragment {
@Inject SourceManager sourceManager;
@Inject PreferencesHelper preferences;
@Inject SourceManager sourceManager;
@Inject ChapterSyncManager syncManager;
public static SettingsAccountsFragment newInstance() {
return new SettingsAccountsFragment();
@ -35,13 +41,30 @@ public class SettingsAccountsFragment extends PreferenceFragment {
List<Source> sourceAccounts = getSourcesWithLogin();
PreferenceCategory sourceCategory = new PreferenceCategory(screen.getContext());
sourceCategory.setTitle("Sources");
screen.addPreference(sourceCategory);
for (Source source : sourceAccounts) {
LoginDialogPreference dialog = new LoginDialogPreference(
SourceLoginDialog dialog = new SourceLoginDialog(
screen.getContext(), preferences, source);
dialog.setTitle(source.getName());
screen.addPreference(dialog);
sourceCategory.addPreference(dialog);
}
PreferenceCategory chapterSyncCategory = new PreferenceCategory(screen.getContext());
chapterSyncCategory.setTitle("Sync");
screen.addPreference(chapterSyncCategory);
for (BaseChapterSync sync : syncManager.getChapterSyncServices()) {
ChapterSyncLoginDialog dialog = new ChapterSyncLoginDialog(
screen.getContext(), preferences, sync);
dialog.setTitle(sync.getName());
chapterSyncCategory.addPreference(dialog);
}
}
@Override

View File

@ -0,0 +1,74 @@
package eu.kanade.mangafeed.ui.setting.dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.view.View;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.chaptersync.BaseChapterSync;
import eu.kanade.mangafeed.data.preference.PreferencesHelper;
import eu.kanade.mangafeed.util.ToastUtil;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class ChapterSyncLoginDialog extends LoginDialogPreference {
private BaseChapterSync sync;
public ChapterSyncLoginDialog(Context context, PreferencesHelper preferences, BaseChapterSync sync) {
super(context, preferences);
this.sync = sync;
}
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
title.setText(getContext().getString(R.string.accounts_login_title, sync.getName()));
username.setText(preferences.getChapterSyncUsername(sync));
password.setText(preferences.getChapterSyncPassword(sync));
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (positiveResult) {
preferences.setChapterSyncCredentials(sync,
username.getText().toString(),
password.getText().toString());
}
}
protected void checkLogin() {
if (requestSubscription != null)
requestSubscription.unsubscribe();
if (username.getText().length() == 0 || password.getText().length() == 0)
return;
loginBtn.setProgress(1);
requestSubscription = sync
.login(username.getText().toString(), password.getText().toString())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(logged -> {
if (logged) {
// Simulate a positive button click and dismiss the dialog
onClick(dialog, DialogInterface.BUTTON_POSITIVE);
dialog.dismiss();
ToastUtil.showShort(context, R.string.login_success);
} else {
preferences.setChapterSyncCredentials(sync, "", "");
loginBtn.setProgress(-1);
}
}, error -> {
loginBtn.setProgress(-1);
loginBtn.setText(R.string.unknown_error);
});
}
}

View File

@ -0,0 +1,78 @@
package eu.kanade.mangafeed.ui.setting.dialog;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.preference.DialogPreference;
import android.text.method.PasswordTransformationMethod;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import com.dd.processbutton.iml.ActionProcessButton;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.preference.PreferencesHelper;
import rx.Subscription;
public abstract class LoginDialogPreference extends DialogPreference {
@Bind(R.id.accounts_login) TextView title;
@Bind(R.id.username) EditText username;
@Bind(R.id.password) EditText password;
@Bind(R.id.show_password) CheckBox showPassword;
@Bind(R.id.login) ActionProcessButton loginBtn;
protected PreferencesHelper preferences;
protected AlertDialog dialog;
protected Subscription requestSubscription;
protected Context context;
public LoginDialogPreference(Context context, PreferencesHelper preferences) {
super(context, null);
this.context = context;
this.preferences = preferences;
setDialogLayoutResource(R.layout.pref_account_login);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
// Hide positive button
builder.setPositiveButton("", this);
}
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
ButterKnife.bind(this, view);
showPassword.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked)
password.setTransformationMethod(null);
else
password.setTransformationMethod(new PasswordTransformationMethod());
});
loginBtn.setMode(ActionProcessButton.Mode.ENDLESS);
loginBtn.setOnClickListener(click -> checkLogin());
}
@Override
public void showDialog(Bundle state) {
super.showDialog(state);
dialog = ((AlertDialog) getDialog());
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (requestSubscription != null)
requestSubscription.unsubscribe();
}
protected abstract void checkLogin();
}

View File

@ -0,0 +1,74 @@
package eu.kanade.mangafeed.ui.setting.dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.view.View;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.preference.PreferencesHelper;
import eu.kanade.mangafeed.data.source.base.Source;
import eu.kanade.mangafeed.util.ToastUtil;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class SourceLoginDialog extends LoginDialogPreference {
private Source source;
public SourceLoginDialog(Context context, PreferencesHelper preferences, Source source) {
super(context, preferences);
this.source = source;
}
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
title.setText(getContext().getString(R.string.accounts_login_title, source.getName()));
username.setText(preferences.getSourceUsername(source));
password.setText(preferences.getSourcePassword(source));
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (positiveResult) {
preferences.setSourceCredentials(source,
username.getText().toString(),
password.getText().toString());
}
}
protected void checkLogin() {
if (requestSubscription != null)
requestSubscription.unsubscribe();
if (username.getText().length() == 0 || password.getText().length() == 0)
return;
loginBtn.setProgress(1);
requestSubscription = source
.login(username.getText().toString(), password.getText().toString())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(logged -> {
if (logged) {
// Simulate a positive button click and dismiss the dialog
onClick(dialog, DialogInterface.BUTTON_POSITIVE);
dialog.dismiss();
ToastUtil.showShort(context, R.string.login_success);
} else {
preferences.setSourceCredentials(source, "", "");
loginBtn.setProgress(-1);
}
}, error -> {
loginBtn.setProgress(-1);
loginBtn.setText(R.string.unknown_error);
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/myanimelist_search_field"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_search"
android:id="@+id/myanimelist_search_button"/>
</LinearLayout>
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/myanimelist_search_results"
android:choiceMode="singleChoice"
android:listSelector="@color/list_choice_pressed_bg_light">
</ListView>
</LinearLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/myanimelist_result_title"
android:padding="10dp"/>
</LinearLayout>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MyAnimeList title:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/myanimelist_title" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Last chapter read" />
<EditText
android:layout_width="56dp"
android:layout_height="wrap_content"
android:inputType="number"
android:ems="10"
android:id="@+id/myanimelist_last_chapter_read"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_update"
android:id="@+id/update_button"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/action_edit"
android:id="@+id/myanimelist_edit"
android:icon="@drawable/ic_create"
app:showAsAction="ifRoom" />
</menu>

View File

@ -17,10 +17,17 @@
<string name="action_mark_as_unread">Mark as unread</string>
<string name="action_download">Download</string>
<string name="action_delete">Delete</string>
<string name="action_update">Update</string>
<string name="action_edit">Edit</string>
<string name="action_sort_up">Sort up</string>
<string name="action_sort_down">Sort down</string>
<string name="action_show_unread">Show unread</string>
<!-- Buttons -->
<string name="button_ok">Ok</string>
<string name="button_cancel">Cancel</string>
<!-- Preferences -->
<!-- Subsections -->
<string name="pref_category_reader">Reader</string>

View File

@ -4,14 +4,9 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript {
repositories {
jcenter()
maven {
url 'https://repos.zeroturnaround.com/nexus/content/repositories/zt-public-releases'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.0'
// This does not break the build when Android Studio is missing the JRebel for Android plugin.
classpath 'com.zeroturnaround.jrebel.android:jr-android-gradle:0.9.+'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.7'
classpath 'me.tatarka:gradle-retrolambda:3.2.3'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.11.3'