Integrate TachiyomiEH changes.
This commit is contained in:
parent
74e3d387eb
commit
3c43bebe64
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
.gradle
|
||||
.gradle/
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
.DS_Store
|
||||
@ -6,4 +6,5 @@
|
||||
.idea/
|
||||
*iml
|
||||
*.iml
|
||||
*/build
|
||||
*/build
|
||||
/libs/SubsamplingScaleImageView/build
|
9
CHANGELOG.md
Normal file
9
CHANGELOG.md
Normal file
@ -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
|
@ -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'
|
||||
|
@ -2,11 +2,11 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
|
||||
<application
|
||||
@ -21,14 +21,14 @@
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:theme="@style/Theme.BrandedLaunch">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.manga.MangaActivity"
|
||||
android:parentActivityName=".ui.main.MainActivity" >
|
||||
android:parentActivityName=".ui.main.MainActivity">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.reader.ReaderActivity"
|
||||
@ -37,7 +37,7 @@
|
||||
<activity
|
||||
android:name=".ui.setting.SettingsActivity"
|
||||
android:label="@string/label_settings"
|
||||
android:parentActivityName=".ui.main.MainActivity" >
|
||||
android:parentActivityName=".ui.main.MainActivity">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.category.CategoryActivity"
|
||||
@ -104,6 +104,39 @@
|
||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||
android:value="GlideModule" />
|
||||
|
||||
<activity
|
||||
android:name="exh.ActivityPE"
|
||||
android:label="Advanced Preferences">
|
||||
</activity>
|
||||
<activity
|
||||
android:name="exh.ActivityInterceptLink"
|
||||
android:label="TachiyomiEH">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data
|
||||
android:host="g.e-hentai.org"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="http"/>
|
||||
<data
|
||||
android:host="g.e-hentai.org"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="https"/>
|
||||
<data
|
||||
android:host="exhentai.org"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="http"/>
|
||||
<data
|
||||
android:host="exhentai.org"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="https"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="exh.ActivityBatchAdd">
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -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<MangaSyncService>()
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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), "")
|
||||
|
||||
|
@ -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)
|
||||
fun getLanguages() = listOf(ALL)
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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<String> 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<String> genreSet = new HashSet<>();
|
||||
genreSet.addAll(ENABLED_GENRES);
|
||||
helper.getPrefs().edit().putStringSet(KEY_GENRE_FILTER, genreSet).commit();
|
||||
}
|
||||
|
||||
public static void loadGenreFilter(PreferencesHelper helper) {
|
||||
Set<String> 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<String> 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<Manga> 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<String, List<Manga>> favs;
|
||||
public List<String> favCategories;
|
||||
|
||||
public FavoritesResponse(Map<String, List<Manga>> favs, List<String> 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<String> 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<String, String> 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<String, List<Manga>> mangas;
|
||||
}
|
||||
|
||||
public static ParsedMangaPage parseMangaPage(Response response, int id) {
|
||||
ParsedMangaPage mangaPage = new ParsedMangaPage();
|
||||
Map<String, List<Manga>> 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<Manga> 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<Manga> 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<Chapter> 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<String> 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<Page> 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<Page> pages) {
|
||||
ArrayList<String> 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<String, String> foundCookies = getCookies(cookies);
|
||||
if(foundCookies == null) {
|
||||
return cookies;
|
||||
}
|
||||
StringJoiner cookieJoiner = new StringJoiner("; ");
|
||||
for(Map.Entry<String, String> cookie : foundCookies.entrySet()) {
|
||||
cookieJoiner.add(cookie.getKey() + "=" + cookie.getValue());
|
||||
}
|
||||
return cookieJoiner.toString();
|
||||
}
|
||||
|
||||
public static Map<String, String> getCookies(String cookies) {
|
||||
Map<String, String> 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;
|
||||
}
|
||||
}
|
@ -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<CataloguePresenter>(), FlexibleViewHold
|
||||
/**
|
||||
* Query of the search box.
|
||||
*/
|
||||
private val query: String?
|
||||
val query: String?
|
||||
get() = presenter.query
|
||||
|
||||
/**
|
||||
@ -212,12 +213,12 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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()
|
||||
|
@ -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<CatalogueFragment>() {
|
||||
|
||||
// Ensure at least one language
|
||||
if (languages.isEmpty()) {
|
||||
languages.add(EN.code)
|
||||
languages.add(ALL.code)
|
||||
}
|
||||
|
||||
return sourceManager.getOnlineSources()
|
||||
|
@ -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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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
|
||||
|
@ -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()
|
||||
|
@ -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<MangaPresenter>() {
|
||||
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<MangaPresenter>() {
|
||||
|
||||
init {
|
||||
pageCount = 2
|
||||
if (!activity.fromCatalogue && activity.presenter.syncManager.myAnimeList.isLogged)
|
||||
pageCount++
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
@ -91,7 +87,6 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
||||
when (position) {
|
||||
INFO_FRAGMENT -> return MangaInfoFragment.newInstance()
|
||||
CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance()
|
||||
MYANIMELIST_FRAGMENT -> return MyAnimeListFragment.newInstance()
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
|
||||
|
377
app/src/main/java/exh/ActivityAskUpdate.java
Normal file
377
app/src/main/java/exh/ActivityAskUpdate.java
Normal file
@ -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("<Tachiyomi E-Hentai Update File>")) 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);
|
||||
}
|
||||
}
|
132
app/src/main/java/exh/ActivityBatchAdd.java
Normal file
132
app/src/main/java/exh/ActivityBatchAdd.java
Normal file
@ -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<String> 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<PutResult>() {
|
||||
@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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
69
app/src/main/java/exh/ActivityInterceptLink.java
Normal file
69
app/src/main/java/exh/ActivityInterceptLink.java
Normal file
@ -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<PutResult>() {
|
||||
@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();
|
||||
}
|
||||
}
|
132
app/src/main/java/exh/ActivityPE.java
Normal file
132
app/src/main/java/exh/ActivityPE.java
Normal file
@ -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<String, ?> entry = (Map.Entry<String, ?>) 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<String, ?> entry = (Map.Entry<String, ?>) 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
31
app/src/main/java/exh/CheckUpdatePref.java
Normal file
31
app/src/main/java/exh/CheckUpdatePref.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
251
app/src/main/java/exh/DialogLogin.java
Normal file
251
app/src/main/java/exh/DialogLogin.java
Normal file
@ -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.
|
||||
* <p/>
|
||||
* 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...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
}
|
115
app/src/main/java/exh/ExHentaiLoginPref.java
Normal file
115
app/src/main/java/exh/ExHentaiLoginPref.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
190
app/src/main/java/exh/FavoritesSyncManager.java
Normal file
190
app/src/main/java/exh/FavoritesSyncManager.java
Normal file
@ -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<String, List<Manga>> favorites = favResponse.favs;
|
||||
List<Category> ourCategories = new ArrayList<>(db.getCategories().executeAsBlocking());
|
||||
List<Manga> ourMangas = new ArrayList<>(db.getMangas().executeAsBlocking());
|
||||
//Add required categories (categories do not sync upwards)
|
||||
List<Category> 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<Category, PutResult> result : db.insertCategories(categoriesToInsert).executeAsBlocking().results().entrySet()) {
|
||||
if(result.getValue().wasInserted()) {
|
||||
result.getKey().setId(result.getValue().insertedId().intValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
//Build category map
|
||||
Map<String, Category> categoryMap = new HashMap<>();
|
||||
for (Category category : ourCategories) {
|
||||
categoryMap.put(category.getName(), category);
|
||||
}
|
||||
//Insert new mangas
|
||||
List<Manga> mangaToInsert = new ArrayList<>();
|
||||
Map<Manga, Category> mangaToSetCategories = new HashMap<>();
|
||||
for (Map.Entry<String, List<Manga>> 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<Manga, PutResult> results : db.insertMangas(mangaToInsert).executeAsBlocking().results().entrySet()) {
|
||||
if(results.getValue().wasInserted()) {
|
||||
results.getKey().setId(results.getValue().insertedId());
|
||||
}
|
||||
}
|
||||
for(Map.Entry<Manga, Category> entry : mangaToSetCategories.entrySet()) {
|
||||
db.setMangaCategories(Collections.singletonList(MangaCategory.Companion.create(entry.getKey(), entry.getValue())),
|
||||
Collections.singletonList(entry.getKey()));
|
||||
}
|
||||
//Determines what
|
||||
/*Map<Integer, List<Manga>> toUpload = new HashMap<>();
|
||||
for (Manga manga : ourMangas) {
|
||||
if(manga.getFavorite()) {
|
||||
boolean remoteHasManga = false;
|
||||
for (List<Manga> remoteMangas : favorites.values()) {
|
||||
for (Manga remoteManga : remoteMangas) {
|
||||
if (remoteManga.getUrl().equals(manga.getUrl())) {
|
||||
remoteHasManga = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!remoteHasManga) {
|
||||
List<Category> mangaCategories = db.getCategoriesForManga(manga).executeAsBlocking();
|
||||
for (Category category : mangaCategories) {
|
||||
int categoryIndex = favResponse.favCategories.indexOf(category.getName());
|
||||
if (categoryIndex >= 0) {
|
||||
List<Manga> 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<Integer, List<Manga>> entry : toUpload.entrySet()) {
|
||||
FormBody.Builder formBody = new FormBody.Builder()
|
||||
.add("ddact", "fav" + entry.getKey());
|
||||
for(Manga manga : entry.getValue()) {
|
||||
List<String> 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());
|
||||
}*/
|
||||
}
|
||||
}
|
30
app/src/main/java/exh/NetworkManager.java
Normal file
30
app/src/main/java/exh/NetworkManager.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
128
app/src/main/java/exh/Objects.java
Normal file
128
app/src/main/java/exh/Objects.java
Normal file
@ -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 <T> int compare(T a, T b, Comparator<? super T> 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> 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> 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();
|
||||
}
|
||||
}
|
29
app/src/main/java/exh/OpenPEPref.java
Normal file
29
app/src/main/java/exh/OpenPEPref.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
247
app/src/main/java/exh/StringJoiner.java
Normal file
247
app/src/main/java/exh/StringJoiner.java
Normal file
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <code>"{}"</code>, where the {@code prefix} is <code>"{"</code>, the
|
||||
* {@code suffix} is <code>"}"</code> and nothing has been added to the
|
||||
* {@code StringJoiner}.
|
||||
*
|
||||
* @apiNote
|
||||
* <p>The String {@code "[George:Sally:Fred]"} may be constructed as follows:
|
||||
*
|
||||
* <pre> {@code
|
||||
* StringJoiner sj = new StringJoiner(":", "[", "]");
|
||||
* sj.add("George").add("Sally").add("Fred");
|
||||
* String desiredString = sj.toString();
|
||||
* }</pre>
|
||||
* <p>
|
||||
* 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:
|
||||
*
|
||||
* <pre> {@code
|
||||
* List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
|
||||
* String commaSeparatedNumbers = numbers.stream()
|
||||
* .map(i -> i.toString())
|
||||
* .collect(Collectors.joining(", "));
|
||||
* }</pre>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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());
|
||||
}
|
||||
}
|
17
app/src/main/java/exh/Util.java
Normal file
17
app/src/main/java/exh/Util.java
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
9
app/src/main/res/drawable/ic_cached_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_cached_white_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M19,8l-4,4h3c0,3.31 -2.69,6 -6,6 -1.01,0 -1.97,-0.25 -2.8,-0.7l-1.46,1.46C8.97,19.54 10.43,20 12,20c4.42,0 8,-3.58 8,-8h3l-4,-4zM6,12c0,-3.31 2.69,-6 6,-6 1.01,0 1.97,0.25 2.8,0.7l1.46,-1.46C15.03,4.46 13.57,4 12,4c-4.42,0 -8,3.58 -8,8H1l4,4 4,-4H6z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_playlist_add_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_playlist_add_black_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M14,10H2v2h12v-2zm0,-4H2v2h12V6zm4,8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2H2v2z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_playlist_add_grey_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_playlist_add_grey_24dp.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:alpha=".54"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M14,10H2v2h12v-2zm0,-4H2v2h12V6zm4,8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2H2v2z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_share_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_share_white_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_whatshot_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_whatshot_black_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13.5,0.67s0.74,2.65 0.74,4.8c0,2.06 -1.35,3.73 -3.41,3.73 -2.07,0 -3.63,-1.67 -3.63,-3.73l0.03,-0.36C5.21,7.51 4,10.62 4,14c0,4.42 3.58,8 8,8s8,-3.58 8,-8C20,8.61 17.41,3.8 13.5,0.67zM11.71,19c-1.78,0 -3.22,-1.4 -3.22,-3.14 0,-1.62 1.05,-2.76 2.81,-3.12 1.77,-0.36 3.6,-1.21 4.62,-2.58 0.39,1.29 0.59,2.65 0.59,4.04 0,2.65 -2.15,4.8 -4.8,4.8z"/>
|
||||
</vector>
|
48
app/src/main/res/layout/activity_activity_batch_add.xml
Normal file
48
app/src/main/res/layout/activity_activity_batch_add.xml
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context="exh.ActivityBatchAdd">
|
||||
|
||||
<EditText
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:ems="10"
|
||||
android:id="@+id/galleryList"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@+id/textView3"
|
||||
android:hint="Example:\n\nhttp://e-hentai.org/g/12345/1a2b3c4e\nhttp://e-hentai.org/g/67890/6f7g8h9i\nhttp://exhentai.org/g/13579/1a3b5c7e\nhttp://exhentai.org/g/24680/2f4g6h8i\n"
|
||||
android:layout_above="@+id/addButton"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Add Galleries"
|
||||
android:id="@+id/addButton"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="Enter the galleries to add (separated by a new line):"
|
||||
android:id="@+id/textView3"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"/>
|
||||
|
||||
</RelativeLayout>
|
34
app/src/main/res/layout/activity_dialog_login.xml
Normal file
34
app/src/main/res/layout/activity_dialog_login.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/dl_button_panel"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true">
|
||||
<Button
|
||||
style="?attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="CANCEL"
|
||||
android:id="@+id/btnCancel" android:layout_gravity="bottom|right"/>
|
||||
|
||||
<Button
|
||||
style="?attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="RECHECK"
|
||||
android:id="@+id/btnAdvanced"
|
||||
android:layout_gravity="bottom|right" />
|
||||
</LinearLayout>
|
||||
<WebView
|
||||
android:layout_width="fill_parent"
|
||||
android:id="@+id/webView"
|
||||
android:layout_above="@+id/dl_button_panel"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_height="fill_parent"/>
|
||||
</RelativeLayout>
|
27
app/src/main/res/layout/activity_intercept.xml
Normal file
27
app/src/main/res/layout/activity_intercept.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="Analysing URL..."
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"/>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
17
app/src/main/res/layout/activity_pe.xml
Normal file
17
app/src/main/res/layout/activity_pe.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:text="Advanced Preferences"
|
||||
android:id="@+id/textView"/>
|
||||
<ListView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/peList"/>
|
||||
</LinearLayout>
|
83
app/src/main/res/layout/activity_update.xml
Normal file
83
app/src/main/res/layout/activity_update.xml
Normal file
@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_above="@+id/relativeLayout2"
|
||||
android:layout_below="@+id/textView4"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/detailsView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Loading details..."
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"/>
|
||||
</ScrollView>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:id="@+id/relativeLayout2">
|
||||
|
||||
<Button
|
||||
android:id="@+id/downloadBtn"
|
||||
style="?attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="false"
|
||||
android:text="Download"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/ignoreUpdatesBtn"
|
||||
style="?attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="false"
|
||||
android:layout_alignParentLeft="false"
|
||||
android:layout_alignParentRight="false"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="Ignore Updates"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancelBtn"
|
||||
style="?attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="false"
|
||||
android:layout_alignParentRight="true"
|
||||
android:text="Not Now"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:text="Update Available"
|
||||
android:id="@+id/textView4"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textStyle="bold"
|
||||
android:layout_alignRight="@+id/scrollView"
|
||||
android:layout_alignEnd="@+id/scrollView"
|
||||
android:textAlignment="center"
|
||||
android:singleLine="false"
|
||||
android:gravity="center_horizontal" />
|
||||
</RelativeLayout>
|
@ -13,4 +13,9 @@
|
||||
android:id="@+id/action_display_mode"
|
||||
android:title="@string/action_display_mode"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_genre_filter"
|
||||
android:title="Modify Genre Filter"
|
||||
app:showAsAction="ifRoom"/>
|
||||
</menu>
|
||||
|
@ -29,10 +29,16 @@
|
||||
app:showAsAction="collapseActionView|ifRoom"
|
||||
app:actionViewClass="android.support.v7.widget.SearchView" />
|
||||
|
||||
<item
|
||||
<!--<item
|
||||
android:id="@+id/action_update_library"
|
||||
android:title="@string/action_update_library"
|
||||
android:icon="@drawable/ic_refresh_white_24dp"
|
||||
app:showAsAction="ifRoom" />-->
|
||||
|
||||
<item
|
||||
android:id="@+id/action_sync"
|
||||
android:title="Download Favorites"
|
||||
android:icon="@drawable/ic_refresh_white_24dp"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
|
@ -8,6 +8,11 @@
|
||||
android:icon="@drawable/ic_create_white_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item android:id="@+id/action_share"
|
||||
android:title="Share URLs"
|
||||
android:icon="@drawable/ic_share_white_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item android:id="@+id/action_move_to_category"
|
||||
android:title="@string/action_move_category"
|
||||
android:icon="@drawable/ic_label_white_24dp"
|
||||
|
@ -20,6 +20,11 @@
|
||||
android:id="@+id/nav_drawer_catalogues"
|
||||
android:icon="@drawable/ic_explore_black_24dp"
|
||||
android:title="@string/label_catalogues" />
|
||||
<item
|
||||
android:id="@+id/nav_drawer_batch_add"
|
||||
android:icon="@drawable/ic_playlist_add_black_24dp"
|
||||
android:title="Batch Add"
|
||||
android:checkable="false" />
|
||||
<item
|
||||
android:id="@+id/nav_drawer_downloads"
|
||||
android:icon="@drawable/ic_file_download_black_24dp"
|
||||
|
@ -156,4 +156,21 @@
|
||||
<item>3</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="ehentai_quality">
|
||||
<item>Auto</item>
|
||||
<item>2400x</item>
|
||||
<item>1600x</item>
|
||||
<item>1280x</item>
|
||||
<item>980x</item>
|
||||
<item>780x</item>
|
||||
</string-array>
|
||||
<string-array name="ehentai_quality_values">
|
||||
<item>auto</item>
|
||||
<item>ovrs_2400</item>
|
||||
<item>ovrs_1600</item>
|
||||
<item>high</item>
|
||||
<item>med</item>
|
||||
<item>low</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
@ -3,6 +3,7 @@
|
||||
|
||||
<string name="pref_category_general_key">pref_category_general_key</string>
|
||||
<string name="pref_category_reader_key">pref_category_reader_key</string>
|
||||
<string name="pref_category_eh_key">pref_category_eh_key</string>
|
||||
<string name="pref_category_sync_key">pref_category_sync_key</string>
|
||||
<string name="pref_category_downloads_key">pref_category_downloads_key</string>
|
||||
<string name="pref_category_advanced_key">pref_category_advanced_key</string>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<resources>
|
||||
<string name="app_name">Tachiyomi</string>
|
||||
<string name="app_name">TachiyomiEH</string>
|
||||
|
||||
<string name="name">Name</string>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<string name="label_library">My library</string>
|
||||
<string name="label_recent_manga">Recently read</string>
|
||||
<string name="label_recent_updates">Recent updates</string>
|
||||
<string name="label_catalogues">Catalogues</string>
|
||||
<string name="label_catalogues">Galleries</string>
|
||||
<string name="label_categories">Categories</string>
|
||||
<string name="label_selected">Selected: %1$d</string>
|
||||
<string name="label_backup">Backup</string>
|
||||
@ -63,6 +63,7 @@
|
||||
<!-- Subsections -->
|
||||
<string name="pref_category_general">General</string>
|
||||
<string name="pref_category_reader">Reader</string>
|
||||
<string name="pref_category_eh">EHentai/ExHentai</string>
|
||||
<string name="pref_category_downloads">Downloads</string>
|
||||
<string name="pref_category_sources">Sources</string>
|
||||
<string name="pref_category_sync">Sync</string>
|
||||
@ -178,7 +179,8 @@
|
||||
<string name="pref_enable_automatic_updates_summary">Automatically check for application updates</string>
|
||||
<!-- ACRA -->
|
||||
<string name="pref_enable_acra">Send crash reports</string>
|
||||
<string name="pref_acra_summary">Helps fix any bugs. No sensitive data will be sent</string>
|
||||
<!--<string name="pref_acra_summary">Helps fix any bugs. No sensitive data will be sent</string>-->
|
||||
<string name="pref_acra_summary">Currently unavailable to prevent crash reports from being sent to the original developer!</string>
|
||||
|
||||
|
||||
<!-- Login dialog -->
|
||||
|
@ -6,11 +6,11 @@
|
||||
android:title="@string/pref_category_about"
|
||||
android:persistent="false">
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="true"
|
||||
<SwitchPreferenceCompat
|
||||
android:key="acra.enable"
|
||||
android:summary="@string/pref_acra_summary"
|
||||
android:title="@string/pref_enable_acra"/>
|
||||
android:title="@string/pref_enable_acra"
|
||||
android:defaultValue="false"/>
|
||||
|
||||
<!--<SwitchPreferenceCompat-->
|
||||
<!--android:defaultValue="false"-->
|
||||
@ -29,6 +29,15 @@
|
||||
android:persistent="false"
|
||||
android:title="@string/build_time"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="auto_update"
|
||||
android:defaultValue="true"
|
||||
android:title="Auto Update"
|
||||
android:summary="Automatically check for updates on startup."/>
|
||||
<exh.CheckUpdatePref
|
||||
android:title="Check for Updates"
|
||||
android:summary="Check for update right now."/>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
</PreferenceScreen>
|
@ -26,6 +26,10 @@
|
||||
android:summary="@string/pref_reencode_summary"
|
||||
android:title="@string/pref_reencode"/>
|
||||
|
||||
<exh.OpenPEPref
|
||||
android:title="Advanced Preferences"
|
||||
android:summary="For advanced users only! Opens SharedPreferences editor."/>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
</PreferenceScreen>
|
30
app/src/main/res/xml/pref_ehentai.xml
Normal file
30
app/src/main/res/xml/pref_ehentai.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="ehentai_screen"
|
||||
android:persistent="false"
|
||||
android:title="@string/pref_category_eh">
|
||||
<ListPreference
|
||||
android:defaultValue="auto"
|
||||
android:key="ehentai_quality"
|
||||
android:summary="The quality of the downloaded images. (The file size stated in the 'Synopsis' no longer applies if this is changed)"
|
||||
android:title="Image Quality"
|
||||
android:entries="@array/ehentai_quality"
|
||||
android:entryValues="@array/ehentai_quality_values"
|
||||
/>
|
||||
<exh.ExHentaiLoginPref
|
||||
android:defaultValue="false"
|
||||
android:key="enable_exhentai"
|
||||
android:summary="Restart the app once this setting is changed."
|
||||
android:title="Enable ExHentai"
|
||||
/>
|
||||
<SwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:key="secure_exh"
|
||||
android:summary="Use the HTTPS version of ExHentai. Uncheck if ExHentai is not working."
|
||||
android:title="Use Secure ExHentai"
|
||||
android:dependency="enable_exhentai"/>
|
||||
</PreferenceScreen>
|
||||
|
||||
</PreferenceScreen>
|
BIN
branding/header.xcf
Normal file
BIN
branding/header.xcf
Normal file
Binary file not shown.
BIN
branding/tutorials.xcf
Normal file
BIN
branding/tutorials.xcf
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user