Integrate TachiyomiEH changes.

This commit is contained in:
NerdNumber9 2016-08-05 19:42:36 -04:00
parent 74e3d387eb
commit 3c43bebe64
56 changed files with 2980 additions and 99 deletions

5
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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), "")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

View 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...");
}
}
}
}
);
}
}

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

View 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());
}*/
}
}

View 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;
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

Binary file not shown.

BIN
branding/tutorials.xcf Normal file

Binary file not shown.