Explorar o código

Initial MAL support

inorichi %!s(int64=9) %!d(string=hai) anos
pai
achega
ee7d76e775
Modificáronse 33 ficheiros con 1037 adicións e 168 borrados
  1. 2 4
      app/build.gradle
  2. 16 0
      app/src/main/java/eu/kanade/mangafeed/data/chaptersync/BaseChapterSync.java
  3. 29 0
      app/src/main/java/eu/kanade/mangafeed/data/chaptersync/ChapterSyncManager.java
  4. 163 0
      app/src/main/java/eu/kanade/mangafeed/data/chaptersync/MyAnimeList.java
  5. 38 0
      app/src/main/java/eu/kanade/mangafeed/data/database/DatabaseHelper.java
  6. 4 2
      app/src/main/java/eu/kanade/mangafeed/data/database/DbOpenHelper.java
  7. 35 0
      app/src/main/java/eu/kanade/mangafeed/data/database/models/ChapterSync.java
  8. 35 0
      app/src/main/java/eu/kanade/mangafeed/data/database/tables/ChapterSyncTable.java
  9. 18 0
      app/src/main/java/eu/kanade/mangafeed/data/preference/PreferencesHelper.java
  10. 7 0
      app/src/main/java/eu/kanade/mangafeed/injection/component/AppComponent.java
  11. 7 0
      app/src/main/java/eu/kanade/mangafeed/injection/module/DataModule.java
  12. 24 7
      app/src/main/java/eu/kanade/mangafeed/ui/manga/MangaActivity.java
  13. 3 23
      app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoFragment.java
  14. 99 0
      app/src/main/java/eu/kanade/mangafeed/ui/manga/myanimelist/MyAnimeListDialogFragment.java
  15. 85 0
      app/src/main/java/eu/kanade/mangafeed/ui/manga/myanimelist/MyAnimeListFragment.java
  16. 105 0
      app/src/main/java/eu/kanade/mangafeed/ui/manga/myanimelist/MyAnimeListPresenter.java
  17. 0 124
      app/src/main/java/eu/kanade/mangafeed/ui/setting/LoginDialogPreference.java
  18. 26 3
      app/src/main/java/eu/kanade/mangafeed/ui/setting/SettingsAccountsFragment.java
  19. 74 0
      app/src/main/java/eu/kanade/mangafeed/ui/setting/dialog/ChapterSyncLoginDialog.java
  20. 78 0
      app/src/main/java/eu/kanade/mangafeed/ui/setting/dialog/LoginDialogPreference.java
  21. 74 0
      app/src/main/java/eu/kanade/mangafeed/ui/setting/dialog/SourceLoginDialog.java
  22. BIN=BIN
      app/src/main/res/drawable-hdpi/ic_create.png
  23. BIN=BIN
      app/src/main/res/drawable-ldpi/ic_create.png
  24. BIN=BIN
      app/src/main/res/drawable-mdpi/ic_create.png
  25. BIN=BIN
      app/src/main/res/drawable-xhdpi/ic_create.png
  26. BIN=BIN
      app/src/main/res/drawable-xxhdpi/ic_create.png
  27. BIN=BIN
      app/src/main/res/drawable-xxxhdpi/ic_create.png
  28. 36 0
      app/src/main/res/layout/dialog_myanimelist_search.xml
  29. 13 0
      app/src/main/res/layout/dialog_myanimelist_search_item.xml
  30. 49 0
      app/src/main/res/layout/fragment_myanimelist.xml
  31. 10 0
      app/src/main/res/menu/myanimelist.xml
  32. 7 0
      app/src/main/res/values/strings.xml
  33. 0 5
      build.gradle

+ 2 - 4
app/build.gradle

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

+ 16 - 0
app/src/main/java/eu/kanade/mangafeed/data/chaptersync/BaseChapterSync.java

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

+ 29 - 0
app/src/main/java/eu/kanade/mangafeed/data/chaptersync/ChapterSyncManager.java

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

+ 163 - 0
app/src/main/java/eu/kanade/mangafeed/data/chaptersync/MyAnimeList.java

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

+ 38 - 0
app/src/main/java/eu/kanade/mangafeed/data/database/DatabaseHelper.java

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

+ 4 - 2
app/src/main/java/eu/kanade/mangafeed/data/database/DbOpenHelper.java

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

+ 35 - 0
app/src/main/java/eu/kanade/mangafeed/data/database/models/ChapterSync.java

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

+ 35 - 0
app/src/main/java/eu/kanade/mangafeed/data/database/tables/ChapterSyncTable.java

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

+ 18 - 0
app/src/main/java/eu/kanade/mangafeed/data/preference/PreferencesHelper.java

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

+ 7 - 0
app/src/main/java/eu/kanade/mangafeed/injection/component/AppComponent.java

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

+ 7 - 0
app/src/main/java/eu/kanade/mangafeed/injection/module/DataModule.java

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

+ 24 - 7
app/src/main/java/eu/kanade/mangafeed/ui/manga/MangaActivity.java

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

+ 3 - 23
app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoFragment.java

@@ -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) {
-                getPresenter().toggleFavorite();
-                return true;
-            }
 
-            return false;
+        favoriteBtn.setOnClickListener(v -> {
+            getPresenter().toggleFavorite();
         });
-        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);
+        return view;
     }
 
     public void setMangaInfo(Manga manga) {

+ 99 - 0
app/src/main/java/eu/kanade/mangafeed/ui/manga/myanimelist/MyAnimeListDialogFragment.java

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

+ 85 - 0
app/src/main/java/eu/kanade/mangafeed/ui/manga/myanimelist/MyAnimeListFragment.java

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

+ 105 - 0
app/src/main/java/eu/kanade/mangafeed/ui/manga/myanimelist/MyAnimeListPresenter.java

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

+ 0 - 124
app/src/main/java/eu/kanade/mangafeed/ui/setting/LoginDialogPreference.java

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

+ 26 - 3
app/src/main/java/eu/kanade/mangafeed/ui/setting/SettingsAccountsFragment.java

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

+ 74 - 0
app/src/main/java/eu/kanade/mangafeed/ui/setting/dialog/ChapterSyncLoginDialog.java

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

+ 78 - 0
app/src/main/java/eu/kanade/mangafeed/ui/setting/dialog/LoginDialogPreference.java

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

+ 74 - 0
app/src/main/java/eu/kanade/mangafeed/ui/setting/dialog/SourceLoginDialog.java

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

BIN=BIN
app/src/main/res/drawable-hdpi/ic_create.png


BIN=BIN
app/src/main/res/drawable-ldpi/ic_create.png


BIN=BIN
app/src/main/res/drawable-mdpi/ic_create.png


BIN=BIN
app/src/main/res/drawable-xhdpi/ic_create.png


BIN=BIN
app/src/main/res/drawable-xxhdpi/ic_create.png


BIN=BIN
app/src/main/res/drawable-xxxhdpi/ic_create.png


+ 36 - 0
app/src/main/res/layout/dialog_myanimelist_search.xml

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

+ 13 - 0
app/src/main/res/layout/dialog_myanimelist_search_item.xml

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

+ 49 - 0
app/src/main/res/layout/fragment_myanimelist.xml

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

+ 10 - 0
app/src/main/res/menu/myanimelist.xml

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

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

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

+ 0 - 5
build.gradle

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