From 3c43bebe64ac8914f7489b9ffc1455cc5f2434bf Mon Sep 17 00:00:00 2001 From: NerdNumber9 Date: Fri, 5 Aug 2016 19:42:36 -0400 Subject: [PATCH] Integrate TachiyomiEH changes. --- .gitignore | 5 +- CHANGELOG.md | 9 + app/build.gradle | 16 +- app/src/main/AndroidManifest.xml | 49 +- .../data/mangasync/MangaSyncManager.kt | 8 +- .../tachiyomi/data/network/NetworkHelper.kt | 4 +- .../kanade/tachiyomi/data/network/Requests.kt | 2 +- .../data/preference/PreferencesHelper.kt | 4 +- .../kanade/tachiyomi/data/source/Language.kt | 6 +- .../tachiyomi/data/source/SourceManager.kt | 36 +- .../data/source/online/OnlineSource.kt | 2 +- .../data/source/online/english/EHentai.java | 739 ++++++++++++++++++ .../ui/catalogue/CatalogueFragment.kt | 18 +- .../ui/catalogue/CataloguePresenter.kt | 4 +- .../tachiyomi/ui/library/LibraryFragment.kt | 16 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 12 +- .../tachiyomi/ui/manga/MangaActivity.kt | 5 - .../ui/setting/SettingsAboutFragment.kt | 14 +- .../tachiyomi/ui/setting/SettingsActivity.kt | 5 +- .../ui/setting/SettingsEHFragment.kt | 14 + .../tachiyomi/ui/setting/SettingsFragment.kt | 10 +- app/src/main/java/exh/ActivityAskUpdate.java | 377 +++++++++ app/src/main/java/exh/ActivityBatchAdd.java | 132 ++++ .../main/java/exh/ActivityInterceptLink.java | 69 ++ app/src/main/java/exh/ActivityPE.java | 132 ++++ app/src/main/java/exh/CheckUpdatePref.java | 31 + app/src/main/java/exh/DialogLogin.java | 251 ++++++ app/src/main/java/exh/ExHentaiLoginPref.java | 115 +++ .../main/java/exh/FavoritesSyncManager.java | 190 +++++ app/src/main/java/exh/NetworkManager.java | 30 + app/src/main/java/exh/Objects.java | 128 +++ app/src/main/java/exh/OpenPEPref.java | 29 + app/src/main/java/exh/StringJoiner.java | 247 ++++++ app/src/main/java/exh/Util.java | 17 + .../res/drawable/ic_cached_white_24dp.xml | 9 + .../drawable/ic_playlist_add_black_24dp.xml | 9 + .../drawable/ic_playlist_add_grey_24dp.xml | 10 + .../main/res/drawable/ic_share_white_24dp.xml | 9 + .../res/drawable/ic_whatshot_black_24dp.xml | 9 + .../layout/activity_activity_batch_add.xml | 48 ++ .../main/res/layout/activity_dialog_login.xml | 34 + .../main/res/layout/activity_intercept.xml | 27 + app/src/main/res/layout/activity_pe.xml | 17 + app/src/main/res/layout/activity_update.xml | 83 ++ app/src/main/res/menu/catalogue_list.xml | 5 + app/src/main/res/menu/library.xml | 8 +- app/src/main/res/menu/library_selection.xml | 5 + app/src/main/res/menu/menu_navigation.xml | 5 + app/src/main/res/values/arrays.xml | 17 + app/src/main/res/values/keys.xml | 1 + app/src/main/res/values/strings.xml | 8 +- app/src/main/res/xml/pref_about.xml | 15 +- app/src/main/res/xml/pref_advanced.xml | 4 + app/src/main/res/xml/pref_ehentai.xml | 30 + branding/header.xcf | Bin 0 -> 664251 bytes branding/tutorials.xcf | Bin 0 -> 885239 bytes 56 files changed, 2980 insertions(+), 99 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/EHentai.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEHFragment.kt create mode 100644 app/src/main/java/exh/ActivityAskUpdate.java create mode 100644 app/src/main/java/exh/ActivityBatchAdd.java create mode 100644 app/src/main/java/exh/ActivityInterceptLink.java create mode 100644 app/src/main/java/exh/ActivityPE.java create mode 100644 app/src/main/java/exh/CheckUpdatePref.java create mode 100644 app/src/main/java/exh/DialogLogin.java create mode 100644 app/src/main/java/exh/ExHentaiLoginPref.java create mode 100644 app/src/main/java/exh/FavoritesSyncManager.java create mode 100644 app/src/main/java/exh/NetworkManager.java create mode 100644 app/src/main/java/exh/Objects.java create mode 100644 app/src/main/java/exh/OpenPEPref.java create mode 100644 app/src/main/java/exh/StringJoiner.java create mode 100644 app/src/main/java/exh/Util.java create mode 100644 app/src/main/res/drawable/ic_cached_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_playlist_add_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_playlist_add_grey_24dp.xml create mode 100644 app/src/main/res/drawable/ic_share_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_whatshot_black_24dp.xml create mode 100644 app/src/main/res/layout/activity_activity_batch_add.xml create mode 100644 app/src/main/res/layout/activity_dialog_login.xml create mode 100644 app/src/main/res/layout/activity_intercept.xml create mode 100644 app/src/main/res/layout/activity_pe.xml create mode 100644 app/src/main/res/layout/activity_update.xml create mode 100644 app/src/main/res/xml/pref_ehentai.xml create mode 100644 branding/header.xcf create mode 100644 branding/tutorials.xcf diff --git a/.gitignore b/.gitignore index af291a578..d6334be5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.gradle +.gradle/ /local.properties /.idea/workspace.xml .DS_Store @@ -6,4 +6,5 @@ .idea/ *iml *.iml -*/build \ No newline at end of file +*/build +/libs/SubsamplingScaleImageView/build \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..ab1677ac6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Update 13 -> Update 14 +- Fixed some fjords bugs +# Update 12 -> Update 13 +- Fixed searches that return no results to not show a confusing error message +- Added batch add function +- Added URL export function +- Bug fixes +- Added genre filtering +- Renamed Catalogues button in navbar to Galleries \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6c06d311f..17bca730f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,8 +38,8 @@ android { minSdkVersion 16 targetSdkVersion 23 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - versionCode 10 - versionName "0.2.3" + versionCode 218 + versionName "Tachiyomi-EH-2.18 (Update 18)" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" @@ -51,11 +51,14 @@ android { buildTypes { debug { - versionNameSuffix "-${getCommitCount()}" - applicationIdSuffix ".debug" + applicationIdSuffix ".eh" + minifyEnabled false + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } release { - minifyEnabled true + applicationIdSuffix ".eh" + minifyEnabled false shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } @@ -165,6 +168,9 @@ dependencies { compile 'me.zhanghai.android.systemuihelper:library:1.0.0' compile 'org.adw.library:discrete-seekbar:1.0.1' + //EXH + compile 'com.jakewharton:process-phoenix:1.0.2' + // Tests testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:1.7.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 503af4860..b192b0128 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,11 +2,11 @@ - - + + - - + + - + - + + android:parentActivityName=".ui.main.MainActivity"> + android:parentActivityName=".ui.main.MainActivity"> + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt index 220e75140..1302d6221 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt @@ -1,17 +1,17 @@ package eu.kanade.tachiyomi.data.mangasync import android.content.Context -import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList +//import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList class MangaSyncManager(private val context: Context) { companion object { - const val MYANIMELIST = 1 +// const val MYANIMELIST = 1 } - val myAnimeList = MyAnimeList(context, MYANIMELIST) +// val myAnimeList = MyAnimeList(context, MYANIMELIST) - val services = listOf(myAnimeList) + val services = emptyList() fun getService(id: Int) = services.find { it.id == id } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.kt index 4e95ed564..063b22233 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.data.network import android.content.Context +import eu.kanade.tachiyomi.data.source.online.english.EHentai import okhttp3.Cache import okhttp3.OkHttpClient import java.io.File @@ -14,8 +15,9 @@ class NetworkHelper(context: Context) { private val cookieManager = PersistentCookieJar(context) val client = OkHttpClient.Builder() - .cookieJar(cookieManager) +// .cookieJar(cookieManager) .cache(Cache(cacheDir, cacheSize)) + .addInterceptor(EHentai.buildInterceptor(context)) .build() val forceCacheClient = client.newBuilder() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/network/Requests.kt b/app/src/main/java/eu/kanade/tachiyomi/data/network/Requests.kt index ec53e21a3..07b68c9d5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/network/Requests.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/network/Requests.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.network import okhttp3.* import java.util.concurrent.TimeUnit.MINUTES -private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() +val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() private val DEFAULT_HEADERS = Headers.Builder().build() private val DEFAULT_BODY: RequestBody = FormBody.Builder().build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 93a0c8bfd..4c77531ad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -17,7 +17,7 @@ class PreferencesHelper(context: Context) { val keys = PreferenceKeys(context) - private val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val rxPrefs = RxSharedPreferences.create(prefs) private val defaultDownloadsDir = File(Environment.getExternalStorageDirectory().absolutePath + @@ -86,7 +86,7 @@ class PreferencesHelper(context: Context) { fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false) - fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN")) + fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("ALL")) fun sourceUsername(source: Source) = prefs.getString(keys.sourceUsername(source.id), "") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/Language.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/Language.kt index bbccebad9..a2ac305b1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/Language.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/Language.kt @@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.source class Language(val code: String, val lang: String) -val DE = Language("DE", "German") -val EN = Language("EN", "English") -val RU = Language("RU", "Russian") +val ALL = Language("ALL", "All") -fun getLanguages() = listOf(DE, EN, RU) \ No newline at end of file +fun getLanguages() = listOf(ALL) \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt index 1b1b8ef08..01e1c9ddd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt @@ -7,29 +7,23 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource import eu.kanade.tachiyomi.data.source.online.english.* -import eu.kanade.tachiyomi.data.source.online.german.WieManga -import eu.kanade.tachiyomi.data.source.online.russian.Mangachan -import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga -import eu.kanade.tachiyomi.data.source.online.russian.Readmanga import eu.kanade.tachiyomi.util.hasPermission +import exh.DialogLogin import org.yaml.snakeyaml.Yaml import timber.log.Timber import java.io.File open class SourceManager(private val context: Context) { - val BATOTO = 1 - val MANGAHERE = 2 - val MANGAFOX = 3 - val KISSMANGA = 4 - val READMANGA = 5 - val MINTMANGA = 6 - val MANGACHAN = 7 - val READMANGATODAY = 8 - val MANGASEE = 9 - val WIEMANGA = 10 + val EHENTAI = 1 + val EXHENTAI = 2 - val LAST_SOURCE = 10 + val LAST_SOURCE by lazy { + if (DialogLogin.isLoggedIn(context, false)) + 2 + else + 1 + } val sourcesMap = createSources() @@ -40,16 +34,8 @@ open class SourceManager(private val context: Context) { fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java) private fun createSource(id: Int): Source? = when (id) { - BATOTO -> Batoto(context, id) - KISSMANGA -> Kissmanga(context, id) - MANGAHERE -> Mangahere(context, id) - MANGAFOX -> Mangafox(context, id) - READMANGA -> Readmanga(context, id) - MINTMANGA -> Mintmanga(context, id) - MANGACHAN -> Mangachan(context, id) - READMANGATODAY -> Readmangatoday(context, id) - MANGASEE -> Mangasee(context, id) - WIEMANGA -> WieManga(context, id) + EHENTAI -> EHentai(context, id, false) + EXHENTAI -> EHentai(context, id, true) else -> null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt index fa9759b82..58fb64deb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt @@ -345,7 +345,7 @@ abstract class OnlineSource(context: Context) : Source { .asObservable() .doOnNext { if (!it.isSuccessful) { - it.close() + it.body().close() throw RuntimeException("Not a valid response") } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/EHentai.java b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/EHentai.java new file mode 100644 index 000000000..cb95f6357 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/EHentai.java @@ -0,0 +1,739 @@ +package eu.kanade.tachiyomi.data.source.online.english; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.v4.app.ShareCompat; +import android.support.v7.app.AlertDialog; +import android.text.TextUtils; +import android.widget.Toast; + +import org.jetbrains.annotations.NotNull; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import eu.kanade.tachiyomi.data.database.models.Chapter; +import eu.kanade.tachiyomi.data.database.models.Manga; +import eu.kanade.tachiyomi.data.network.RequestsKt; +import eu.kanade.tachiyomi.data.preference.PreferencesHelper; +import eu.kanade.tachiyomi.data.source.Language; +import eu.kanade.tachiyomi.data.source.LanguageKt; +import eu.kanade.tachiyomi.data.source.SourceManager; +import eu.kanade.tachiyomi.data.source.model.MangasPage; +import eu.kanade.tachiyomi.data.source.model.Page; +import eu.kanade.tachiyomi.data.source.online.OnlineSource; +import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment; +import exh.DialogLogin; +import exh.ExHentaiLoginPref; +import exh.NetworkManager; +import exh.StringJoiner; +import exh.Util; +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +public class EHentai extends OnlineSource { + + public static final String[] GENRE_LIST = { + "doujinshi", + "manga", + "artistcg", + "gamecg", + "western", + "non-h", + "imageset", + "cosplay", + "asianporn", + "misc" + }; + + public static ArrayList ENABLED_GENRES = null; + public static final String KEY_GENRE_FILTER = "exh_genre_filter"; + + public static final String QUERY_PREFIX = "?f_apply=Apply+Filter"; + + public static String HOST = "http://g.e-hentai.org/"; + public static String RAW_EXHHOST = "exhentai.org/"; + public static String EXHHOST = "http://" + RAW_EXHHOST; + + private static final String QUALITY_PLACEHOLDER = "{QUALITY}"; + private static final String DEFAULT_CONFIG = "uconfig=uh_y-lt_m-tl_r-tr_2-ts_m-prn_y-dm_l-ar_0-xns_0-rc_0-rx_0-ry_0-cs_a-fs_p-to_a-pn_0-sc_0-ru_rrggb-xr_" + QUALITY_PLACEHOLDER + "-sa_y-oi_n-qb_n-tf_n-hh_-hp_-hk_-cats_0-xl_-ms_n-mt_n;"; + + public static final String FAVORITES_PATH = "favorites.php"; + + boolean isExhentai = false; + Context context; + int id; + + PreferencesHelper helper; + + public EHentai(Context context, int id, boolean isExhentai) { + super(context); + this.context = context.getApplicationContext(); + this.isExhentai = isExhentai; + helper = new PreferencesHelper(context); + this.id = id; +// requestHeaders = headersBuilder().build(); +// glideHeaders = glideHeadersBuilder().build(); + } + + public static void saveGenreFilter(PreferencesHelper helper) { + Set genreSet = new HashSet<>(); + genreSet.addAll(ENABLED_GENRES); + helper.getPrefs().edit().putStringSet(KEY_GENRE_FILTER, genreSet).commit(); + } + + public static void loadGenreFilter(PreferencesHelper helper) { + Set defaultSet = new HashSet<>(); + defaultSet.addAll(Arrays.asList(GENRE_LIST)); + ENABLED_GENRES.clear(); + ENABLED_GENRES.addAll(helper.getPrefs().getStringSet(KEY_GENRE_FILTER, defaultSet)); + } + + public static List getEnabledGenres(PreferencesHelper helper) { + if(ENABLED_GENRES == null) { + ENABLED_GENRES = new ArrayList<>(); + loadGenreFilter(helper); + } + return ENABLED_GENRES; + } + + public static void launchGenreSelectionDialog(Context context, final CatalogueFragment catalogueFragment) { + final PreferencesHelper helper = new PreferencesHelper(context); + final boolean[] selectedGenres = new boolean[GENRE_LIST.length]; + for (int i = 0; i < GENRE_LIST.length; i++) { + selectedGenres[i] = getEnabledGenres(helper).contains(GENRE_LIST[i]); + } + AlertDialog dialog = new AlertDialog.Builder(context) + .setTitle("Genre Filter") + .setMultiChoiceItems(GENRE_LIST, selectedGenres, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog1, int indexSelected, boolean isChecked) { + if (isChecked) { + selectedGenres[indexSelected] = true; + } else if (selectedGenres[indexSelected]) { + selectedGenres[indexSelected] = false; + } + } + }).setPositiveButton("Apply", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog1, int id) { + dialog1.dismiss(); + getEnabledGenres(helper).clear(); + for (int i = 0; i < GENRE_LIST.length; i++) { + if (selectedGenres[i]) { + getEnabledGenres(helper).add(GENRE_LIST[i]); + } + } + //Save the new genre filter + saveGenreFilter(helper); + String originalQuery = catalogueFragment.getQuery(); + if(originalQuery == null){ + originalQuery = ""; + } + //Force a new search event + catalogueFragment.onSearchEvent(originalQuery, true, true); + } + }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog1, int id) { + dialog1.dismiss(); + } + }).create(); + dialog.show(); + } + + public static String buildGenreString(PreferencesHelper helper) { + StringBuilder genreString = new StringBuilder(); + for (String genre : GENRE_LIST) { + genreString.append("&f_"); + genreString.append(genre); + genreString.append("="); + genreString.append(getEnabledGenres(helper).contains(genre) ? "1" : "0"); + } + return genreString.toString(); + } + + public static String getQualityMode(PreferencesHelper prefHelper) { + return prefHelper.getPrefs().getString("ehentai_quality", "auto"); + } + + @NonNull + @Override public Language getLang() { + return LanguageKt.getALL(); + } + + @NonNull + @Override public String getName() { + return isExhentai ? "ExHentai" : "EHentai"; + } + + @NonNull + @Override public String getBaseUrl() { + if(isExhentai) { + return buildExhHost(helper.getPrefs()); + } else { + return HOST; + } + } + + public static String buildExhHost(SharedPreferences preferences) { + boolean secureExh = preferences.getBoolean("secure_exh", true); + if (secureExh) { + return "https://" + RAW_EXHHOST; + } else { + return "http://" + RAW_EXHHOST; + } + } + + @NonNull + @Override protected String popularMangaInitialUrl() { + return getBaseUrl() + QUERY_PREFIX + buildGenreString(helper); + } + + @NonNull + @Override protected String searchMangaInitialUrl(@NonNull String query) { + try { + log("Query: " + getBaseUrl() + QUERY_PREFIX + buildGenreString(helper) + "&f_search=" + URLEncoder.encode(query, "UTF-8")); + return getBaseUrl() + QUERY_PREFIX + buildGenreString(helper) + "&f_search=" + URLEncoder.encode(query, "UTF-8"); + } catch (UnsupportedEncodingException e) { + //How can this happen :/ + throw new RuntimeException(e); + } + } + + //Yes OkHttp has an interceptor but the glide headers still require the cookie strings + @NonNull + @Override protected Headers.Builder headersBuilder() { + return getHeadersBuilder(helper); + } + + public static Headers.Builder getHeadersBuilder(PreferencesHelper helper) { + Headers.Builder builder = new Headers.Builder(); + String cookies = appendQualityChar(helper, helper.getPrefs().getString("eh_cookie_string", "").trim()); + cookies = cleanCookieString("nw=1; " + cookies); + log("New cookies: " + cookies); + builder.add("Cookie", cookies); + return builder; + } + + /*@Override protected LazyHeaders.Builder glideHeadersBuilder() { + LazyHeaders.Builder builder = super.glideHeadersBuilder(); + builder.addHeader("Cookie", helper.getPrefs().getString("eh_cookie_string", "").trim()); + return builder; + }*/ + + public static void exportMangaURLs(Activity activity, List mangaList) { + StringJoiner urlJoiner = new StringJoiner("\n"); + for (Manga manga : mangaList) { + if (!TextUtils.isEmpty(manga.getUrl())) { + String url = manga.getUrl(); + if (manga.getSource() == 1) { + url = HOST + url; + } else if (manga.getSource() == 2) { + url = EXHHOST + url; + } + urlJoiner.add(url); + } + } + ShareCompat.IntentBuilder + .from(activity) // getActivity() or activity field if within Fragment + .setText(urlJoiner.toString()) + .setType("text/plain") // most general text sharing MIME type + .setChooserTitle("Share Gallery URLs") + .startChooser(); + } + + public static class FavoritesResponse { + public Map> favs; + public List favCategories; + + public FavoritesResponse(Map> favs, List favCategories) { + this.favs = favs; + this.favCategories = favCategories; + } + } + + public static class BuildFavoritesBaseResponse { + public final String favoritesBase; + public final int id; + + public BuildFavoritesBaseResponse(String favoritesBase, int id) { + this.favoritesBase = favoritesBase; + this.id = id; + } + } + public static BuildFavoritesBaseResponse buildFavoritesBase(Context context, SharedPreferences preferences) { + String favoritesBase; + int id; + if(DialogLogin.isLoggedIn(context, false)) { + favoritesBase = buildExhHost(preferences); + id = 2; + } else { + favoritesBase = HOST; + id = 1; + } + favoritesBase += FAVORITES_PATH; + return new BuildFavoritesBaseResponse(favoritesBase, id); + } + + public static FavoritesResponse fetchFavorites(Context context) throws IOException { + PreferencesHelper helper = new PreferencesHelper(context); + BuildFavoritesBaseResponse buildFavoritesBaseResponse = buildFavoritesBase(context, helper.getPrefs()); + String favoritesBase = buildFavoritesBaseResponse.favoritesBase; + int id = buildFavoritesBaseResponse.id; + //Used to get "s" cookie + Response response1 = NetworkManager.getInstance().getClient().newCall( + RequestsKt.GET(favoritesBase, getHeadersBuilder(helper).build(), RequestsKt.getDEFAULT_CACHE_CONTROL())).execute(); + //Extract favorite names + List favNames = new ArrayList<>(); + Document onlyFavsDoc = responseToDocument(response1); + for(Element element : onlyFavsDoc.select(".nosel").first().children()) { + if(element.children().size() > 0) { + favNames.add(element.child(2).text()); + } + } + String sCookie = null; + Map foundCookies = getCookies(response1.header("Set-Cookie")); + if(foundCookies != null) { + sCookie = foundCookies.get("s"); + } + Headers.Builder cookiesBuilder = getHeadersBuilder(helper); + String oldCookies; + if((oldCookies = cookiesBuilder.get("Cookie")) != null && sCookie != null) { + cookiesBuilder.removeAll("Cookie"); + cookiesBuilder.add("Cookie", "s=" + sCookie + "; " + oldCookies); + } + Response response2 = NetworkManager.getInstance().getClient().newCall( + RequestsKt.GET(favoritesBase, cookiesBuilder.build(), RequestsKt.getDEFAULT_CACHE_CONTROL())).execute(); + ParsedMangaPage parsed = parseMangaPage(response2, id); + return new FavoritesResponse(parsed.mangas, favNames); + } + + private static class ParsedMangaPage { + public String nextPageUrl; + public Map> mangas; + } + + public static ParsedMangaPage parseMangaPage(Response response, int id) { + ParsedMangaPage mangaPage = new ParsedMangaPage(); + Map> mangas = new HashMap<>(); + mangaPage.mangas = mangas; + Document parsedHtml = responseToDocument(response); + for (Element element : parsedHtml.select("div[style=position:relative]")) { + Element info = element.select("div.it5").first().children().first(); + //Append no warning query + Manga manga = Manga.Companion.create(pathOnly(info.attr("href")), id); + manga.setTitle(info.text()); + Element pic = element.select("div.it2").first(); + if (pic.children().first() != null) { + manga.setThumbnail_url(pic.children().first().attr("src")); + } else { + //Thumbnails are encoded + String[] split = pic.text().split("~"); + manga.setThumbnail_url("http://" + split[1] + "/" + split[2]); + } + String favoriteName = "Default"; + Element parent = element.select("div.it3").first(); + if(parent != null) { + for(Element possibleFavoriteElement : parent.children()) { + if(possibleFavoriteElement.id().startsWith("favicon")) { + favoriteName = possibleFavoriteElement.attr("title"); + break; + } + } + } + List mangaList = mangas.get(favoriteName); + if(mangaList == null) { + mangaList = new ArrayList<>(); + mangas.put(favoriteName, mangaList); + } + mangaList.add(manga); + } + mangaPage.nextPageUrl = parseNextSearchUrl(parsedHtml); + return mangaPage; + } + + private static Document responseToDocument(Response response) { + try { + return Jsoup.parse(response.body().string(), response.request().url().toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override protected void popularMangaParse(@NonNull Response response, @NonNull MangasPage page) { + if (isExhentai && !loginIfEHentai()) { + return; + } + ParsedMangaPage parsedPage = parseMangaPage(response, getId()); + for(List found : parsedPage.mangas.values()) { + for(Manga manga : found) { + page.getMangas().add(manga); + } + } + page.setNextPageUrl(parsedPage.nextPageUrl); + } + + public static String pathOnly(String url) { + return pathOnly(url, true); + } + + public static String pathOnly(String url, boolean appendNw) { + if(url == null) { + return null; + } + try { + URL urlObj = new URL(url); + StringBuilder builder = new StringBuilder(urlObj.getPath()); + if (urlObj.getQuery() != null) { + builder.append('?'); + builder.append(urlObj.getQuery()); + if(appendNw && !urlObj.getQuery().trim().isEmpty()) { + builder.append("&"); + } + } else if(appendNw) { + builder.append("?"); + } + if(appendNw) { + builder.append("nw=always"); + } + String string = builder.toString(); + while(string.startsWith("/")) { + string = string.substring(1); + } + return string; + } catch (MalformedURLException e) { + return url; + } + } + + @Override + protected void searchMangaParse(@NotNull Response response, @NotNull MangasPage page, @NotNull String query) { + popularMangaParse(response, page); + } + + + protected static String parseNextSearchUrl(Document parsedHtml) { + Elements buttons = parsedHtml.select("a[onclick=return false]"); + Element lastButton = buttons.last(); + if (lastButton != null) { + if (lastButton.text().equals(">")) { + return buttons.last().attr("href"); + } + } + return null; + } + + @Override + protected void mangaDetailsParse(@NotNull Response response, @NotNull Manga m) { + Document document = responseToDocument(response); + m.setUrl(pathOnly(response.request().url().toString())); + log(pathOnly(response.request().url().toString())); + m.setSource(getId()); + StringBuilder synopsis = new StringBuilder(); + String title = ""; + try { + title = document.select("#gn").text(); + synopsis.append("Title: "); + synopsis.append(title); + try { + Elements jpTitleElements = document.select("h1[id=gj]"); + if(jpTitleElements.size() > 0) { + synopsis.append("\n"); + synopsis.append("Japanese Title: "); + synopsis.append(jpTitleElements.text()); + } + } catch (Exception ignored) { + } + synopsis.append("\n\n"); + } catch (Exception ignored) { + } + m.setTitle(title); + //Synopsis + try { + StringBuilder tempBuilder = new StringBuilder(); + for (Element element : document.select("div[id=gdd]").first().children().first().children().first().children()) { + tempBuilder.append(element.text()).append("\n"); + } + synopsis.append(tempBuilder); + } catch (Exception e) { + synopsis.append("Error fetching description!"); + } + //Ratings + try { + String ratingString = document.select("td[id=rating_label]").first().text(); + String ratingCount = document.select("span[id=rating_count]").first().text().trim(); + ratingString = ratingString.split(": ")[1].trim(); + synopsis.append("Rating: ").append(ratingString).append(" (").append(ratingCount).append(")\n"); + } catch (Exception ignored) { + } + synopsis.append("\nTags:\n"); + try { + StringBuilder tempBuilder = new StringBuilder(); + Element tbody = document.select("div[id=taglist]").first().children().first().children().first(); + for (Element element : tbody.children()) { + String name = element.child(0).text(); + Elements tags = element.select("a"); + StringBuilder tagBuilder = new StringBuilder(); + for (Element tag : tags) { + tagBuilder.append(" <"); + tagBuilder.append(tag.text()); + tagBuilder.append(">"); + } + tempBuilder.append("▪ "); + tempBuilder.append(name); + tempBuilder.append(tagBuilder); + tempBuilder.append('\n'); + } + synopsis.append(tempBuilder); + } catch (Exception e) { + synopsis.append("No tags have been added for this gallery yet."); + } + m.setDescription(synopsis.toString()); + //Image + try { + m.setThumbnail_url(document.select("div[id=gd1]").first().children().first().attr("src")); + } catch (Exception ignored) { + } + //Author + try { + m.setAuthor(document.select("div[id=gdn]").first().children().first().text()); + } catch (Exception e) { + synopsis.append("Error fetching author!"); + } + //Genre + try { + m.setGenre(document.select("img[class=ic]").first().attr("alt")); + } catch (Exception e) { + synopsis.append("Error fetching genre!"); + } + } + + @NotNull + @Override + protected Request popularMangaRequest(@NotNull MangasPage page) { + if (isExhentai && !loginIfEHentai()) { + page.getMangas().clear(); + page.setNextPageUrl(null); + return super.popularMangaRequest(page); + } + return super.popularMangaRequest(page); + } + + @Override + protected void chapterListParse(@NotNull Response response, @NotNull List chapters) { + //Chapters + Chapter mainChapter = Chapter.Companion.create(); + mainChapter.setUrl(pathOnly(response.request().url().toString())); + mainChapter.setName("Chapter"); + chapters.add(mainChapter); + } + + boolean loginIfEHentai() { + Handler uiHandler = new Handler(Looper.getMainLooper()); + final boolean isLoggedIn = DialogLogin.isLoggedIn(context, false); + if (!isLoggedIn) { + uiHandler.post(new Runnable() { + @Override public void run() { + Toast.makeText(context, "In order to access ExHentai you must be logged in! Please log in the settings section!", Toast.LENGTH_SHORT).show(); + } + }); + return false; +// DialogLogin.requestLogin(context); +// if (!DialogLogin.isLoggedIn(context, true)) { +// return false; +// } + } + return true; + } + + String parseChapterPage(ArrayList urls, String url) throws Exception { + log("Parsing chapter page: " + url); + String source = getClient().newCall(RequestsKt.GET(getBaseUrl() + url, getHeaders(), RequestsKt.getDEFAULT_CACHE_CONTROL())) + .execute().body().string(); + Document document = Jsoup.parse(source, url); + //Parse each page + for (Element element : document.select("div[class=gdtm]")) { + Element next = element.children().first().children().first(); + String pageUrl = next.attr("href"); + int pageNumber = Integer.parseInt(next.children().first().attr("alt")); + log("Got page: " + pageNumber + ", " + pageUrl); +// List pages = c.getPages(); +// if(pages == null) pages = new ArrayList<>(); +// pages.add(new Page(pageNumber, pageUrl)); + urls.add(pageUrl); + } + + //Parse to get next page + Elements selection = document.select("a[onclick=return false]"); + if (selection.size() < 1) { + return null; + } else { + if (selection.last().text().equals(">")) { + return pathOnly(selection.last().attr("href"), false); + } else { + return null; + } + } + } + + @Override + protected void pageListParse(@NotNull Response response, @NotNull List pages) { + ArrayList urls = new ArrayList<>(); + response.body().close(); + String url = pathOnly(response.request().url().toString()); //Have to do this as EXH chapters span multiple pages + while (url != null) { + try { + url = parseChapterPage(urls, url); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + for(int i = 0; i < urls.size(); i++) { + pages.add(new Page(i, urls.get(i), null, null)); + } + } + + public static void performLogout(Context context) { + log("Logging out..."); + NetworkManager.getInstance().getCookieManager().getCookieStore().removeAll(); + android.webkit.CookieManager.getInstance().removeAllCookie(); + PreferenceManager.getDefaultSharedPreferences(context).edit().remove("eh_cookie_string").apply(); + } + + @NotNull + @Override + protected String imageUrlParse(@NotNull Response response) { + Document document = responseToDocument(response); + + if (isExhentai) { + Element element = document.select("#img").first(); + if (element != null) { + log("Image URL: " + element.attr("src")); + return element.attr("src"); + } + log("NO IMAGE FOUND!"); + } else { + for (Element element : document.select("div[class=sni] img")) { + if (!element.attr("src").contains("http://ehgt.org/")) { + log("Image URL: " + element.attr("src")); + return element.attr("src"); + } + } + log("NO IMAGE FOUND!"); + } + return ""; + } + + private static String appendQualityChar(PreferencesHelper helper, String string) { + String qualityChar = "a"; + switch (getQualityMode(helper)) { + case "auto": + qualityChar = "a"; + break; + case "ovrs_2400": + qualityChar = "2400"; + break; + case "ovrs_1600": + qualityChar = "1600"; + break; + case "high": + qualityChar = "1280"; + break; + case "med": + qualityChar = "980"; + break; + case "low": + qualityChar = "780"; + break; + } + if(!string.endsWith(";") && !string.isEmpty()) + string += ";"; + if(!string.endsWith(" ") && !string.isEmpty()) + string += " "; + return string + DEFAULT_CONFIG.replace(QUALITY_PLACEHOLDER, qualityChar); + } + + public static Interceptor buildInterceptor(Context context) { + final PreferencesHelper localPreferenceHelper = new PreferencesHelper(context); + return new Interceptor() { + @Override public Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); + String originalCookies; + if (originalRequest.header("Cookie") != null) { + originalCookies = originalRequest.header("Cookie").trim(); + } else { + originalCookies = ""; + } + String newCookies = appendQualityChar(localPreferenceHelper, localPreferenceHelper.getPrefs().getString("eh_cookie_string", "").trim()) + " " + originalCookies; + newCookies = cleanCookieString("nw=1; " + newCookies); //No warning + Request requestWithUserAgent = originalRequest.newBuilder() + .removeHeader("Cookie") + .addHeader("Cookie", newCookies) + .removeHeader("User-Agent") + .addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36") + .build(); + log("NewCookies: " + newCookies); + return chain.proceed(requestWithUserAgent); + } + }; + } + + //Removes duplicate entries in cookie string and reformats it + public static String cleanCookieString(String cookies) { + Map foundCookies = getCookies(cookies); + if(foundCookies == null) { + return cookies; + } + StringJoiner cookieJoiner = new StringJoiner("; "); + for(Map.Entry cookie : foundCookies.entrySet()) { + cookieJoiner.add(cookie.getKey() + "=" + cookie.getValue()); + } + return cookieJoiner.toString(); + } + + public static Map getCookies(String cookies) { + Map foundCookies = new HashMap<>(); + for(String cookie : cookies.split(";")) { + String[] splitCookie = cookie.split("="); + if(splitCookie.length < 2) { + log("Invalid cookie string!"); + return null; + } + String trimmedKey = splitCookie[0].trim(); + if(!foundCookies.containsKey(trimmedKey)) { + foundCookies.put(trimmedKey, splitCookie[1].trim()); + } + } + return foundCookies; + } + + private static void log(String string) { +// Util.d("EHentai", string); + } + + @Override + public int getId() { + return id; + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt index ae62b22b5..278fd2737 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt @@ -16,6 +16,7 @@ import com.afollestad.materialdialogs.MaterialDialog import com.f2prateek.rx.preferences.Preference import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.source.online.english.EHentai import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.main.MainActivity @@ -64,7 +65,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold /** * Query of the search box. */ - private val query: String? + val query: String? get() = presenter.query /** @@ -212,12 +213,12 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold } searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { - onSearchEvent(query, true) + onSearchEvent(query, true, false) return true } override fun onQueryTextChange(newText: String): Boolean { - onSearchEvent(newText, false) + onSearchEvent(newText, false, false) return true } }) @@ -237,6 +238,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_display_mode -> swapDisplayMode() + R.id.action_genre_filter -> EHentai.launchGenreSelectionDialog(context, this) else -> return super.onOptionsItemSelected(item) } return true @@ -246,7 +248,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold super.onResume() queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .subscribe { searchWithQuery(it) } + .subscribe { searchWithQuery(it, false) } } override fun onPause() { @@ -269,9 +271,9 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold * @param query the new query. * @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT]. */ - private fun onSearchEvent(query: String, now: Boolean) { + fun onSearchEvent(query: String, now: Boolean, forceRequest: Boolean) { if (now) { - searchWithQuery(query) + searchWithQuery(query, forceRequest) } else { queryDebouncerSubject.onNext(query) } @@ -282,9 +284,9 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold * * @param newQuery the new query. */ - private fun searchWithQuery(newQuery: String) { + private fun searchWithQuery(newQuery: String, forceRequest: Boolean) { // If text didn't change, do nothing - if (query == newQuery) + if (query == newQuery && !forceRequest) return showProgressBar() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt index 6d1b9426a..17fce8664 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt @@ -6,7 +6,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.data.source.EN +import eu.kanade.tachiyomi.data.source.ALL import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.model.MangasPage @@ -326,7 +326,7 @@ class CataloguePresenter : BasePresenter() { // Ensure at least one language if (languages.isEmpty()) { - languages.add(EN.code) + languages.add(ALL.code) } return sourceManager.getOnlineSources() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt index 5f14d4eec..c948f18ab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt @@ -11,14 +11,17 @@ import android.view.* import com.afollestad.materialdialogs.MaterialDialog import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.source.online.english.EHentai import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.category.CategoryActivity import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.toast +import exh.FavoritesSyncManager import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.fragment_library.* import nucleus.factory.RequiresPresenter @@ -74,6 +77,8 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback */ var isFilterUnread = false + lateinit var favoritesSyncManager: FavoritesSyncManager + companion object { /** * Key to change the cover of a manga in [onActivityResult]. @@ -105,6 +110,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback setHasOptionsMenu(true) isFilterDownloaded = presenter.preferences.filterDownloaded().get() as Boolean isFilterUnread = presenter.preferences.filterUnread().get() as Boolean + favoritesSyncManager = FavoritesSyncManager(context, DatabaseHelper(context)) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { @@ -208,8 +214,13 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback // Apply filter onFilterCheckboxChanged() } - R.id.action_update_library -> { - LibraryUpdateService.start(activity, true) +// R.id.action_update_library -> { +// LibraryUpdateService.start(activity, true) +// } + R.id.action_sync -> { + favoritesSyncManager.guiSyncFavorites({ + (activity as MainActivity).setFragment(LibraryFragment.newInstance(), 0) + }); } R.id.action_edit_categories -> { val intent = CategoryActivity.newIntent(activity) @@ -307,6 +318,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback changeSelectedCover(presenter.selectedMangas) destroyActionModeIfNeeded() } + R.id.action_share -> EHentai.exportMangaURLs(this.activity, presenter.selectedMangas) R.id.action_move_to_category -> moveMangasToCategories(presenter.selectedMangas) R.id.action_delete -> showDeleteMangaDialog() else -> return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 54a3f78eb..333944283 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -16,6 +16,8 @@ import eu.kanade.tachiyomi.ui.library.LibraryFragment import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment import eu.kanade.tachiyomi.ui.setting.SettingsActivity +import exh.ActivityAskUpdate +import exh.ActivityBatchAdd import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.toolbar.* import uy.kohesive.injekt.injectLazy @@ -66,6 +68,7 @@ class MainActivity : BaseActivity() { val intent = Intent(this, SettingsActivity::class.java) startActivityForResult(intent, REQUEST_OPEN_SETTINGS) } + R.id.nav_drawer_batch_add -> startActivity(Intent(this, ActivityBatchAdd::class.java)) R.id.nav_drawer_backup -> setFragment(BackupFragment.newInstance(), id) } drawer.closeDrawer(GravityCompat.START) @@ -76,9 +79,10 @@ class MainActivity : BaseActivity() { // Set start screen setSelectedDrawerItem(startScreenId) - // Show changelog if needed - ChangelogDialogFragment.show(preferences, supportFragmentManager) - } + //Check for update + val context = this + Thread { ActivityAskUpdate.checkAndDoUpdateIfNeeded(context, true) }.start() + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -121,7 +125,7 @@ class MainActivity : BaseActivity() { } } - private fun setFragment(fragment: Fragment, itemId: Int) { + fun setFragment(fragment: Fragment, itemId: Int) { supportFragmentManager.beginTransaction() .replace(R.id.frame_container, fragment, "$itemId") .commit() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt index b717d29f1..2ace5ef93 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt @@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment -import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListFragment import eu.kanade.tachiyomi.util.SharedData import kotlinx.android.synthetic.main.activity_manga.* import kotlinx.android.synthetic.main.toolbar.* @@ -26,7 +25,6 @@ class MangaActivity : BaseRxActivity() { const val MANGA_EXTRA = "manga" const val INFO_FRAGMENT = 0 const val CHAPTERS_FRAGMENT = 1 - const val MYANIMELIST_FRAGMENT = 2 fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent { SharedData.put(MangaEvent(manga)) @@ -79,8 +77,6 @@ class MangaActivity : BaseRxActivity() { init { pageCount = 2 - if (!activity.fromCatalogue && activity.presenter.syncManager.myAnimeList.isLogged) - pageCount++ } override fun getCount(): Int { @@ -91,7 +87,6 @@ class MangaActivity : BaseRxActivity() { when (position) { INFO_FRAGMENT -> return MangaInfoFragment.newInstance() CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance() - MYANIMELIST_FRAGMENT -> return MyAnimeListFragment.newInstance() else -> return null } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt index 284995a36..46764bfeb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt @@ -46,18 +46,9 @@ class SettingsAboutFragment : SettingsFragment() { val version = findPreference(getString(R.string.pref_version)) val buildTime = findPreference(getString(R.string.pref_build_time)) + findPreference("acra.enable").isEnabled = false; - version.summary = if (BuildConfig.DEBUG) - "r" + BuildConfig.COMMIT_COUNT - else - BuildConfig.VERSION_NAME - - if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER) { - //Set onClickListener to check for new version - version.setOnPreferenceClickListener { - checkVersion() - true - } + version.summary = BuildConfig.VERSION_NAME //TODO One glorious day enable this and add the magnificent option for auto update checking. // automaticUpdateToggle.isEnabled = true @@ -66,7 +57,6 @@ class SettingsAboutFragment : SettingsFragment() { // UpdateDownloaderAlarm.startAlarm(activity, 12, status) // true // } - } buildTime.summary = getFormattedBuildTime() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt index 331e63d0b..4f578cd58 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt @@ -62,9 +62,10 @@ class SettingsActivity : BaseActivity(), return when (key) { "general_screen" -> SettingsGeneralFragment.newInstance(key) "downloads_screen" -> SettingsDownloadsFragment.newInstance(key) - "sources_screen" -> SettingsSourcesFragment.newInstance(key) - "sync_screen" -> SettingsSyncFragment.newInstance(key) +// "sources_screen" -> SettingsSourcesFragment.newInstance(key) +// "sync_screen" -> SettingsSyncFragment.newInstance(key) "advanced_screen" -> SettingsAdvancedFragment.newInstance(key) + "ehentai_screen" -> SettingsEHFragment.newInstance(key) "about_screen" -> SettingsAboutFragment.newInstance(key) else -> SettingsFragment.newInstance(key) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEHFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEHFragment.kt new file mode 100644 index 000000000..f3e00750f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEHFragment.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.os.Bundle +import android.support.v7.preference.XpPreferenceFragment + +class SettingsEHFragment : SettingsFragment() { + companion object { + fun newInstance(rootKey: String): SettingsEHFragment { + val args = Bundle() + args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey) + return SettingsEHFragment().apply { arguments = args } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt index 8c74375c6..89e13455c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt @@ -34,9 +34,10 @@ open class SettingsFragment : XpPreferenceFragment() { addPreferencesFromResource(R.xml.pref_general) addPreferencesFromResource(R.xml.pref_reader) addPreferencesFromResource(R.xml.pref_downloads) - addPreferencesFromResource(R.xml.pref_sources) - addPreferencesFromResource(R.xml.pref_sync) +// addPreferencesFromResource(R.xml.pref_sources) +// addPreferencesFromResource(R.xml.pref_sync) addPreferencesFromResource(R.xml.pref_advanced) + addPreferencesFromResource(R.xml.pref_ehentai) addPreferencesFromResource(R.xml.pref_about) // Add an icon to each subscreen @@ -76,9 +77,10 @@ open class SettingsFragment : XpPreferenceFragment() { "general_screen" to R.drawable.ic_tune_black_24dp, "reader_screen" to R.drawable.ic_chrome_reader_mode_black_24dp, "downloads_screen" to R.drawable.ic_file_download_black_24dp, - "sources_screen" to R.drawable.ic_language_black_24dp, - "sync_screen" to R.drawable.ic_sync_black_24dp, +// "sources_screen" to R.drawable.ic_language_black_24dp, +// "sync_screen" to R.drawable.ic_sync_black_24dp, "advanced_screen" to R.drawable.ic_code_black_24dp, + "ehentai_screen" to R.drawable.ic_whatshot_black_24dp, "about_screen" to R.drawable.ic_help_black_24dp ) diff --git a/app/src/main/java/exh/ActivityAskUpdate.java b/app/src/main/java/exh/ActivityAskUpdate.java new file mode 100644 index 000000000..451c71e4e --- /dev/null +++ b/app/src/main/java/exh/ActivityAskUpdate.java @@ -0,0 +1,377 @@ +package exh; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.support.v4.app.NotificationCompat; +import android.support.v7.app.AppCompatDialog; +import android.text.Html; +import android.util.Base64; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.TextView; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import eu.kanade.tachiyomi.BuildConfig; +import eu.kanade.tachiyomi.R; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class ActivityAskUpdate extends AppCompatDialog { + + String details; + String downloadURL; + + public ActivityAskUpdate(Context context, String details, String downloadURL) { + super(context); + this.details = details; + this.downloadURL = downloadURL; + setCancelable(false); + setTitle("New Version Available"); + } + + public static void checkAndDoUpdateIfNeeded(final Context context, boolean isAutoUpdate) { + final ProgressDialog[] pDialog = {null}; + try { + //Return immediately if auto update is disabled + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + if (!preferences.getBoolean("auto_update", true) && isAutoUpdate) return; + if(!isAutoUpdate) { + runOnUiThread(new Runnable() { + @Override public void run() { + pDialog[0] = ProgressDialog.show(context, "Please wait...", "Checking for updates...", true, true); + } + }); + } + if (!preferences.contains("force_update")) { + preferences + .edit() + .putBoolean("force_update", false) + .apply(); + } + Request request = new Request.Builder().url("http://nn9.pe.hu/tyeh/update.php").build(); + OkHttpClient client = NetworkManager.getInstance().getClient(); + Response response; + try { + response = client.newCall(request).execute(); + } catch (IOException e) { + Log.w("EHentai", "Could not check for updates!", e); + return; + } + if (response.isSuccessful()) { + String responseString; + try { + responseString = response.body().string(); + } catch (IOException e) { + Log.w("EHentai", "Could not check for updates!", e); + return; + } + String[] split = responseString.split("[\\r\\n]+"); + boolean hasUpdateHeader = false; + String author = ""; + int version = BuildConfig.VERSION_CODE; + String download = ""; + String description = ""; + for (String line : split) { + if (line.contains("")) hasUpdateHeader = true; + else { + int equalIndex = line.indexOf('='); + if(equalIndex == -1) continue; + String key = line.substring(0, equalIndex); + String value = line.substring(equalIndex + 1); + switch (key) { + case "Author": + author = value; + break; + case "Version": + try { + version = Integer.parseInt(value); + } catch (NumberFormatException e) { + Log.e("EHentai", "Exception parsing version number!", e); + } + break; + case "Download": + download = value; + break; + case "Description": + description = new String(Base64.decode(value, Base64.NO_WRAP)); + break; + } + } + } + if ((hasUpdateHeader && version > BuildConfig.VERSION_CODE) || preferences + .getBoolean("force_update", false)) { + Log.i("EHentai", "Update available, requesting!"); + final String finalDescription = description; + final String finalDownload = download; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override public void run() { + ActivityAskUpdate dialog = new ActivityAskUpdate(context, finalDescription, finalDownload); + if (pDialog[0] != null) { + pDialog[0].dismiss(); + } + dialog.show(); + } + }); + } else if (!isAutoUpdate) { + if(pDialog[0] != null) + pDialog[0].dismiss(); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override public void run() { + new AlertDialog.Builder(context) + .setTitle("Update Checker") + .setMessage("No update found!") + .setPositiveButton("OK!", new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }).show(); + } + }); + } + } + } catch(Throwable t) { + Log.e("EHentai", "Update check error!", t); + } finally { + runOnUiThread(new Runnable() { + @Override public void run() { + if (pDialog[0] != null) + pDialog[0].dismiss(); + } + }); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_update); + getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + ((TextView) findViewById(R.id.detailsView)).setText(Html.fromHtml(details)); + findViewById(R.id.downloadBtn).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { + //The comments below are for background downloading, background downloading is sort of useless however so to reduce maintenance costs, it has been disabled + /*new AlertDialog.Builder(ActivityAskUpdate.this.getContext()) + .setCancelable(false) + .setTitle("Download in Background?") + .setMessage("Would you like to download the update in the background? " + + "This means you can keep reading while the update is downloading! " + + "I will notify you when the download is done!") + .setPositiveButton("Yes", new OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + ActivityAskUpdate.this.dismiss(); + ActivityAskUpdate.this.performUpdate(true); + } + }) + .setNegativeButton("No", new OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { + dialog.dismiss();*/ + ActivityAskUpdate.this.performUpdate(false); + /* + } + }) + .setNeutralButton("Cancel", new OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + ActivityAskUpdate.this.dismiss(); + } + }).show();*/ + } + }); + findViewById(R.id.ignoreUpdatesBtn).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { + PreferenceManager.getDefaultSharedPreferences(ActivityAskUpdate.this.getContext()).edit().putBoolean("auto_update", false).apply(); + ActivityAskUpdate.this.dismiss(); + } + }); + findViewById(R.id.cancelBtn).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { + ActivityAskUpdate.this.dismiss(); + } + }); + } + + private void notifyBackgroundDownloadDone(File apkFile) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext()) + .setLargeIcon(BitmapFactory.decodeResource(getContext().getResources(), R.drawable.ic_file_download_white_24dp)) + .setSmallIcon(R.drawable.ic_file_download_white_24dp) + .setContentTitle("Update Download Complete") + .setContentText("Update download complete! Press on me to begin start installation!"); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent pendingIntent = PendingIntent.getActivity(ActivityAskUpdate.this.getContext().getApplicationContext(), + 0, + intent, + 0); + builder.setContentIntent(pendingIntent); + ((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE)).notify(0, builder.build()); + Log.i("EHentai", "Update download complete!"); + } + + private void notifyBackgroundDownloadFail(String failure) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext()) + .setLargeIcon(BitmapFactory.decodeResource(getContext().getResources(), R.drawable.ic_file_download_white_24dp)) + .setSmallIcon(R.drawable.ic_file_download_white_24dp) + .setContentTitle("Update Download Failed") + .setContentText(failure); + ((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE)).notify(1, builder.build()); + Log.i("EHentai", "Update download failed! (" + failure + ")"); + } + + private void performUpdate(final boolean background) { + final ProgressDialog dialog = new ProgressDialog(getContext()); + dialog.setTitle("Downloading Update"); + dialog.setMessage("Downloading update... (This may take a while)"); + dialog.setIndeterminate(true); + dialog.setCancelable(false); + dialog.show(); + + doKeepDialog(dialog); + //Just dismiss it right away if we are downloading in the background + if (background) { + dialog.dismiss(); + } + + + new Thread(new Runnable() { + @Override public void run() { + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url(downloadURL) + .build(); + Response response = null; + try { + response = client.newCall(request).execute(); + } catch (IOException e) { + Log.e("EHentai", "Update download failed!", e); + e.printStackTrace(); + } + if (response == null || !response.isSuccessful()) { + dialog.dismiss(); + if (!background) { + runOnUiThread(new Runnable() { + @Override public void run() { + new AlertDialog.Builder(ActivityAskUpdate.this.getContext()) + .setTitle("Error!") + .setMessage("Could not download update! Please try again later!") + .setNeutralButton("Ok", new OnClickListener() { + @Override + public void onClick(DialogInterface dialog1, int which) { + dialog1.dismiss(); + ActivityAskUpdate.this.dismiss(); + } + }).create().show(); + } + }); + } else { + ActivityAskUpdate.this.notifyBackgroundDownloadFail("Could not download update! Please try again later!"); + } + } else { + File downloadFolder = getContext().getExternalCacheDir(); + downloadFolder.mkdirs(); + final File apkFile = new File(downloadFolder, "teh-autoupdate.apk"); + if (apkFile.exists()) + apkFile.delete(); + try { + apkFile.createNewFile(); + FileOutputStream outputStream = new FileOutputStream(apkFile); + InputStream inputStream = response.body().byteStream(); + int bytesCopied = 0; + long lastUpdate = System.currentTimeMillis(); + byte[] buffer = new byte[1024 * 4]; + int len = inputStream.read(buffer); + while (len != -1) { + outputStream.write(buffer, 0, len); + len = inputStream.read(buffer); + bytesCopied += len; + final int finalBytesCopied = bytesCopied; + if (lastUpdate < System.currentTimeMillis() - 500) { + runOnUiThread(new Runnable() { + @Override public void run() { + dialog.setMessage("Downloading update... (This may take a while) [" + finalBytesCopied + " bytes downloaded]"); + } + }); + lastUpdate = System.currentTimeMillis(); + } + } + dialog.dismiss(); + if (!background) { + runOnUiThread(new Runnable() { + @Override public void run() { + ActivityAskUpdate.this.dismiss(); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + ActivityAskUpdate.this.getContext().startActivity(intent); + } + }); + } else { + ActivityAskUpdate.this.notifyBackgroundDownloadDone(apkFile); + } + } catch (IOException e) { + Log.e("EHentai", "APK write failed!", e); + e.printStackTrace(); + dialog.dismiss(); + if (!background) { + runOnUiThread(new Runnable() { + @Override public void run() { + new AlertDialog.Builder(ActivityAskUpdate.this.getContext()) + .setTitle("Error!") + .setMessage("Could not write APK to sdcard! Do you have enough space?") + .setNeutralButton("Ok", new OnClickListener() { + @Override + public void onClick(DialogInterface d, int which) { + d.dismiss(); + ActivityAskUpdate.this.dismiss(); + } + }).create().show(); + } + }); + } else { + ActivityAskUpdate.this.notifyBackgroundDownloadFail("Could not write APK to sdcard! Do you have enough space?"); + } + } + } + } + }).start(); + } + + // Prevent dialog dismiss when orientation changes + private static void doKeepDialog(Dialog dialog) { + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(dialog.getWindow().getAttributes()); + lp.width = WindowManager.LayoutParams.WRAP_CONTENT; + lp.height = WindowManager.LayoutParams.WRAP_CONTENT; + dialog.getWindow().setAttributes(lp); + } + + static Handler handler = null; + + public static void runOnUiThread(Runnable r) { + if (handler == null) handler = new Handler(Looper.getMainLooper()); + handler.post(r); + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/ActivityBatchAdd.java b/app/src/main/java/exh/ActivityBatchAdd.java new file mode 100644 index 000000000..1062d84b3 --- /dev/null +++ b/app/src/main/java/exh/ActivityBatchAdd.java @@ -0,0 +1,132 @@ +package exh; + +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.EditText; + +import com.pushtorefresh.storio.sqlite.operations.put.PutResult; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; + +import eu.kanade.tachiyomi.R; +import eu.kanade.tachiyomi.data.database.DatabaseHelper; +import eu.kanade.tachiyomi.data.database.models.Manga; +import eu.kanade.tachiyomi.data.source.online.english.EHentai; +import rx.functions.Action1; + +public class ActivityBatchAdd extends AppCompatActivity { + + DatabaseHelper db; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_activity_batch_add); + + //Inject later (but I don't know how to use this dep-injection library) + db = new DatabaseHelper(this); + + findViewById(R.id.addButton).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { + final EditText textBox = ((EditText) ActivityBatchAdd.this.findViewById(R.id.galleryList)); + String textBoxContent = textBox.getText().toString(); + if (textBoxContent.isEmpty()) { + new AlertDialog.Builder(ActivityBatchAdd.this) + .setTitle("No galleries to add!") + .setMessage("You must specify at least one gallery to add!") + .setPositiveButton("Ok", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .show(); + return; + } + final ProgressDialog progressDialog + = ProgressDialog.show(ActivityBatchAdd.this, "Adding galleries...", "Initializing...", false, false); + final StringJoiner report = new StringJoiner("\n"); + final String[] splitUrls = textBoxContent.split("\n"); + final ArrayList failed = new ArrayList<>(); + progressDialog.setMax(splitUrls.length); + new Thread(new Runnable() { + @Override public void run() { + for (int i = 0; i < splitUrls.length; i++) { + final String trimmed = splitUrls[i].trim(); + final int finalI = i; + ActivityBatchAdd.this.runOnUiThread(new Runnable() { + @Override public void run() { + progressDialog.setMessage("Adding: '" + trimmed + "'... (" + (finalI + 1) + "/" + splitUrls.length + ")"); + progressDialog.setProgress(finalI); + } + }); + try { + if (TextUtils.isEmpty(trimmed)) { + throw new MalformedURLException("Empty URL!"); + } + URL parsedUrl = new URL(trimmed); + int source; + switch (parsedUrl.getHost()) { + case "g.e-hentai.org": + source = 1; + break; + case "exhentai.org": + source = 2; + break; + default: + throw new MalformedURLException("Invalid host!"); + } + final Manga manga = Manga.Companion.create(EHentai.pathOnly(trimmed), source); + manga.setTitle(trimmed); + manga.setFavorite(true); + db.insertManga(manga).asRxObservable().single().forEach(new Action1() { + @Override public void call(PutResult putResult) { + manga.setId(putResult.insertedId()); + } + }); + report.add("Successfully added: " + trimmed); + } catch (MalformedURLException e) { + Log.e("EHentai", "Could not add URL: " + trimmed + "!", e); + report.add("Coult not add: " + trimmed); + failed.add(trimmed); + } + } + if (failed.size() > 0) { + report.add("Failed to add " + failed.size() + " galleries!"); + } + ActivityBatchAdd.this.runOnUiThread(new Runnable() { + @Override public void run() { + if (failed.size() > 0) { + StringJoiner failedJoiner = new StringJoiner("\n"); + for (String failedUrl : failed) + failedJoiner.add(failedUrl); + textBox.setText(failedJoiner.toString()); + } else { + textBox.setText(""); + } + new AlertDialog.Builder(ActivityBatchAdd.this) + .setTitle("Batch Add Report") + .setMessage(report.toString()) + .setPositiveButton("Ok", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog1, int which) { + dialog1.dismiss(); + } + }) + .show(); + } + }); + progressDialog.dismiss(); + } + }).start(); + } + }); + } +} diff --git a/app/src/main/java/exh/ActivityInterceptLink.java b/app/src/main/java/exh/ActivityInterceptLink.java new file mode 100644 index 000000000..cb4272132 --- /dev/null +++ b/app/src/main/java/exh/ActivityInterceptLink.java @@ -0,0 +1,69 @@ +package exh; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.widget.Toast; + +import com.pushtorefresh.storio.sqlite.operations.put.PutResult; + +import java.net.MalformedURLException; +import java.net.URL; + +import eu.kanade.tachiyomi.R; +import eu.kanade.tachiyomi.data.database.DatabaseHelper; +import eu.kanade.tachiyomi.data.database.models.Manga; +import eu.kanade.tachiyomi.data.source.online.english.EHentai; +import eu.kanade.tachiyomi.ui.manga.MangaActivity; +import rx.functions.Action1; + +public class ActivityInterceptLink extends AppCompatActivity { + + DatabaseHelper db; + + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + //Inject later (but I don't know how to use this dep-injection library) + db = new DatabaseHelper(this); + + setContentView(R.layout.activity_intercept); + + final Intent intent = getIntent(); + final String action = intent.getAction(); + + try { + if (Intent.ACTION_VIEW.equals(action)) { + String url = intent.getDataString(); + URL parsedUrl = new URL(url); + int source; + switch (parsedUrl.getHost()) { + case "g.e-hentai.org": + source = 1; + break; + case "exhentai.org": + source = 2; + break; + default: + throw new MalformedURLException("Invalid host!"); + } + final Manga manga = Manga.Companion.create(EHentai.pathOnly(url), source); + manga.setTitle(url); + db.insertManga(manga).asRxObservable().single().forEach(new Action1() { + @Override public void call(PutResult putResult) { + manga.setId(putResult.insertedId()); + Intent outIntent = MangaActivity.Companion.newIntent(ActivityInterceptLink.this, manga, false); + ActivityInterceptLink.this.startActivity(outIntent); + } + }); + } else { + throw new IllegalArgumentException("Invalid action!"); + } + } catch (Exception e) { + Log.e("EHentai", "Error intercepting URL!", e); + Toast.makeText(this, "Invalid URL!", Toast.LENGTH_SHORT).show(); + } + finish(); + } +} diff --git a/app/src/main/java/exh/ActivityPE.java b/app/src/main/java/exh/ActivityPE.java new file mode 100644 index 000000000..de47a871a --- /dev/null +++ b/app/src/main/java/exh/ActivityPE.java @@ -0,0 +1,132 @@ +package exh; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.Map; + +import eu.kanade.tachiyomi.R; + +public class ActivityPE extends Activity { + + ListView listView; + ArrayAdapter listAdapter; + SharedPreferences preferences; + + void updateList() { + listAdapter.clear(); + listAdapter.addAll(preferences.getAll().entrySet()); + listView.deferNotifyDataSetChanged(); + listView.invalidate(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final ActivityPE instance = this; + + setContentView(R.layout.activity_pe); + + preferences = PreferenceManager.getDefaultSharedPreferences(this); + + listView = (ListView) findViewById(R.id.peList); + listAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new ArrayList(preferences.getAll().entrySet())); + listView.setAdapter(listAdapter); + listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + final Map.Entry entry = (Map.Entry) listAdapter.getItem(position); + new AlertDialog.Builder(instance) + .setTitle("Delete Preference Entry") + .setMessage("Delete '" + entry.getKey() + "'?") + .setPositiveButton("Delete", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { + preferences.edit().remove(entry.getKey()).commit(); + ActivityPE.this.updateList(); + dialog.dismiss(); + } + }) + .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }).show(); + return true; + } + }); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final Map.Entry entry = (Map.Entry) listAdapter.getItem(position); + if (entry != null) { + LinearLayout view1 = new LinearLayout(instance); + view1.setOrientation(LinearLayout.VERTICAL); + EditText keyView = new EditText(instance); + keyView.setHint("Key"); + keyView.setText(entry.getKey()); + keyView.setEnabled(false); + final EditText valueView = new EditText(instance); + valueView.setHint("Value"); + valueView.setText(entry.getValue().toString()); + view1.addView(keyView); + view1.addView(valueView); + new AlertDialog.Builder(instance) + .setTitle("Edit Entry") + .setView(view1) + .setPositiveButton("Apply", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { + Object object = entry.getValue(); + String key = entry.getKey(); + String value = valueView.getText().toString(); + SharedPreferences.Editor editor = preferences.edit(); + try { + if (object instanceof Boolean) { + editor.putBoolean(key, Boolean.parseBoolean(value)); + } else if (object instanceof Integer) { + editor.putInt(key, Integer.parseInt(value)); + } else if (object instanceof String) { + editor.putString(key, value); + } else if (object instanceof Float) { + editor.putFloat(key, Float.parseFloat(value)); + } else if (object instanceof Long) { + editor.putLong(key, Long.parseLong(value)); + } else { + new AlertDialog.Builder(instance) + .setTitle("Error") + .setMessage("Unsupported type!") + .show(); + } + } catch (Exception e) { + new AlertDialog.Builder(instance) + .setTitle("Error") + .setMessage("Type mismatch!") + .show(); + } + editor.commit(); + ActivityPE.this.updateList(); + dialog.dismiss(); + } + }) + .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .show(); + } + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/CheckUpdatePref.java b/app/src/main/java/exh/CheckUpdatePref.java new file mode 100644 index 000000000..19697d9a0 --- /dev/null +++ b/app/src/main/java/exh/CheckUpdatePref.java @@ -0,0 +1,31 @@ +package exh; + +import android.content.Context; +import android.support.v7.preference.Preference; +import android.util.AttributeSet; + +//Performs an update check on press +public class CheckUpdatePref extends Preference { + + public CheckUpdatePref(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CheckUpdatePref(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CheckUpdatePref(Context context) { + super(context); + } + + @Override + protected void onClick() { + super.onClick(); + new Thread(new Runnable() { + @Override public void run() { + ActivityAskUpdate.checkAndDoUpdateIfNeeded(CheckUpdatePref.this.getContext(), false); + } + }).start(); + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/DialogLogin.java b/app/src/main/java/exh/DialogLogin.java new file mode 100644 index 000000000..cc263c8d0 --- /dev/null +++ b/app/src/main/java/exh/DialogLogin.java @@ -0,0 +1,251 @@ +package exh; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatDialog; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.webkit.CookieManager; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Toast; + +import java.io.IOException; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.URI; +import java.util.concurrent.locks.ReentrantLock; + +import eu.kanade.tachiyomi.R; +import okhttp3.Request; +import okhttp3.Response; + +public class DialogLogin extends AppCompatDialog { + + public static ReentrantLock DIALOG_LOCK = new ReentrantLock(); + + public DialogLogin(Context context) { + super(context); + setup(); + } + + public DialogLogin(Context context, int theme) { + super(context, theme); + setup(); + } + + protected DialogLogin(Context context, boolean cancelable, OnCancelListener cancelListener) { + super(context, cancelable, cancelListener); + setup(); + } + + void setup() { + setOnDismissListener(new OnDismissListener() { + @Override public void onDismiss(DialogInterface dialog) { + DIALOG_LOCK.unlock(); + } + }); + setCancelable(false); + setTitle("ExHentai Log-In"); + } + + /** + * Requests a login. + *

+ * NOTE: THIS METHOD BLOCKS, DO NOT CALL FROM UI THREAD! + */ + public static void requestLogin(final Context context) { + if (!DIALOG_LOCK.isLocked()) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override public void run() { + DialogLogin dialog = new DialogLogin(context); + dialog.show(); + doKeepDialog(dialog); + } + }); + //Wait for the dialog to lock + while (!DIALOG_LOCK.isLocked()) { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + } + //Wait for unlock + DIALOG_LOCK.lock(); + DIALOG_LOCK.unlock(); + } else { + Log.w("EHentai", "Login box lock held, waiting until unlocked..."); + DIALOG_LOCK.lock(); + DIALOG_LOCK.unlock(); + } + } + + public static boolean isLoggedIn(final Context context, boolean useWeb) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String ehCookieString = prefs.getString("eh_cookie_string", ""); + if (ehCookieString.startsWith("ipb_member_id")) { + if (useWeb) { + //Perform further verification + Request request = new Request.Builder().url("http://exhentai.org/img/b.png").header("Cookie", ehCookieString).build(); + Response response; + try { + response = NetworkManager.getInstance().getClient().newCall(request).execute(); + } catch (IOException e) { + Log.e("EHentai", "Exception contacting ExHentai!", e); + return false; + } + return response.isSuccessful(); + } else { + return true; + } + } else { + return false; + } + } + + // Prevent dialog dismiss when orientation changes + private static void doKeepDialog(Dialog dialog) { + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(dialog.getWindow().getAttributes()); + lp.width = WindowManager.LayoutParams.WRAP_CONTENT; + lp.height = WindowManager.LayoutParams.WRAP_CONTENT; + dialog.getWindow().setAttributes(lp); + } + + @SuppressLint("SetJavaScriptEnabled") + @Override + protected void onCreate(Bundle savedInstanceState) { + DIALOG_LOCK.lock(); + setContentView(R.layout.activity_dialog_login); + getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + final WebView wv = (WebView) findViewById(R.id.webView); + final DialogLogin instance = this; + findViewById(R.id.btnCancel).setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { + instance.dismiss(); + } + }); + findViewById(R.id.btnAdvanced).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + wv.loadUrl("http://exhentai.org/"); + } + }); + super.onCreate(savedInstanceState); + wv.getSettings().setJavaScriptEnabled(true); + wv.getSettings().setDomStorageEnabled(true); + wv.loadUrl("https://forums.e-hentai.org/index.php?act=Login"); + wv.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + view.loadUrl(url); + + return true; + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + + Log.i("EHentai", "Webview loaded: " + url); + + if (url.equals("https://forums.e-hentai.org/index.php?act=Login")) { + //Hide distracting content + view.loadUrl("javascript:(function () {document.getElementsByTagName('body')[0].style.visibility='hidden';" + + "document.getElementsByName('submit')[0].style.visibility='visible';" + + "document.querySelectorAll('td[width=\"60%\"][valign=\"top\"]')[0].style.visibility='visible';})()"); + } else if (url.startsWith("https://forums.e-hentai.org/index.php") + && url.contains("act=Login") + && url.contains("CODE=01") + || url.equals("https://forums.e-hentai.org/index.php?") + || url.equals("https://forums.e-hentai.org/index.php")) { + String cookies = CookieManager.getInstance().getCookie(url); + String[] cookieSplit = cookies.split(";"); + String memberID = null; + String passHash = null; + CookieStore cookieStore = NetworkManager.getInstance().getCookieManager().getCookieStore(); + URI uri = URI.create(url); + for (String cookie : cookieSplit) { + String trimmedCookie = cookie.trim(); + int equalIndex = trimmedCookie.indexOf("="); + String key = trimmedCookie.substring(0, equalIndex); + String value = trimmedCookie.substring(equalIndex + 1).replace("\"", ""); + HttpCookie newCookie = new HttpCookie(key, value); + newCookie.setDomain(".e-hentai.org"); + + cookieStore.add(uri, newCookie); + + if (key.equals("ipb_member_id")) { + memberID = value; + } else if (key.equals("ipb_pass_hash")) { + passHash = value; + } + } + + if (memberID == null || passHash == null) { + Toast.makeText(instance.getContext(), "Invalid login or captcha invalid, please try again!", Toast.LENGTH_SHORT).show(); + wv.loadUrl("https://forums.e-hentai.org/index.php?act=Login"); + } else { + Log.i("EHentai", "Login OK, accessing ExHentai..."); + wv.loadUrl("http://exhentai.org/"); + } + } else if (url.startsWith("http://exhentai.org") || url.startsWith("https://exhentai.org")) { + String cookies = CookieManager.getInstance().getCookie(url); + if(cookies == null) cookies = ""; + String[] cookieSplit = cookies.split(";"); + String memberID = null; + String passHash = null; + String igneous = null; + CookieStore cookieStore = NetworkManager.getInstance().getCookieManager().getCookieStore(); + URI uri = URI.create(url); + for (String cookie : cookieSplit) { + String trimmedCookie = cookie.trim(); + if(trimmedCookie.isEmpty()) { + continue; + } + int equalIndex = trimmedCookie.indexOf("="); + if(equalIndex == -1) continue; + String key = trimmedCookie.substring(0, equalIndex); + String value = trimmedCookie.substring(equalIndex + 1).replace("\"", ""); + HttpCookie newCookie = new HttpCookie(key, value); + newCookie.setDomain(".e-hentai.org"); + + cookieStore.add(uri, newCookie); + + switch (key) { + case "ipb_member_id": + memberID = value; + break; + case "ipb_pass_hash": + passHash = value; + break; + case "igneous": + igneous = value; + break; + } + } + + if (memberID != null && passHash != null && igneous != null) { + Log.i("EHentai", "@ ExHentai and cookies are set, finalizing login..."); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(instance.getContext()); + preferences.edit().putString("eh_cookie_string", "ipb_member_id=" + memberID + "; ipb_pass_hash=" + passHash + "; igneous=" + igneous + "; ").commit(); + instance.dismiss(); + } else { + Log.i("EHentai", "@ ExHentai but cookies not fully set, waiting..."); + } + } + } + } + + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/ExHentaiLoginPref.java b/app/src/main/java/exh/ExHentaiLoginPref.java new file mode 100644 index 000000000..7f4b261ee --- /dev/null +++ b/app/src/main/java/exh/ExHentaiLoginPref.java @@ -0,0 +1,115 @@ +package exh; + +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.support.v7.app.AlertDialog; +import android.support.v7.preference.Preference; +import android.support.v7.preference.SwitchPreferenceCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.WindowManager; +import android.widget.Toast; + +import com.jakewharton.processphoenix.ProcessPhoenix; + +import eu.kanade.tachiyomi.data.source.online.english.EHentai; +import eu.kanade.tachiyomi.ui.main.MainActivity; + +public class ExHentaiLoginPref extends SwitchPreferenceCompat { + + public ExHentaiLoginPref(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setup(); + } + + public ExHentaiLoginPref(Context context, AttributeSet attrs) { + super(context, attrs); + setup(); + } + + public ExHentaiLoginPref(Context context) { + super(context); + setup(); + } + + void setup() { + enableListeners(); + } + + void disableListeners() { + setOnPreferenceChangeListener(null); + } + + void forceAppRestart() { + AlertDialog dialog = new AlertDialog.Builder(getContext()) + .setTitle("App Restart Required") + .setMessage("An app restart is required to apply changes. Press the 'RESTART' button to restart the application now.") + .setCancelable(false) + .setPositiveButton("Restart", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + ProgressDialog progressDialog = ProgressDialog.show(getContext(), "Restarting App", "Please wait...", true, false); + doKeepDialog(progressDialog); + Intent intent = new Intent(getContext(), MainActivity.class); + ProcessPhoenix.triggerRebirth(getContext(), intent); + } + }).show(); + + doKeepDialog(dialog); + } + + private static void doKeepDialog(Dialog dialog){ + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(dialog.getWindow().getAttributes()); + lp.width = WindowManager.LayoutParams.WRAP_CONTENT; + lp.height = WindowManager.LayoutParams.WRAP_CONTENT; + dialog.getWindow().setAttributes(lp); + } + + void enableListeners() { + setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override public boolean onPreferenceChange(Preference preference, Object newValue) { + final Context context = ExHentaiLoginPref.this.getContext(); + + if ((Boolean) newValue) { + EHentai.performLogout(context); + new Thread(new Runnable() { + @Override public void run() { + DialogLogin.requestLogin(context); + final boolean isLoggedIn = DialogLogin.isLoggedIn(context, true); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override public void run() { + if (isLoggedIn) { + ExHentaiLoginPref.this.quietSetChecked(true); + forceAppRestart(); + } else { + Toast.makeText(context, "Login failed, please try again!", Toast.LENGTH_LONG).show(); + } + } + }); + + } + }).start(); + return false; + } else { + EHentai.performLogout(context); + forceAppRestart(); + return true; + } + } + }); + } + + void quietSetChecked(boolean checked) { + disableListeners(); + Log.i("EHentai", "Setting checked..."); + setChecked(checked); + enableListeners(); + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/FavoritesSyncManager.java b/app/src/main/java/exh/FavoritesSyncManager.java new file mode 100644 index 000000000..e56816e00 --- /dev/null +++ b/app/src/main/java/exh/FavoritesSyncManager.java @@ -0,0 +1,190 @@ +package exh; + +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Handler; +import android.os.Looper; +import android.support.v7.app.AlertDialog; + +import com.pushtorefresh.storio.sqlite.operations.put.PutResult; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import eu.kanade.tachiyomi.data.database.DatabaseHelper; +import eu.kanade.tachiyomi.data.database.models.Category; +import eu.kanade.tachiyomi.data.database.models.Manga; +import eu.kanade.tachiyomi.data.database.models.MangaCategory; +import eu.kanade.tachiyomi.data.source.online.english.EHentai; + +public class FavoritesSyncManager { + Context context; + DatabaseHelper db; + + public FavoritesSyncManager(Context context, DatabaseHelper db) { + this.context = context; + this.db = db; + } + + public void guiSyncFavorites(final Runnable onComplete) { + if(!DialogLogin.isLoggedIn(context, false)) { + new AlertDialog.Builder(context).setTitle("Error") + .setMessage("You are not logged in! Please log in and try again!") + .setPositiveButton("Ok", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }).show(); + return; + } + final ProgressDialog dialog = ProgressDialog.show(context, "Downloading Favorites", "Please wait...", true, false); + new Thread(new Runnable() { + @Override + public void run() { + Handler mainLooper = new Handler(Looper.getMainLooper()); + try { + syncFavorites(); + } catch (Exception e) { + mainLooper.post(new Runnable() { + @Override + public void run() { + new AlertDialog.Builder(context) + .setTitle("Error") + .setMessage("There was an error downloading your favorites, please try again later!") + .setPositiveButton("Ok", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }).show(); + } + }); + e.printStackTrace(); + } + dialog.dismiss(); + mainLooper.post(onComplete); + } + }).start(); + } + + public void syncFavorites() throws IOException { + EHentai.FavoritesResponse favResponse = EHentai.fetchFavorites(context); + Map> favorites = favResponse.favs; + List ourCategories = new ArrayList<>(db.getCategories().executeAsBlocking()); + List ourMangas = new ArrayList<>(db.getMangas().executeAsBlocking()); + //Add required categories (categories do not sync upwards) + List categoriesToInsert = new ArrayList<>(); + for (String theirCategory : favorites.keySet()) { + boolean haveCategory = false; + for (Category category : ourCategories) { + if (category.getName().endsWith(theirCategory)) { + haveCategory = true; + } + } + if (!haveCategory) { + Category category = Category.Companion.create(theirCategory); + ourCategories.add(category); + categoriesToInsert.add(category); + } + } + if (!categoriesToInsert.isEmpty()) { + for(Map.Entry result : db.insertCategories(categoriesToInsert).executeAsBlocking().results().entrySet()) { + if(result.getValue().wasInserted()) { + result.getKey().setId(result.getValue().insertedId().intValue()); + } + } + } + //Build category map + Map categoryMap = new HashMap<>(); + for (Category category : ourCategories) { + categoryMap.put(category.getName(), category); + } + //Insert new mangas + List mangaToInsert = new ArrayList<>(); + Map mangaToSetCategories = new HashMap<>(); + for (Map.Entry> entry : favorites.entrySet()) { + Category category = categoryMap.get(entry.getKey()); + for (Manga manga : entry.getValue()) { + boolean alreadyHaveManga = false; + for (Manga ourManga : ourMangas) { + if (ourManga.getUrl().equals(manga.getUrl())) { + alreadyHaveManga = true; + manga = ourManga; + break; + } + } + if (!alreadyHaveManga) { + ourMangas.add(manga); + mangaToInsert.add(manga); + } + mangaToSetCategories.put(manga, category); + manga.setFavorite(true); + } + } + for (Map.Entry results : db.insertMangas(mangaToInsert).executeAsBlocking().results().entrySet()) { + if(results.getValue().wasInserted()) { + results.getKey().setId(results.getValue().insertedId()); + } + } + for(Map.Entry entry : mangaToSetCategories.entrySet()) { + db.setMangaCategories(Collections.singletonList(MangaCategory.Companion.create(entry.getKey(), entry.getValue())), + Collections.singletonList(entry.getKey())); + } + //Determines what + /*Map> toUpload = new HashMap<>(); + for (Manga manga : ourMangas) { + if(manga.getFavorite()) { + boolean remoteHasManga = false; + for (List remoteMangas : favorites.values()) { + for (Manga remoteManga : remoteMangas) { + if (remoteManga.getUrl().equals(manga.getUrl())) { + remoteHasManga = true; + break; + } + } + } + if (!remoteHasManga) { + List mangaCategories = db.getCategoriesForManga(manga).executeAsBlocking(); + for (Category category : mangaCategories) { + int categoryIndex = favResponse.favCategories.indexOf(category.getName()); + if (categoryIndex >= 0) { + List uploadMangas = toUpload.get(categoryIndex); + if (uploadMangas == null) { + uploadMangas = new ArrayList<>(); + toUpload.put(categoryIndex, uploadMangas); + } + uploadMangas.add(manga); + } + } + } + } + }*/ + /********** NON-FUNCTIONAL, modifygids[] CANNOT ADD NEW FAVORITES! (or as of my testing it can't, maybe I'll do more testing)**/ + /*PreferencesHelper helper = new PreferencesHelper(context); + for(Map.Entry> entry : toUpload.entrySet()) { + FormBody.Builder formBody = new FormBody.Builder() + .add("ddact", "fav" + entry.getKey()); + for(Manga manga : entry.getValue()) { + List splitUrl = new ArrayList<>(Arrays.asList(manga.getUrl().split("/"))); + splitUrl.removeAll(Collections.singleton("")); + if(splitUrl.size() < 2) { + continue; + } + formBody.add("modifygids[]", splitUrl.get(1).trim()); + } + formBody.add("apply", "Apply"); + Request request = RequestsKt.POST(EHentai.buildFavoritesBase(context, helper.getPrefs()).favoritesBase, + EHentai.getHeadersBuilder(helper).build(), + formBody.build(), + RequestsKt.getDEFAULT_CACHE_CONTROL()); + Response response = NetworkManager.getInstance().getClient().newCall(request).execute(); + Util.d("EHentai", response.body().string()); + }*/ + } +} diff --git a/app/src/main/java/exh/NetworkManager.java b/app/src/main/java/exh/NetworkManager.java new file mode 100644 index 000000000..ee868be71 --- /dev/null +++ b/app/src/main/java/exh/NetworkManager.java @@ -0,0 +1,30 @@ +package exh; + +import java.net.CookieManager; + +import okhttp3.OkHttpClient; + +public class NetworkManager { + public static NetworkManager INSTANCE = null; + public static NetworkManager getInstance() { + if(INSTANCE == null) INSTANCE = new NetworkManager(); + return INSTANCE; + } + + public NetworkManager() {} + + OkHttpClient httpClient = new OkHttpClient(); + private CookieManager cookieManager = new CookieManager(); + + public OkHttpClient getClient() { + return getHttpClient(); + } + + public OkHttpClient getHttpClient() { + return httpClient; + } + + public CookieManager getCookieManager() { + return cookieManager; + } +} diff --git a/app/src/main/java/exh/Objects.java b/app/src/main/java/exh/Objects.java new file mode 100644 index 000000000..7bd8cde7f --- /dev/null +++ b/app/src/main/java/exh/Objects.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package exh; + +import java.util.Arrays; +import java.util.Comparator; + +/** + * Utility methods for objects. + * @since 1.7 + */ +public final class Objects { + private Objects() {} + + /** + * Returns 0 if {@code a == b}, or {@code c.compare(a, b)} otherwise. + * That is, this makes {@code c} null-safe. + */ + public static int compare(T a, T b, Comparator c) { + if (a == b) { + return 0; + } + return c.compare(a, b); + } + + /** + * Returns true if both arguments are null, + * the result of {@link Arrays#equals} if both arguments are primitive arrays, + * the result of {@link Arrays#deepEquals} if both arguments are arrays of reference types, + * and the result of {@link #equals} otherwise. + */ + public static boolean deepEquals(Object a, Object b) { + if (a == null || b == null) { + return a == b; + } else if (a instanceof Object[] && b instanceof Object[]) { + return Arrays.deepEquals((Object[]) a, (Object[]) b); + } else if (a instanceof boolean[] && b instanceof boolean[]) { + return Arrays.equals((boolean[]) a, (boolean[]) b); + } else if (a instanceof byte[] && b instanceof byte[]) { + return Arrays.equals((byte[]) a, (byte[]) b); + } else if (a instanceof char[] && b instanceof char[]) { + return Arrays.equals((char[]) a, (char[]) b); + } else if (a instanceof double[] && b instanceof double[]) { + return Arrays.equals((double[]) a, (double[]) b); + } else if (a instanceof float[] && b instanceof float[]) { + return Arrays.equals((float[]) a, (float[]) b); + } else if (a instanceof int[] && b instanceof int[]) { + return Arrays.equals((int[]) a, (int[]) b); + } else if (a instanceof long[] && b instanceof long[]) { + return Arrays.equals((long[]) a, (long[]) b); + } else if (a instanceof short[] && b instanceof short[]) { + return Arrays.equals((short[]) a, (short[]) b); + } + return a.equals(b); + } + + /** + * Null-safe equivalent of {@code a.equals(b)}. + */ + public static boolean equals(Object a, Object b) { + return (a == null) ? (b == null) : a.equals(b); + } + + /** + * Convenience wrapper for {@link Arrays#hashCode}, adding varargs. + * This can be used to compute a hash code for an object's fields as follows: + * {@code Objects.hash(a, b, c)}. + */ + public static int hash(Object... values) { + return Arrays.hashCode(values); + } + + /** + * Returns 0 for null or {@code o.hashCode()}. + */ + public static int hashCode(Object o) { + return (o == null) ? 0 : o.hashCode(); + } + + /** + * Returns {@code o} if non-null, or throws {@code NullPointerException}. + */ + public static T requireNonNull(T o) { + if (o == null) { + throw new NullPointerException(); + } + return o; + } + + /** + * Returns {@code o} if non-null, or throws {@code NullPointerException} + * with the given detail message. + */ + public static T requireNonNull(T o, String message) { + if (o == null) { + throw new NullPointerException(message); + } + return o; + } + + /** + * Returns "null" for null or {@code o.toString()}. + */ + public static String toString(Object o) { + return (o == null) ? "null" : o.toString(); + } + + /** + * Returns {@code nullString} for null or {@code o.toString()}. + */ + public static String toString(Object o, String nullString) { + return (o == null) ? nullString : o.toString(); + } +} diff --git a/app/src/main/java/exh/OpenPEPref.java b/app/src/main/java/exh/OpenPEPref.java new file mode 100644 index 000000000..11ce924dd --- /dev/null +++ b/app/src/main/java/exh/OpenPEPref.java @@ -0,0 +1,29 @@ +package exh; + +import android.content.Context; +import android.content.Intent; +import android.support.v7.preference.Preference; +import android.util.AttributeSet; + +public class OpenPEPref extends Preference { + + public OpenPEPref(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public OpenPEPref(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public OpenPEPref(Context context) { + super(context); + } + + @Override + protected void onClick() { + super.onClick(); + + Intent openPeIntent = new Intent(this.getContext(), ActivityPE.class); + this.getContext().startActivity(openPeIntent); + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/StringJoiner.java b/app/src/main/java/exh/StringJoiner.java new file mode 100644 index 000000000..b16b8742f --- /dev/null +++ b/app/src/main/java/exh/StringJoiner.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package exh; + +/** + * {@code StringJoiner} is used to construct a sequence of characters separated + * by a delimiter and optionally starting with a supplied prefix + * and ending with a supplied suffix. + *

+ * Prior to adding something to the {@code StringJoiner}, its + * {@code sj.toString()} method will, by default, return {@code prefix + suffix}. + * However, if the {@code setEmptyValue} method is called, the {@code emptyValue} + * supplied will be returned instead. This can be used, for example, when + * creating a string using set notation to indicate an empty set, i.e. + * "{}", where the {@code prefix} is "{", the + * {@code suffix} is "}" and nothing has been added to the + * {@code StringJoiner}. + * + * @apiNote + *

The String {@code "[George:Sally:Fred]"} may be constructed as follows: + * + *

 {@code
+ * StringJoiner sj = new StringJoiner(":", "[", "]");
+ * sj.add("George").add("Sally").add("Fred");
+ * String desiredString = sj.toString();
+ * }
+ *

+ * A {@code StringJoiner} may be employed to create formatted output from a + * {@link java.util.stream.Stream} using + * {@link java.util.stream.Collectors#joining(CharSequence)}. For example: + * + *

 {@code
+ * List numbers = Arrays.asList(1, 2, 3, 4);
+ * String commaSeparatedNumbers = numbers.stream()
+ *     .map(i -> i.toString())
+ *     .collect(Collectors.joining(", "));
+ * }
+ * + * @see java.util.stream.Collectors#joining(CharSequence) + * @see java.util.stream.Collectors#joining(CharSequence, CharSequence, CharSequence) + * @since 1.8 +*/ +public final class StringJoiner { + private final String prefix; + private final String delimiter; + private final String suffix; + + /* + * StringBuilder value -- at any time, the characters constructed from the + * prefix, the added element separated by the delimiter, but without the + * suffix, so that we can more easily add elements without having to jigger + * the suffix each time. + */ + private StringBuilder value; + + /* + * By default, the string consisting of prefix+suffix, returned by + * toString(), or properties of value, when no elements have yet been added, + * i.e. when it is empty. This may be overridden by the user to be some + * other value including the empty String. + */ + private String emptyValue; + + /** + * Constructs a {@code StringJoiner} with no characters in it, with no + * {@code prefix} or {@code suffix}, and a copy of the supplied + * {@code delimiter}. + * If no characters are added to the {@code StringJoiner} and methods + * accessing the value of it are invoked, it will not return a + * {@code prefix} or {@code suffix} (or properties thereof) in the result, + * unless {@code setEmptyValue} has first been called. + * + * @param delimiter the sequence of characters to be used between each + * element added to the {@code StringJoiner} value + * @throws NullPointerException if {@code delimiter} is {@code null} + */ + public StringJoiner(CharSequence delimiter) { + this(delimiter, "", ""); + } + + /** + * Constructs a {@code StringJoiner} with no characters in it using copies + * of the supplied {@code prefix}, {@code delimiter} and {@code suffix}. + * If no characters are added to the {@code StringJoiner} and methods + * accessing the string value of it are invoked, it will return the + * {@code prefix + suffix} (or properties thereof) in the result, unless + * {@code setEmptyValue} has first been called. + * + * @param delimiter the sequence of characters to be used between each + * element added to the {@code StringJoiner} + * @param prefix the sequence of characters to be used at the beginning + * @param suffix the sequence of characters to be used at the end + * @throws NullPointerException if {@code prefix}, {@code delimiter}, or + * {@code suffix} is {@code null} + */ + public StringJoiner(CharSequence delimiter, + CharSequence prefix, + CharSequence suffix) { + Objects.requireNonNull(prefix, "The prefix must not be null"); + Objects.requireNonNull(delimiter, "The delimiter must not be null"); + Objects.requireNonNull(suffix, "The suffix must not be null"); + // make defensive copies of arguments + this.prefix = prefix.toString(); + this.delimiter = delimiter.toString(); + this.suffix = suffix.toString(); + this.emptyValue = this.prefix + this.suffix; + } + + /** + * Sets the sequence of characters to be used when determining the string + * representation of this {@code StringJoiner} and no elements have been + * added yet, that is, when it is empty. A copy of the {@code emptyValue} + * parameter is made for this purpose. Note that once an add method has been + * called, the {@code StringJoiner} is no longer considered empty, even if + * the element(s) added correspond to the empty {@code String}. + * + * @param emptyValue the characters to return as the value of an empty + * {@code StringJoiner} + * @return this {@code StringJoiner} itself so the calls may be chained + * @throws NullPointerException when the {@code emptyValue} parameter is + * {@code null} + */ + public StringJoiner setEmptyValue(CharSequence emptyValue) { + this.emptyValue = Objects.requireNonNull(emptyValue, + "The empty value must not be null").toString(); + return this; + } + + /** + * Returns the current value, consisting of the {@code prefix}, the values + * added so far separated by the {@code delimiter}, and the {@code suffix}, + * unless no elements have been added in which case, the + * {@code prefix + suffix} or the {@code emptyValue} characters are returned + * + * @return the string representation of this {@code StringJoiner} + */ + @Override + public String toString() { + if (value == null) { + return emptyValue; + } else { + if (suffix.equals("")) { + return value.toString(); + } else { + int initialLength = value.length(); + String result = value.append(suffix).toString(); + // reset value to pre-append initialLength + value.setLength(initialLength); + return result; + } + } + } + + /** + * Adds a copy of the given {@code CharSequence} value as the next + * element of the {@code StringJoiner} value. If {@code newElement} is + * {@code null}, then {@code "null"} is added. + * + * @param newElement The element to add + * @return a reference to this {@code StringJoiner} + */ + public StringJoiner add(CharSequence newElement) { + prepareBuilder().append(newElement); + return this; + } + + /** + * Adds the contents of the given {@code StringJoiner} without prefix and + * suffix as the next element if it is non-empty. If the given {@code + * StringJoiner} is empty, the call has no effect. + * + *

A {@code StringJoiner} is empty if {@link #add(CharSequence) add()} + * has never been called, and if {@code merge()} has never been called + * with a non-empty {@code StringJoiner} argument. + * + *

If the other {@code StringJoiner} is using a different delimiter, + * then elements from the other {@code StringJoiner} are concatenated with + * that delimiter and the result is appended to this {@code StringJoiner} + * as a single element. + * + * @param other The {@code StringJoiner} whose contents should be merged + * into this one + * @throws NullPointerException if the other {@code StringJoiner} is null + * @return This {@code StringJoiner} + */ + public StringJoiner merge(StringJoiner other) { + Objects.requireNonNull(other); + if (other.value != null) { + final int length = other.value.length(); + // lock the length so that we can seize the data to be appended + // before initiate copying to avoid interference, especially when + // merge 'this' + StringBuilder builder = prepareBuilder(); + builder.append(other.value, other.prefix.length(), length); + } + return this; + } + + private StringBuilder prepareBuilder() { + if (value != null) { + value.append(delimiter); + } else { + value = new StringBuilder().append(prefix); + } + return value; + } + + /** + * Returns the length of the {@code String} representation + * of this {@code StringJoiner}. Note that if + * no add methods have been called, then the length of the {@code String} + * representation (either {@code prefix + suffix} or {@code emptyValue}) + * will be returned. The value should be equivalent to + * {@code toString().length()}. + * + * @return the length of the current value of {@code StringJoiner} + */ + public int length() { + // Remember that we never actually append the suffix unless we return + // the full (present) value or some sub-string or length of it, so that + // we can add on more if we need to. + return (value != null ? value.length() + suffix.length() : + emptyValue.length()); + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/Util.java b/app/src/main/java/exh/Util.java new file mode 100644 index 000000000..dcea5adc4 --- /dev/null +++ b/app/src/main/java/exh/Util.java @@ -0,0 +1,17 @@ +package exh; + +/** + * Project: tachiyomi + * Created: 19/04/16 + */ +public class Util { + public static void d(String TAG, String message) { + int maxLogSize = 1000; + for(int i = 0; i <= message.length() / maxLogSize; i++) { + int start = i * maxLogSize; + int end = (i+1) * maxLogSize; + end = end > message.length() ? message.length() : end; + android.util.Log.d(TAG, message.substring(start, end)); + } + } +} diff --git a/app/src/main/res/drawable/ic_cached_white_24dp.xml b/app/src/main/res/drawable/ic_cached_white_24dp.xml new file mode 100644 index 000000000..9cfba0c8d --- /dev/null +++ b/app/src/main/res/drawable/ic_cached_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist_add_black_24dp.xml b/app/src/main/res/drawable/ic_playlist_add_black_24dp.xml new file mode 100644 index 000000000..0460472b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_add_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist_add_grey_24dp.xml b/app/src/main/res/drawable/ic_playlist_add_grey_24dp.xml new file mode 100644 index 000000000..f713462c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_add_grey_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_white_24dp.xml b/app/src/main/res/drawable/ic_share_white_24dp.xml new file mode 100644 index 000000000..034ea67d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_whatshot_black_24dp.xml b/app/src/main/res/drawable/ic_whatshot_black_24dp.xml new file mode 100644 index 000000000..1cbc037f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_whatshot_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_activity_batch_add.xml b/app/src/main/res/layout/activity_activity_batch_add.xml new file mode 100644 index 000000000..443e91763 --- /dev/null +++ b/app/src/main/res/layout/activity_activity_batch_add.xml @@ -0,0 +1,48 @@ + + + + + +