mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Upstream merge
Internal permission change Fix url adder
This commit is contained in:
		
							
								
								
									
										29
									
								
								app/src/main/java/exh/EHSourceHelpers.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										29
									
								
								app/src/main/java/exh/EHSourceHelpers.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
package exh
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Source helpers
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
val LEWD_SOURCE_SERIES = 6900L
 | 
			
		||||
val EH_SOURCE_ID = LEWD_SOURCE_SERIES + 1
 | 
			
		||||
val EXH_SOURCE_ID = LEWD_SOURCE_SERIES + 2
 | 
			
		||||
val EH_METADATA_SOURCE_ID = LEWD_SOURCE_SERIES + 3
 | 
			
		||||
val EXH_METADATA_SOURCE_ID = LEWD_SOURCE_SERIES + 4
 | 
			
		||||
 | 
			
		||||
val PERV_EDEN_EN_SOURCE_ID = LEWD_SOURCE_SERIES + 5
 | 
			
		||||
val PERV_EDEN_IT_SOURCE_ID = LEWD_SOURCE_SERIES + 6
 | 
			
		||||
 | 
			
		||||
val NHENTAI_SOURCE_ID = LEWD_SOURCE_SERIES + 7
 | 
			
		||||
 | 
			
		||||
fun isLewdSource(source: Long) = source in 6900..6999
 | 
			
		||||
 | 
			
		||||
fun isEhSource(source: Long) = source == EH_SOURCE_ID
 | 
			
		||||
    || source == EH_METADATA_SOURCE_ID
 | 
			
		||||
 | 
			
		||||
fun isExSource(source: Long) = source == EXH_SOURCE_ID
 | 
			
		||||
        || source == EXH_METADATA_SOURCE_ID
 | 
			
		||||
 | 
			
		||||
fun isPervEdenSource(source: Long) = source == PERV_EDEN_IT_SOURCE_ID
 | 
			
		||||
|| source == PERV_EDEN_EN_SOURCE_ID
 | 
			
		||||
 | 
			
		||||
fun isNhentaiSource(source: Long) = source == NHENTAI_SOURCE_ID
 | 
			
		||||
							
								
								
									
										135
									
								
								app/src/main/java/exh/FavoritesSyncHelper.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										135
									
								
								app/src/main/java/exh/FavoritesSyncHelper.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
package exh
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.support.v7.app.AlertDialog
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
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.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.EHentai
 | 
			
		||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
class FavoritesSyncHelper(val activity: Activity) {
 | 
			
		||||
 | 
			
		||||
    val db: DatabaseHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    val sourceManager: SourceManager by injectLazy()
 | 
			
		||||
 | 
			
		||||
    val prefs: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    fun guiSyncFavorites(onComplete: () -> Unit) {
 | 
			
		||||
        //ExHentai must be enabled/user must be logged in
 | 
			
		||||
        if (!prefs.enableExhentai().getOrDefault()) {
 | 
			
		||||
            AlertDialog.Builder(activity).setTitle("Error")
 | 
			
		||||
                    .setMessage("You are not logged in! Please log in and try again!")
 | 
			
		||||
                    .setPositiveButton("Ok") { dialog, _ -> dialog.dismiss() }.show()
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        val dialog = MaterialDialog.Builder(activity)
 | 
			
		||||
                .progress(true, 0)
 | 
			
		||||
                .title("Downloading favorites")
 | 
			
		||||
                .content("Please wait...")
 | 
			
		||||
                .cancelable(false)
 | 
			
		||||
                .show()
 | 
			
		||||
        thread {
 | 
			
		||||
            var error = false
 | 
			
		||||
            try {
 | 
			
		||||
                syncFavorites()
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                error = true
 | 
			
		||||
                Timber.e(e, "Could not sync favorites!")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            dialog.dismiss()
 | 
			
		||||
 | 
			
		||||
            activity.runOnUiThread {
 | 
			
		||||
                if (error)
 | 
			
		||||
                    MaterialDialog.Builder(activity)
 | 
			
		||||
                            .title("Error")
 | 
			
		||||
                            .content("There was an error downloading your favorites, please try again later!")
 | 
			
		||||
                            .positiveText("Ok")
 | 
			
		||||
                            .show()
 | 
			
		||||
                onComplete()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun syncFavorites() {
 | 
			
		||||
        val onlineSources = sourceManager.getOnlineSources()
 | 
			
		||||
        var ehSource: EHentai? = null
 | 
			
		||||
        var exSource: EHentai? = null
 | 
			
		||||
        onlineSources.forEach {
 | 
			
		||||
            if(it.id == EH_SOURCE_ID)
 | 
			
		||||
                ehSource = it as EHentai
 | 
			
		||||
            else if(it.id == EXH_SOURCE_ID)
 | 
			
		||||
                exSource = it as EHentai
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        (exSource ?: ehSource)?.let { source ->
 | 
			
		||||
            val favResponse = source.fetchFavorites()
 | 
			
		||||
            val ourCategories = ArrayList<Category>(db.getCategories().executeAsBlocking())
 | 
			
		||||
            val ourMangas = ArrayList<Manga>(db.getMangas().executeAsBlocking())
 | 
			
		||||
            //Add required categories (categories do not sync upwards)
 | 
			
		||||
            favResponse.second.filter { theirCategory ->
 | 
			
		||||
                ourCategories.find {
 | 
			
		||||
                    it.name.endsWith(theirCategory)
 | 
			
		||||
                } == null
 | 
			
		||||
            }.map {
 | 
			
		||||
                Category.create(it)
 | 
			
		||||
            }.let {
 | 
			
		||||
                db.inTransaction {
 | 
			
		||||
                    //Insert new categories
 | 
			
		||||
                    db.insertCategories(it).executeAsBlocking().results().entries.filter {
 | 
			
		||||
                        it.value.wasInserted()
 | 
			
		||||
                    }.forEach { it.key.id = it.value.insertedId()!!.toInt() }
 | 
			
		||||
 | 
			
		||||
                    val categoryMap = (it + ourCategories).associateBy { it.name }
 | 
			
		||||
 | 
			
		||||
                    //Insert new mangas
 | 
			
		||||
                    val mangaToInsert = java.util.ArrayList<Manga>()
 | 
			
		||||
                    favResponse.first.map {
 | 
			
		||||
                        val category = categoryMap[it.fav]!!
 | 
			
		||||
                        var manga = it.manga
 | 
			
		||||
                        val alreadyHaveManga = ourMangas.find {
 | 
			
		||||
                            it.url == manga.url
 | 
			
		||||
                        }?.apply {
 | 
			
		||||
                            manga = this
 | 
			
		||||
                        } != null
 | 
			
		||||
                        if (!alreadyHaveManga) {
 | 
			
		||||
                            ourMangas.add(manga)
 | 
			
		||||
                            mangaToInsert.add(manga)
 | 
			
		||||
                        }
 | 
			
		||||
                        manga.favorite = true
 | 
			
		||||
                        Pair(manga, category)
 | 
			
		||||
                    }.apply {
 | 
			
		||||
                        //Insert mangas
 | 
			
		||||
                        db.insertMangas(mangaToInsert).executeAsBlocking().results().entries.filter {
 | 
			
		||||
                            it.value.wasInserted()
 | 
			
		||||
                        }.forEach { manga ->
 | 
			
		||||
                            manga.key.id = manga.value.insertedId()
 | 
			
		||||
                            try {
 | 
			
		||||
                                source.fetchChapterList(manga.key).map {
 | 
			
		||||
                                    syncChaptersWithSource(db, it, manga.key, source)
 | 
			
		||||
                                }.toBlocking().first()
 | 
			
		||||
                            } catch (e: Exception) {
 | 
			
		||||
                                Timber.w(e, "Failed to update chapters for gallery: ${manga.key.title}!")
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        //Set categories
 | 
			
		||||
                        val categories = map { MangaCategory.create(it.first, it.second) }
 | 
			
		||||
                        val mangas = map { it.first }
 | 
			
		||||
                        db.setMangaCategories(categories, mangas)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										192
									
								
								app/src/main/java/exh/FavoritesSyncManager.java
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										192
									
								
								app/src/main/java/exh/FavoritesSyncManager.java
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,192 @@
 | 
			
		||||
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.source.online.all.EHentai;
 | 
			
		||||
import kotlin.Pair;
 | 
			
		||||
//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 {
 | 
			
		||||
        Pair 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());
 | 
			
		||||
        }*/
 | 
			
		||||
//    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										80
									
								
								app/src/main/java/exh/GalleryAdder.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										80
									
								
								app/src/main/java/exh/GalleryAdder.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
package exh
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
 | 
			
		||||
import exh.metadata.MetadataHelper
 | 
			
		||||
import exh.metadata.copyTo
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.net.MalformedURLException
 | 
			
		||||
import java.net.URI
 | 
			
		||||
import java.net.URISyntaxException
 | 
			
		||||
import java.net.URL
 | 
			
		||||
 | 
			
		||||
class GalleryAdder {
 | 
			
		||||
 | 
			
		||||
    private val db: DatabaseHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val sourceManager: SourceManager by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val metadataHelper = MetadataHelper()
 | 
			
		||||
 | 
			
		||||
    fun addGallery(url: String, fav: Boolean = false): Manga {
 | 
			
		||||
        val source = when(URL(url).host) {
 | 
			
		||||
            "g.e-hentai.org", "e-hentai.org" -> EH_SOURCE_ID
 | 
			
		||||
            "exhentai.org" -> EXH_SOURCE_ID
 | 
			
		||||
            else -> throw MalformedURLException("Not a valid gallery URL!")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val sourceObj = sourceManager.get(source)
 | 
			
		||||
                ?: throw IllegalStateException("Could not find EH source!")
 | 
			
		||||
 | 
			
		||||
        val pathOnlyUrl = getUrlWithoutDomain(url)
 | 
			
		||||
 | 
			
		||||
        //Use manga in DB if possible, otherwise, make a new manga
 | 
			
		||||
        val manga = db.getManga(pathOnlyUrl, source).executeAsBlocking()
 | 
			
		||||
                ?: Manga.create(source).apply {
 | 
			
		||||
            this.url = pathOnlyUrl
 | 
			
		||||
            title = url
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Copy basics
 | 
			
		||||
        manga.copyFrom(sourceObj.fetchMangaDetails(manga).toBlocking().first())
 | 
			
		||||
 | 
			
		||||
        //Apply metadata
 | 
			
		||||
        metadataHelper.fetchEhMetadata(url, isExSource(source))?.copyTo(manga)
 | 
			
		||||
 | 
			
		||||
        if(fav) manga.favorite = true
 | 
			
		||||
 | 
			
		||||
        db.insertManga(manga).executeAsBlocking().insertedId()?.let {
 | 
			
		||||
            manga.id = it
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Fetch and copy chapters
 | 
			
		||||
        try {
 | 
			
		||||
            sourceObj.fetchChapterList(manga).map {
 | 
			
		||||
                syncChaptersWithSource(db, it, manga, sourceObj)
 | 
			
		||||
            }.toBlocking().first()
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            Timber.w(e, "Failed to update chapters for gallery: ${manga.title}!")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return manga
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getUrlWithoutDomain(orig: String): String {
 | 
			
		||||
        try {
 | 
			
		||||
            val uri = URI(orig)
 | 
			
		||||
            var out = uri.path
 | 
			
		||||
            if (uri.query != null)
 | 
			
		||||
                out += "?" + uri.query
 | 
			
		||||
            if (uri.fragment != null)
 | 
			
		||||
                out += "#" + uri.fragment
 | 
			
		||||
            return out
 | 
			
		||||
        } catch (e: URISyntaxException) {
 | 
			
		||||
            return orig
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								app/src/main/java/exh/StringBuilderExtensions.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								app/src/main/java/exh/StringBuilderExtensions.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
package exh
 | 
			
		||||
 | 
			
		||||
operator fun StringBuilder.plusAssign(other: String) { append(other) }
 | 
			
		||||
							
								
								
									
										5
									
								
								app/src/main/java/exh/VerbelExpressionExtensions.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/java/exh/VerbelExpressionExtensions.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
package exh
 | 
			
		||||
 | 
			
		||||
import ru.lanwen.verbalregex.VerbalExpression
 | 
			
		||||
 | 
			
		||||
fun VerbalExpression.Builder.anyChar() = add(".")!!
 | 
			
		||||
							
								
								
									
										62
									
								
								app/src/main/java/exh/metadata/MetadataHelper.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										62
									
								
								app/src/main/java/exh/metadata/MetadataHelper.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
package exh.metadata
 | 
			
		||||
 | 
			
		||||
import exh.*
 | 
			
		||||
import exh.metadata.models.ExGalleryMetadata
 | 
			
		||||
import exh.metadata.models.NHentaiMetadata
 | 
			
		||||
import exh.metadata.models.PervEdenGalleryMetadata
 | 
			
		||||
import exh.metadata.models.SearchableGalleryMetadata
 | 
			
		||||
import io.paperdb.Paper
 | 
			
		||||
 | 
			
		||||
class MetadataHelper {
 | 
			
		||||
 | 
			
		||||
    fun writeGallery(galleryMetadata: SearchableGalleryMetadata, source: Long)
 | 
			
		||||
            = (if(isExSource(source) || isEhSource(source)) exGalleryBook()
 | 
			
		||||
    else if(isPervEdenSource(source)) pervEdenGalleryBook()
 | 
			
		||||
        else if(isNhentaiSource(source)) nhentaiGalleryBook()
 | 
			
		||||
    else null)?.write(galleryMetadata.galleryUniqueIdentifier(), galleryMetadata)!!
 | 
			
		||||
 | 
			
		||||
    fun fetchEhMetadata(url: String, exh: Boolean): ExGalleryMetadata?
 | 
			
		||||
            = ExGalleryMetadata().let {
 | 
			
		||||
        it.url = url
 | 
			
		||||
        it.exh = exh
 | 
			
		||||
        return exGalleryBook().read<ExGalleryMetadata>(it.galleryUniqueIdentifier())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun fetchPervEdenMetadata(url: String, source: Long): PervEdenGalleryMetadata?
 | 
			
		||||
            = PervEdenGalleryMetadata().let {
 | 
			
		||||
        it.url = url
 | 
			
		||||
        if(source == PERV_EDEN_EN_SOURCE_ID)
 | 
			
		||||
            it.lang = "en"
 | 
			
		||||
        else if(source == PERV_EDEN_IT_SOURCE_ID)
 | 
			
		||||
            it.lang = "it"
 | 
			
		||||
        else throw IllegalArgumentException("Invalid source id!")
 | 
			
		||||
        return pervEdenGalleryBook().read<PervEdenGalleryMetadata>(it.galleryUniqueIdentifier())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun fetchNhentaiMetadata(url: String) = NHentaiMetadata().let {
 | 
			
		||||
        it.url = url
 | 
			
		||||
        nhentaiGalleryBook().read<NHentaiMetadata>(it.galleryUniqueIdentifier())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun fetchMetadata(url: String, source: Long): SearchableGalleryMetadata? {
 | 
			
		||||
        if(isExSource(source) || isEhSource(source)) {
 | 
			
		||||
            return fetchEhMetadata(url, isExSource(source))
 | 
			
		||||
        } else if(isPervEdenSource(source)) {
 | 
			
		||||
            return fetchPervEdenMetadata(url, source)
 | 
			
		||||
        } else if(isNhentaiSource(source)) {
 | 
			
		||||
            return fetchNhentaiMetadata(url)
 | 
			
		||||
        } else {
 | 
			
		||||
            return null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getAllGalleries() = exGalleryBook().allKeys.map {
 | 
			
		||||
        exGalleryBook().read<ExGalleryMetadata>(it)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun exGalleryBook() = Paper.book("gallery-ex")!!
 | 
			
		||||
 | 
			
		||||
    fun pervEdenGalleryBook() = Paper.book("gallery-perveden")!!
 | 
			
		||||
 | 
			
		||||
    fun nhentaiGalleryBook() = Paper.book("gallery-nhentai")!!
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								app/src/main/java/exh/metadata/MetadataUtil.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										47
									
								
								app/src/main/java/exh/metadata/MetadataUtil.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
package exh.metadata
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Metadata utils
 | 
			
		||||
 */
 | 
			
		||||
fun humanReadableByteCount(bytes: Long, si: Boolean): String {
 | 
			
		||||
    val unit = if (si) 1000 else 1024
 | 
			
		||||
    if (bytes < unit) return bytes.toString() + " B"
 | 
			
		||||
    val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt()
 | 
			
		||||
    val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
 | 
			
		||||
    return String.format("%.1f %sB", bytes / Math.pow(unit.toDouble(), exp.toDouble()), pre)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val KB_FACTOR: Long = 1000
 | 
			
		||||
private val KIB_FACTOR: Long = 1024
 | 
			
		||||
private val MB_FACTOR = 1000 * KB_FACTOR
 | 
			
		||||
private val MIB_FACTOR = 1024 * KIB_FACTOR
 | 
			
		||||
private val GB_FACTOR = 1000 * MB_FACTOR
 | 
			
		||||
private val GIB_FACTOR = 1024 * MIB_FACTOR
 | 
			
		||||
 | 
			
		||||
fun parseHumanReadableByteCount(arg0: String): Double? {
 | 
			
		||||
    val spaceNdx = arg0.indexOf(" ")
 | 
			
		||||
    val ret = java.lang.Double.parseDouble(arg0.substring(0, spaceNdx))
 | 
			
		||||
    when (arg0.substring(spaceNdx + 1)) {
 | 
			
		||||
        "GB" -> return ret * GB_FACTOR
 | 
			
		||||
        "GiB" -> return ret * GIB_FACTOR
 | 
			
		||||
        "MB" -> return ret * MB_FACTOR
 | 
			
		||||
        "MiB" -> return ret * MIB_FACTOR
 | 
			
		||||
        "KB" -> return ret * KB_FACTOR
 | 
			
		||||
        "KiB" -> return ret * KIB_FACTOR
 | 
			
		||||
    }
 | 
			
		||||
    return null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
fun String?.nullIfBlank(): String? = if(isNullOrBlank())
 | 
			
		||||
    null
 | 
			
		||||
else
 | 
			
		||||
    this
 | 
			
		||||
 | 
			
		||||
fun <T> ignore(expr: () -> T): T? {
 | 
			
		||||
    return try { expr() } catch (t: Throwable) { null }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun <K,V> Set<Map.Entry<K,V>>.forEach(action: (K, V) -> Unit) {
 | 
			
		||||
    forEach { action(it.key, it.value) }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										218
									
								
								app/src/main/java/exh/metadata/MetdataCopier.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										218
									
								
								app/src/main/java/exh/metadata/MetdataCopier.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,218 @@
 | 
			
		||||
package exh.metadata
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.EHentai
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.EHentaiMetadata
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.PervEden
 | 
			
		||||
import exh.metadata.models.*
 | 
			
		||||
import exh.plusAssign
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Copies gallery metadata to a manga object
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
private const val EH_ARTIST_NAMESPACE = "artist"
 | 
			
		||||
private const val EH_AUTHOR_NAMESPACE = "author"
 | 
			
		||||
 | 
			
		||||
private const val NHENTAI_ARTIST_NAMESPACE = "artist"
 | 
			
		||||
private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
 | 
			
		||||
 | 
			
		||||
private val ONGOING_SUFFIX = arrayOf(
 | 
			
		||||
        "[ongoing]",
 | 
			
		||||
        "(ongoing)",
 | 
			
		||||
        "{ongoing}"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
 | 
			
		||||
 | 
			
		||||
private val prefs: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
fun ExGalleryMetadata.copyTo(manga: SManga) {
 | 
			
		||||
    //TODO Find some way to do this with SManga
 | 
			
		||||
    /*exh?.let {
 | 
			
		||||
        manga.source = if(it)
 | 
			
		||||
            2
 | 
			
		||||
        else
 | 
			
		||||
            1
 | 
			
		||||
    }*/
 | 
			
		||||
    url?.let { manga.url = it }
 | 
			
		||||
    thumbnailUrl?.let { manga.thumbnail_url = it }
 | 
			
		||||
 | 
			
		||||
    //No title bug?
 | 
			
		||||
    val titleObj = if(prefs.useJapaneseTitle().getOrDefault())
 | 
			
		||||
        altTitle ?: title
 | 
			
		||||
    else
 | 
			
		||||
        title
 | 
			
		||||
    titleObj?.let { manga.title = it }
 | 
			
		||||
 | 
			
		||||
    //Set artist (if we can find one)
 | 
			
		||||
    tags[EH_ARTIST_NAMESPACE]?.let {
 | 
			
		||||
        if(it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
 | 
			
		||||
    }
 | 
			
		||||
    //Set author (if we can find one)
 | 
			
		||||
    tags[EH_AUTHOR_NAMESPACE]?.let {
 | 
			
		||||
        if(it.isNotEmpty()) manga.author = it.joinToString(transform = Tag::name)
 | 
			
		||||
    }
 | 
			
		||||
    //Set genre
 | 
			
		||||
    genre?.let { manga.genre = it }
 | 
			
		||||
 | 
			
		||||
    //Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
 | 
			
		||||
    //We default to completed
 | 
			
		||||
    manga.status = SManga.COMPLETED
 | 
			
		||||
    title?.let { t ->
 | 
			
		||||
        ONGOING_SUFFIX.find {
 | 
			
		||||
            t.endsWith(it, ignoreCase = true)
 | 
			
		||||
        }?.let {
 | 
			
		||||
            manga.status = SManga.ONGOING
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //Build a nice looking description out of what we know
 | 
			
		||||
    val titleDesc = StringBuilder()
 | 
			
		||||
    title?.let { titleDesc += "Title: $it\n" }
 | 
			
		||||
    altTitle?.let { titleDesc += "Alternate Title: $it\n" }
 | 
			
		||||
 | 
			
		||||
    val detailsDesc = StringBuilder()
 | 
			
		||||
    uploader?.let { detailsDesc += "Uploader: $it\n" }
 | 
			
		||||
    datePosted?.let { detailsDesc += "Posted: ${EX_DATE_FORMAT.format(Date(it))}\n" }
 | 
			
		||||
    visible?.let { detailsDesc += "Visible: $it\n" }
 | 
			
		||||
    language?.let {
 | 
			
		||||
        detailsDesc += "Language: $it"
 | 
			
		||||
        if(translated == true) detailsDesc += " TR"
 | 
			
		||||
        detailsDesc += "\n"
 | 
			
		||||
    }
 | 
			
		||||
    size?.let { detailsDesc += "File Size: ${humanReadableByteCount(it, true)}\n" }
 | 
			
		||||
    length?.let { detailsDesc += "Length: $it pages\n" }
 | 
			
		||||
    favorites?.let { detailsDesc += "Favorited: $it times\n" }
 | 
			
		||||
    averageRating?.let {
 | 
			
		||||
        detailsDesc += "Rating: $it"
 | 
			
		||||
        ratingCount?.let { detailsDesc += " ($it)" }
 | 
			
		||||
        detailsDesc += "\n"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val tagsDesc = buildTagsDescription(this)
 | 
			
		||||
 | 
			
		||||
    manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
 | 
			
		||||
            .filter(String::isNotBlank)
 | 
			
		||||
            .joinToString(separator = "\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun PervEdenGalleryMetadata.copyTo(manga: SManga) {
 | 
			
		||||
    url?.let { manga.url = it }
 | 
			
		||||
    thumbnailUrl?.let { manga.thumbnail_url = it }
 | 
			
		||||
 | 
			
		||||
    val titleDesc = StringBuilder()
 | 
			
		||||
    title?.let {
 | 
			
		||||
        manga.title = it
 | 
			
		||||
        titleDesc += "Title: $it\n"
 | 
			
		||||
    }
 | 
			
		||||
    if(altTitles.isNotEmpty())
 | 
			
		||||
        titleDesc += "Alternate Titles: \n" + altTitles.map {
 | 
			
		||||
            "▪ $it"
 | 
			
		||||
        }.joinToString(separator = "\n", postfix = "\n")
 | 
			
		||||
 | 
			
		||||
    val detailsDesc = StringBuilder()
 | 
			
		||||
    artist?.let {
 | 
			
		||||
        manga.artist = it
 | 
			
		||||
        detailsDesc += "Artist: $it\n"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    type?.let {
 | 
			
		||||
        manga.genre = it
 | 
			
		||||
        detailsDesc += "Type: $it\n"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    status?.let {
 | 
			
		||||
        manga.status = when(it) {
 | 
			
		||||
            "Ongoing" -> SManga.ONGOING
 | 
			
		||||
            "Completed", "Suspended" -> SManga.COMPLETED
 | 
			
		||||
            else -> SManga.UNKNOWN
 | 
			
		||||
        }
 | 
			
		||||
        detailsDesc += "Status: $it\n"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    rating?.let {
 | 
			
		||||
        detailsDesc += "Rating: %.2\n".format(it)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val tagsDesc = buildTagsDescription(this)
 | 
			
		||||
 | 
			
		||||
    manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
 | 
			
		||||
            .filter(String::isNotBlank)
 | 
			
		||||
            .joinToString(separator = "\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun NHentaiMetadata.copyTo(manga: SManga) {
 | 
			
		||||
    url?.let { manga.url = it }
 | 
			
		||||
 | 
			
		||||
    //TODO next update allow this to be changed to use HD covers
 | 
			
		||||
    if(mediaId != null)
 | 
			
		||||
        NHentaiMetadata.typeToExtension(thumbnailImageType)?.let {
 | 
			
		||||
            manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/thumb.$it"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    manga.title = englishTitle ?: japaneseTitle ?: shortTitle!!
 | 
			
		||||
 | 
			
		||||
    //Set artist (if we can find one)
 | 
			
		||||
    tags[NHENTAI_ARTIST_NAMESPACE]?.let {
 | 
			
		||||
        if(it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tags[NHENTAI_CATEGORIES_NAMESPACE]?.let {
 | 
			
		||||
        if(it.isNotEmpty()) manga.genre = it.joinToString(transform = Tag::name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
 | 
			
		||||
    //We default to completed
 | 
			
		||||
    manga.status = SManga.COMPLETED
 | 
			
		||||
    englishTitle?.let { t ->
 | 
			
		||||
        ONGOING_SUFFIX.find {
 | 
			
		||||
            t.endsWith(it, ignoreCase = true)
 | 
			
		||||
        }?.let {
 | 
			
		||||
            manga.status = SManga.ONGOING
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val titleDesc = StringBuilder()
 | 
			
		||||
    englishTitle?.let { titleDesc += "English Title: $it\n" }
 | 
			
		||||
    japaneseTitle?.let { titleDesc += "Japanese Title: $it\n" }
 | 
			
		||||
    shortTitle?.let { titleDesc += "Short Title: $it\n" }
 | 
			
		||||
 | 
			
		||||
    val detailsDesc = StringBuilder()
 | 
			
		||||
    uploadDate?.let { detailsDesc += "Upload Date: ${EX_DATE_FORMAT.format(Date(it))}\n" }
 | 
			
		||||
    pageImageTypes.size.let { detailsDesc += "Length: $it pages\n" }
 | 
			
		||||
    favoritesCount?.let { detailsDesc += "Favorited: $it times\n" }
 | 
			
		||||
    scanlator?.nullIfBlank()?.let { detailsDesc += "Scanlator: $it\n" }
 | 
			
		||||
 | 
			
		||||
    val tagsDesc = buildTagsDescription(this)
 | 
			
		||||
 | 
			
		||||
    manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
 | 
			
		||||
            .filter(String::isNotBlank)
 | 
			
		||||
            .joinToString(separator = "\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun SearchableGalleryMetadata.genericCopyTo(manga: SManga): Boolean {
 | 
			
		||||
    when(this) {
 | 
			
		||||
        is ExGalleryMetadata -> this.copyTo(manga)
 | 
			
		||||
        is PervEdenGalleryMetadata -> this.copyTo(manga)
 | 
			
		||||
        is NHentaiMetadata -> this.copyTo(manga)
 | 
			
		||||
        else -> return false
 | 
			
		||||
    }
 | 
			
		||||
    return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun buildTagsDescription(metadata: SearchableGalleryMetadata)
 | 
			
		||||
        = StringBuilder("Tags:\n").apply {
 | 
			
		||||
        //BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
 | 
			
		||||
        metadata.tags.entries.forEach { namespace, tags ->
 | 
			
		||||
            if (tags.isNotEmpty()) {
 | 
			
		||||
                val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
 | 
			
		||||
                this += "▪ $namespace: $joinedTags\n"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
							
								
								
									
										51
									
								
								app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										51
									
								
								app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
package exh.metadata.models
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gallery metadata storage model
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
class ExGalleryMetadata : SearchableGalleryMetadata() {
 | 
			
		||||
    var url: String? = null
 | 
			
		||||
 | 
			
		||||
    var exh: Boolean? = null
 | 
			
		||||
 | 
			
		||||
    var thumbnailUrl: String? = null
 | 
			
		||||
 | 
			
		||||
    var title: String? = null
 | 
			
		||||
    var altTitle: String? = null
 | 
			
		||||
 | 
			
		||||
    var genre: String? = null
 | 
			
		||||
 | 
			
		||||
    var datePosted: Long? = null
 | 
			
		||||
    var parent: String? = null
 | 
			
		||||
    var visible: String? = null //Not a boolean
 | 
			
		||||
    var language: String? = null
 | 
			
		||||
    var translated: Boolean? = null
 | 
			
		||||
    var size: Long? = null
 | 
			
		||||
    var length: Int? = null
 | 
			
		||||
    var favorites: Int? = null
 | 
			
		||||
    var ratingCount: Int? = null
 | 
			
		||||
    var averageRating: Double? = null
 | 
			
		||||
 | 
			
		||||
    override fun getTitles() = listOf(title, altTitle).filterNotNull()
 | 
			
		||||
 | 
			
		||||
    private fun splitGalleryUrl()
 | 
			
		||||
            = url?.let {
 | 
			
		||||
        Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun galleryId() = splitGalleryUrl()?.let { it[it.size - 2] }
 | 
			
		||||
 | 
			
		||||
    fun galleryToken() =
 | 
			
		||||
        splitGalleryUrl()?.last()
 | 
			
		||||
 | 
			
		||||
    override fun galleryUniqueIdentifier() = exh?.let { exh ->
 | 
			
		||||
        url?.let {
 | 
			
		||||
            //Fuck, this should be EXH and EH but it's too late to change it now...
 | 
			
		||||
            "${if(exh) "EXH" else "EX"}-${galleryId()}-${galleryToken()}"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								app/src/main/java/exh/metadata/models/NHentaiMetadata.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										48
									
								
								app/src/main/java/exh/metadata/models/NHentaiMetadata.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
package exh.metadata.models
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * NHentai metadata
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
class NHentaiMetadata : SearchableGalleryMetadata() {
 | 
			
		||||
 | 
			
		||||
    var id: Long? = null
 | 
			
		||||
 | 
			
		||||
    var url get() = id?.let { "$BASE_URL/g/$it" }
 | 
			
		||||
    set(a) {
 | 
			
		||||
        a?.let {
 | 
			
		||||
            id = a.split("/").last().toLong()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var uploadDate: Long? = null
 | 
			
		||||
 | 
			
		||||
    var favoritesCount: Long? = null
 | 
			
		||||
 | 
			
		||||
    var mediaId: String? = null
 | 
			
		||||
 | 
			
		||||
    var japaneseTitle: String? = null
 | 
			
		||||
    var englishTitle: String? = null
 | 
			
		||||
    var shortTitle: String? = null
 | 
			
		||||
 | 
			
		||||
    var coverImageType: String? = null
 | 
			
		||||
    var pageImageTypes: MutableList<String> = mutableListOf()
 | 
			
		||||
    var thumbnailImageType: String? = null
 | 
			
		||||
 | 
			
		||||
    var scanlator: String? = null
 | 
			
		||||
 | 
			
		||||
    override fun galleryUniqueIdentifier(): String? = "NHENTAI-$id"
 | 
			
		||||
 | 
			
		||||
    override fun getTitles() = listOf(japaneseTitle, englishTitle, shortTitle).filterNotNull()
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        val BASE_URL = "https://nhentai.net"
 | 
			
		||||
 | 
			
		||||
        fun typeToExtension(t: String?) =
 | 
			
		||||
            when(t) {
 | 
			
		||||
                "p" -> "png"
 | 
			
		||||
                "j" -> "jpg"
 | 
			
		||||
                else -> null
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										32
									
								
								app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
package exh.metadata.models
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
 | 
			
		||||
class PervEdenGalleryMetadata : SearchableGalleryMetadata() {
 | 
			
		||||
    var url: String? = null
 | 
			
		||||
    var thumbnailUrl: String? = null
 | 
			
		||||
 | 
			
		||||
    var title: String? = null
 | 
			
		||||
    var altTitles: MutableList<String> = mutableListOf()
 | 
			
		||||
 | 
			
		||||
    var artist: String? = null
 | 
			
		||||
 | 
			
		||||
    var type: String? = null
 | 
			
		||||
 | 
			
		||||
    var rating: Float? = null
 | 
			
		||||
 | 
			
		||||
    var status: String? = null
 | 
			
		||||
 | 
			
		||||
    var lang: String? = null
 | 
			
		||||
 | 
			
		||||
    override fun getTitles() = listOf(title).plus(altTitles).filterNotNull()
 | 
			
		||||
 | 
			
		||||
    private fun splitGalleryUrl()
 | 
			
		||||
            = url?.let {
 | 
			
		||||
        Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun galleryUniqueIdentifier() = splitGalleryUrl()?.let {
 | 
			
		||||
        "PERVEDEN-${lang?.toUpperCase()}-${it.last()}"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										18
									
								
								app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
package exh.metadata.models
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList
 | 
			
		||||
import java.util.HashMap
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A gallery that can be searched using the EH search engine
 | 
			
		||||
 */
 | 
			
		||||
abstract class SearchableGalleryMetadata {
 | 
			
		||||
    var uploader: String? = null
 | 
			
		||||
 | 
			
		||||
    //Being specific about which classes are used in generics to make deserialization easier
 | 
			
		||||
    val tags: HashMap<String, ArrayList<Tag>> = HashMap()
 | 
			
		||||
 | 
			
		||||
    abstract fun galleryUniqueIdentifier(): String?
 | 
			
		||||
 | 
			
		||||
    abstract fun getTitles(): List<String>
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								app/src/main/java/exh/metadata/models/Tag.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								app/src/main/java/exh/metadata/models/Tag.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
package exh.metadata.models
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Simple tag model
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
data class Tag(var name: String, var light: Boolean)
 | 
			
		||||
							
								
								
									
										3
									
								
								app/src/main/java/exh/search/MultiWildcard.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								app/src/main/java/exh/search/MultiWildcard.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
package exh.search
 | 
			
		||||
 | 
			
		||||
class MultiWildcard : TextComponent()
 | 
			
		||||
							
								
								
									
										4
									
								
								app/src/main/java/exh/search/Namespace.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								app/src/main/java/exh/search/Namespace.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
package exh.search
 | 
			
		||||
 | 
			
		||||
class Namespace(var namespace: String,
 | 
			
		||||
                var tag: Text? = null) : QueryComponent()
 | 
			
		||||
							
								
								
									
										6
									
								
								app/src/main/java/exh/search/QueryComponent.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										6
									
								
								app/src/main/java/exh/search/QueryComponent.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
package exh.search
 | 
			
		||||
 | 
			
		||||
open class QueryComponent {
 | 
			
		||||
    var excluded = false
 | 
			
		||||
    var exact = false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										150
									
								
								app/src/main/java/exh/search/SearchEngine.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										150
									
								
								app/src/main/java/exh/search/SearchEngine.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,150 @@
 | 
			
		||||
package exh.search
 | 
			
		||||
 | 
			
		||||
import exh.metadata.models.SearchableGalleryMetadata
 | 
			
		||||
import exh.metadata.models.Tag
 | 
			
		||||
 | 
			
		||||
class SearchEngine {
 | 
			
		||||
 | 
			
		||||
    private val queryCache = mutableMapOf<String, List<QueryComponent>>()
 | 
			
		||||
 | 
			
		||||
    fun matches(metadata: SearchableGalleryMetadata, query: List<QueryComponent>): Boolean {
 | 
			
		||||
 | 
			
		||||
        fun matchTagList(tags: Sequence<Tag>,
 | 
			
		||||
                         component: Text): Boolean {
 | 
			
		||||
            //Match tags
 | 
			
		||||
            val tagMatcher = if(!component.exact)
 | 
			
		||||
                component.asLenientRegex()
 | 
			
		||||
            else
 | 
			
		||||
                component.asRegex()
 | 
			
		||||
            //Match beginning of tag
 | 
			
		||||
            if (tags.find {
 | 
			
		||||
                tagMatcher.testExact(it.name)
 | 
			
		||||
            } != null) {
 | 
			
		||||
                if(component.excluded) return false
 | 
			
		||||
            } else {
 | 
			
		||||
                //No tag matched for this component
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val cachedTitles = metadata.getTitles().map(String::toLowerCase)
 | 
			
		||||
 | 
			
		||||
        for(component in query) {
 | 
			
		||||
            if(component is Text) {
 | 
			
		||||
                //Match title
 | 
			
		||||
                if (cachedTitles.find { component.asRegex().test(it) } != null) {
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                //Match tags
 | 
			
		||||
                if(!matchTagList(metadata.tags.entries.asSequence().flatMap { it.value.asSequence() },
 | 
			
		||||
                        component)) return false
 | 
			
		||||
            } else if(component is Namespace) {
 | 
			
		||||
                if(component.namespace == "uploader") {
 | 
			
		||||
                    //Match uploader
 | 
			
		||||
                    if(!component.tag?.rawTextOnly().equals(metadata.uploader,
 | 
			
		||||
                            ignoreCase = true)) {
 | 
			
		||||
                        return false
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    if(component.tag!!.components.size > 0) {
 | 
			
		||||
                        //Match namespace
 | 
			
		||||
                        val ns = metadata.tags.entries.asSequence().filter {
 | 
			
		||||
                            it.key == component.namespace
 | 
			
		||||
                        }.flatMap { it.value.asSequence() }
 | 
			
		||||
                        //Match tags
 | 
			
		||||
                        if (!matchTagList(ns, component.tag!!))
 | 
			
		||||
                            return false
 | 
			
		||||
                    } else {
 | 
			
		||||
                        //Perform namespace search
 | 
			
		||||
                        val hasNs = metadata.tags.entries.find {
 | 
			
		||||
                            it.key == component.namespace
 | 
			
		||||
                        } != null
 | 
			
		||||
 | 
			
		||||
                        if(hasNs && component.excluded)
 | 
			
		||||
                            return false
 | 
			
		||||
                        else if(!hasNs && !component.excluded)
 | 
			
		||||
                            return false
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun parseQuery(query: String) = queryCache.getOrPut(query, {
 | 
			
		||||
        val res = mutableListOf<QueryComponent>()
 | 
			
		||||
 | 
			
		||||
        var inQuotes = false
 | 
			
		||||
        val queuedRawText = StringBuilder()
 | 
			
		||||
        val queuedText = mutableListOf<TextComponent>()
 | 
			
		||||
        var namespace: Namespace? = null
 | 
			
		||||
 | 
			
		||||
        var nextIsExcluded = false
 | 
			
		||||
        var nextIsExact = false
 | 
			
		||||
 | 
			
		||||
        fun flushText() {
 | 
			
		||||
            if(queuedRawText.isNotEmpty()) {
 | 
			
		||||
                queuedText += StringTextComponent(queuedRawText.toString())
 | 
			
		||||
                queuedRawText.setLength(0)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun flushToText() = Text().apply {
 | 
			
		||||
            components += queuedText
 | 
			
		||||
            queuedText.clear()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun flushAll() {
 | 
			
		||||
            flushText()
 | 
			
		||||
            if (queuedText.isNotEmpty() || namespace != null) {
 | 
			
		||||
                val component = namespace?.apply {
 | 
			
		||||
                    tag = flushToText()
 | 
			
		||||
                    namespace = null
 | 
			
		||||
                } ?: flushToText()
 | 
			
		||||
                component.excluded = nextIsExcluded
 | 
			
		||||
                component.exact = nextIsExact
 | 
			
		||||
                res += component
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for(char in query.toLowerCase()) {
 | 
			
		||||
            if(char == '"') {
 | 
			
		||||
                inQuotes = !inQuotes
 | 
			
		||||
            } else if(char == '?' || char == '_') {
 | 
			
		||||
                flushText()
 | 
			
		||||
                queuedText.add(SingleWildcard())
 | 
			
		||||
            } else if(char == '*' || char == '%') {
 | 
			
		||||
                flushText()
 | 
			
		||||
                queuedText.add(MultiWildcard())
 | 
			
		||||
            } else if(char == '-') {
 | 
			
		||||
                nextIsExcluded = true
 | 
			
		||||
            } else if(char == '$') {
 | 
			
		||||
                nextIsExact = true
 | 
			
		||||
            } else if(char == ':') {
 | 
			
		||||
                flushText()
 | 
			
		||||
                var flushed = flushToText().rawTextOnly()
 | 
			
		||||
                //Map tag aliases
 | 
			
		||||
                flushed = when(flushed) {
 | 
			
		||||
                    "a" -> "artist"
 | 
			
		||||
                    "c", "char" -> "character"
 | 
			
		||||
                    "f" -> "female"
 | 
			
		||||
                    "g", "creator", "circle" -> "group"
 | 
			
		||||
                    "l", "lang" -> "language"
 | 
			
		||||
                    "m" -> "male"
 | 
			
		||||
                    "p", "series" -> "parody"
 | 
			
		||||
                    "r" -> "reclass"
 | 
			
		||||
                    else -> flushed
 | 
			
		||||
                }
 | 
			
		||||
                namespace = Namespace(flushed, null)
 | 
			
		||||
            } else if(char == ' ' && !inQuotes) {
 | 
			
		||||
                flushAll()
 | 
			
		||||
            } else {
 | 
			
		||||
                queuedRawText.append(char)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        flushAll()
 | 
			
		||||
 | 
			
		||||
        res
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								app/src/main/java/exh/search/SingleWildcard.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								app/src/main/java/exh/search/SingleWildcard.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
package exh.search
 | 
			
		||||
 | 
			
		||||
class SingleWildcard : TextComponent()
 | 
			
		||||
							
								
								
									
										3
									
								
								app/src/main/java/exh/search/StringTextComponent.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								app/src/main/java/exh/search/StringTextComponent.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
package exh.search
 | 
			
		||||
 | 
			
		||||
class StringTextComponent(val value: String) : TextComponent()
 | 
			
		||||
							
								
								
									
										49
									
								
								app/src/main/java/exh/search/Text.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										49
									
								
								app/src/main/java/exh/search/Text.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
package exh.search
 | 
			
		||||
 | 
			
		||||
import exh.anyChar
 | 
			
		||||
import ru.lanwen.verbalregex.VerbalExpression
 | 
			
		||||
 | 
			
		||||
class Text: QueryComponent() {
 | 
			
		||||
    val components = mutableListOf<TextComponent>()
 | 
			
		||||
 | 
			
		||||
    private var regex: VerbalExpression? = null
 | 
			
		||||
    private var lenientRegex: VerbalExpression? = null
 | 
			
		||||
    private var rawText: String? = null
 | 
			
		||||
 | 
			
		||||
    fun asRegex(): VerbalExpression {
 | 
			
		||||
        if(regex == null) {
 | 
			
		||||
            regex = baseBuilder().build()
 | 
			
		||||
        }
 | 
			
		||||
        return regex!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun asLenientRegex(): VerbalExpression {
 | 
			
		||||
        if(lenientRegex == null) {
 | 
			
		||||
            lenientRegex = baseBuilder().anything().build()
 | 
			
		||||
        }
 | 
			
		||||
        return lenientRegex!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun baseBuilder(): VerbalExpression.Builder {
 | 
			
		||||
        val builder = VerbalExpression.regex()
 | 
			
		||||
        for(component in components) {
 | 
			
		||||
            when(component) {
 | 
			
		||||
                is StringTextComponent -> builder.then(component.value)
 | 
			
		||||
                is SingleWildcard -> builder.anyChar()
 | 
			
		||||
                is MultiWildcard -> builder.anything()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return builder
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun rawTextOnly() = if(rawText != null)
 | 
			
		||||
        rawText!!
 | 
			
		||||
    else {
 | 
			
		||||
        rawText = components
 | 
			
		||||
                .filter { it is StringTextComponent }
 | 
			
		||||
                .joinToString(separator = "", transform = {
 | 
			
		||||
                    (it as StringTextComponent).value
 | 
			
		||||
                })
 | 
			
		||||
        rawText!!
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								app/src/main/java/exh/search/TextComponent.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								app/src/main/java/exh/search/TextComponent.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
package exh.search
 | 
			
		||||
 | 
			
		||||
open class TextComponent
 | 
			
		||||
							
								
								
									
										131
									
								
								app/src/main/java/exh/ui/batchadd/BatchAddFragment.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										131
									
								
								app/src/main/java/exh/ui/batchadd/BatchAddFragment.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,131 @@
 | 
			
		||||
package exh.ui.batchadd
 | 
			
		||||
 | 
			
		||||
import android.content.pm.ActivityInfo
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
 | 
			
		||||
import exh.GalleryAdder
 | 
			
		||||
import exh.metadata.nullIfBlank
 | 
			
		||||
import kotlinx.android.synthetic.main.eh_fragment_batch_add.*
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * LoginActivity
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
class BatchAddFragment : BaseFragment() {
 | 
			
		||||
 | 
			
		||||
    private val galleryAdder by lazy { GalleryAdder() }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?)
 | 
			
		||||
        = inflater.inflate(R.layout.eh_fragment_batch_add, container, false)!!
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
 | 
			
		||||
        setToolbarTitle("Batch add")
 | 
			
		||||
 | 
			
		||||
        setup()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setup() {
 | 
			
		||||
        btn_add_galleries.setOnClickListener {
 | 
			
		||||
            val galleries = galleries_box.text.toString()
 | 
			
		||||
            //Check text box has content
 | 
			
		||||
            if(galleries.isNullOrBlank()) {
 | 
			
		||||
                noGalleriesSpecified()
 | 
			
		||||
                return@setOnClickListener
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Too lazy to actually deal with orientation changes
 | 
			
		||||
            activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
 | 
			
		||||
 | 
			
		||||
            val splitGalleries = galleries.split("\n").map {
 | 
			
		||||
                it.trim().nullIfBlank()
 | 
			
		||||
            }.filterNotNull()
 | 
			
		||||
 | 
			
		||||
            val dialog = MaterialDialog.Builder(context)
 | 
			
		||||
                    .title("Adding galleries...")
 | 
			
		||||
                    .progress(false, splitGalleries.size, true)
 | 
			
		||||
                    .cancelable(false)
 | 
			
		||||
                    .canceledOnTouchOutside(false)
 | 
			
		||||
                    .show()
 | 
			
		||||
 | 
			
		||||
            val succeeded = mutableListOf<String>()
 | 
			
		||||
            val failed = mutableListOf<String>()
 | 
			
		||||
 | 
			
		||||
            thread {
 | 
			
		||||
                splitGalleries.forEachIndexed { i, s ->
 | 
			
		||||
                    activity.runOnUiThread {
 | 
			
		||||
                        dialog.setContent("Processing: $s")
 | 
			
		||||
                    }
 | 
			
		||||
                    if(addGallery(s)) {
 | 
			
		||||
                        succeeded.add(s)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        failed.add(s)
 | 
			
		||||
                    }
 | 
			
		||||
                    activity.runOnUiThread {
 | 
			
		||||
                        dialog.setProgress(i + 1)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //Show report
 | 
			
		||||
                val succeededCount = succeeded.size
 | 
			
		||||
                val failedCount = failed.size
 | 
			
		||||
 | 
			
		||||
                if(succeeded.isEmpty()) succeeded += "None"
 | 
			
		||||
                if(failed.isEmpty()) failed += "None"
 | 
			
		||||
                val succeededReport = succeeded.joinToString(separator = "\n", prefix = "Added:\n")
 | 
			
		||||
                val failedReport = failed.joinToString(separator = "\n", prefix = "Failed:\n")
 | 
			
		||||
 | 
			
		||||
                val summary = "Summary:\nAdded: $succeededCount gallerie(s)\nFailed: $failedCount gallerie(s)"
 | 
			
		||||
 | 
			
		||||
                val report = listOf(succeededReport, failedReport, summary).joinToString(separator = "\n\n")
 | 
			
		||||
 | 
			
		||||
                activity.runOnUiThread {
 | 
			
		||||
                    //Enable orientation changes again
 | 
			
		||||
                    activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
 | 
			
		||||
 | 
			
		||||
                    dialog.dismiss()
 | 
			
		||||
 | 
			
		||||
                    MaterialDialog.Builder(context)
 | 
			
		||||
                            .title("Batch add report")
 | 
			
		||||
                            .content(report)
 | 
			
		||||
                            .positiveText("Ok")
 | 
			
		||||
                            .cancelable(true)
 | 
			
		||||
                            .canceledOnTouchOutside(true)
 | 
			
		||||
                            .show()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun addGallery(url: String): Boolean {
 | 
			
		||||
        try {
 | 
			
		||||
            galleryAdder.addGallery(url, true)
 | 
			
		||||
        } catch(t: Throwable) {
 | 
			
		||||
            Timber.e(t, "Could not add gallery!")
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun noGalleriesSpecified() {
 | 
			
		||||
        MaterialDialog.Builder(context)
 | 
			
		||||
                .title("No galleries to add!")
 | 
			
		||||
                .content("You must specify at least one gallery to add!")
 | 
			
		||||
                .positiveText("Ok")
 | 
			
		||||
                .onPositive { materialDialog, _ -> materialDialog.dismiss() }
 | 
			
		||||
                .cancelable(true)
 | 
			
		||||
                .canceledOnTouchOutside(true)
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun newInstance() = BatchAddFragment()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								app/src/main/java/exh/ui/intercept/InterceptActivity.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										82
									
								
								app/src/main/java/exh/ui/intercept/InterceptActivity.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
package exh.ui.intercept
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.MenuItem
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
 | 
			
		||||
import exh.GalleryAdder
 | 
			
		||||
import kotlinx.android.synthetic.main.toolbar.*
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
class InterceptActivity : BaseActivity() {
 | 
			
		||||
 | 
			
		||||
    private val galleryAdder = GalleryAdder()
 | 
			
		||||
 | 
			
		||||
    var finished = false
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        setAppTheme()
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        setContentView(R.layout.eh_activity_intercept)
 | 
			
		||||
 | 
			
		||||
        setupToolbar(toolbar, backNavigation = false)
 | 
			
		||||
 | 
			
		||||
        if(savedInstanceState == null)
 | 
			
		||||
            thread { setup() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setup() {
 | 
			
		||||
        try {
 | 
			
		||||
            processLink()
 | 
			
		||||
        } catch(t: Throwable) {
 | 
			
		||||
            Timber.e(t, "Could not intercept link!")
 | 
			
		||||
            if(!finished)
 | 
			
		||||
                runOnUiThread {
 | 
			
		||||
                    MaterialDialog.Builder(this)
 | 
			
		||||
                            .title("Error")
 | 
			
		||||
                            .content("Could not load this gallery!")
 | 
			
		||||
                            .cancelable(true)
 | 
			
		||||
                            .canceledOnTouchOutside(true)
 | 
			
		||||
                            .cancelListener { onBackPressed() }
 | 
			
		||||
                            .positiveText("Ok")
 | 
			
		||||
                            .onPositive { _, _ -> onBackPressed() }
 | 
			
		||||
                            .dismissListener { onBackPressed() }
 | 
			
		||||
                            .show()
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun processLink() {
 | 
			
		||||
        if(Intent.ACTION_VIEW == intent.action) {
 | 
			
		||||
            val manga = galleryAdder.addGallery(intent.dataString)
 | 
			
		||||
 | 
			
		||||
            if(!finished)
 | 
			
		||||
                startActivity(MangaActivity.newIntent(this, manga, true))
 | 
			
		||||
            onBackPressed()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            android.R.id.home -> onBackPressed()
 | 
			
		||||
            else -> return super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBackPressed() {
 | 
			
		||||
        if(!finished)
 | 
			
		||||
            runOnUiThread {
 | 
			
		||||
                super.onBackPressed()
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onStop() {
 | 
			
		||||
        super.onStop()
 | 
			
		||||
        finished = true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								app/src/main/java/exh/ui/lock/LockActivity.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										60
									
								
								app/src/main/java/exh/ui/lock/LockActivity.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
package exh.ui.lock
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.andrognito.pinlockview.PinLockListener
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 | 
			
		||||
import kotlinx.android.synthetic.main.activity_lock.*
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class LockActivity : BaseActivity() {
 | 
			
		||||
 | 
			
		||||
    val prefs: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        disableLock = true
 | 
			
		||||
 | 
			
		||||
        setTheme(R.style.Theme_Tachiyomi_Dark)
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        if(!lockEnabled(prefs)) {
 | 
			
		||||
            finish()
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setContentView(R.layout.activity_lock)
 | 
			
		||||
 | 
			
		||||
        pin_lock_view.attachIndicatorDots(indicator_dots)
 | 
			
		||||
 | 
			
		||||
        pin_lock_view.pinLength = prefs.lockLength().getOrDefault()
 | 
			
		||||
        pin_lock_view.setPinLockListener(object : PinLockListener {
 | 
			
		||||
            override fun onEmpty() {}
 | 
			
		||||
 | 
			
		||||
            override fun onComplete(pin: String) {
 | 
			
		||||
                if(sha512(pin, prefs.lockSalt().get()!!) == prefs.lockHash().get()) {
 | 
			
		||||
                    //Yay!
 | 
			
		||||
                    finish()
 | 
			
		||||
                } else {
 | 
			
		||||
                    MaterialDialog.Builder(this@LockActivity)
 | 
			
		||||
                            .title("PIN code incorrect")
 | 
			
		||||
                            .content("The PIN code you entered is incorrect. Please try again.")
 | 
			
		||||
                            .cancelable(true)
 | 
			
		||||
                            .canceledOnTouchOutside(true)
 | 
			
		||||
                            .positiveText("Ok")
 | 
			
		||||
                            .autoDismiss(true)
 | 
			
		||||
                            .show()
 | 
			
		||||
                    pin_lock_view.resetPinLockView()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onPinChange(pinLength: Int, intermediatePin: String?) {}
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBackPressed() {
 | 
			
		||||
        moveTaskToBack(true)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										85
									
								
								app/src/main/java/exh/ui/lock/LockPreference.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										85
									
								
								app/src/main/java/exh/ui/lock/LockPreference.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
package exh.ui.lock
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.support.v7.preference.Preference
 | 
			
		||||
import android.text.InputType
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.math.BigInteger
 | 
			
		||||
import java.security.SecureRandom
 | 
			
		||||
 | 
			
		||||
class LockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
 | 
			
		||||
        Preference(context, attrs) {
 | 
			
		||||
 | 
			
		||||
    val secureRandom by lazy { SecureRandom() }
 | 
			
		||||
 | 
			
		||||
    val prefs: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    override fun onAttached() {
 | 
			
		||||
        super.onAttached()
 | 
			
		||||
        updateSummary()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateSummary() {
 | 
			
		||||
        if(lockEnabled(prefs)) {
 | 
			
		||||
            summary = "Application is locked"
 | 
			
		||||
        } else {
 | 
			
		||||
            summary = "Application is not locked, tap to lock"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onClick() {
 | 
			
		||||
        super.onClick()
 | 
			
		||||
        if(!notifyLockSecurity(context)) {
 | 
			
		||||
            MaterialDialog.Builder(context)
 | 
			
		||||
                    .title("Lock application")
 | 
			
		||||
                    .content("Enter a pin to lock the application. Enter nothing to disable the pin lock.")
 | 
			
		||||
                    .inputRangeRes(0, 10, R.color.material_red_500)
 | 
			
		||||
                    .inputType(InputType.TYPE_CLASS_NUMBER)
 | 
			
		||||
                    .input("", "", { _, c ->
 | 
			
		||||
                        val progressDialog = MaterialDialog.Builder(context)
 | 
			
		||||
                                .title("Saving password")
 | 
			
		||||
                                .progress(true, 0)
 | 
			
		||||
                                .cancelable(false)
 | 
			
		||||
                                .show()
 | 
			
		||||
                        Observable.fromCallable {
 | 
			
		||||
                            savePassword(c.toString())
 | 
			
		||||
                        }.subscribeOn(Schedulers.computation())
 | 
			
		||||
                                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                                .subscribe {
 | 
			
		||||
                                    progressDialog.dismiss()
 | 
			
		||||
                                    updateSummary()
 | 
			
		||||
                                }
 | 
			
		||||
                    })
 | 
			
		||||
                    .negativeText("Cancel")
 | 
			
		||||
                    .autoDismiss(true)
 | 
			
		||||
                    .cancelable(true)
 | 
			
		||||
                    .canceledOnTouchOutside(true)
 | 
			
		||||
                    .show()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun savePassword(password: String) {
 | 
			
		||||
        val salt: String?
 | 
			
		||||
        val hash: String?
 | 
			
		||||
        val length: Int
 | 
			
		||||
        if(password.isEmpty()) {
 | 
			
		||||
            salt = null
 | 
			
		||||
            hash = null
 | 
			
		||||
            length = -1
 | 
			
		||||
        } else {
 | 
			
		||||
            salt = BigInteger(130, secureRandom).toString(32)
 | 
			
		||||
            hash = sha512(password, salt)
 | 
			
		||||
            length = password.length
 | 
			
		||||
        }
 | 
			
		||||
        prefs.lockSalt().set(salt)
 | 
			
		||||
        prefs.lockHash().set(hash)
 | 
			
		||||
        prefs.lockLength().set(length)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								app/src/main/java/exh/ui/lock/LockUtils.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										91
									
								
								app/src/main/java/exh/ui/lock/LockUtils.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
package exh.ui.lock
 | 
			
		||||
 | 
			
		||||
import android.annotation.TargetApi
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.app.AppOpsManager
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.pm.PackageManager
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.provider.Settings
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
import kotlin.experimental.and
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Password hashing utils
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Yes, I know SHA512 is fast, but bcrypt on mobile devices is too slow apparently
 | 
			
		||||
 */
 | 
			
		||||
fun sha512(passwordToHash: String, salt: String): String {
 | 
			
		||||
    val md = MessageDigest.getInstance("SHA-512")
 | 
			
		||||
    md.update(salt.toByteArray(charset("UTF-8")))
 | 
			
		||||
    val bytes = md.digest(passwordToHash.toByteArray(charset("UTF-8")))
 | 
			
		||||
    val sb = StringBuilder()
 | 
			
		||||
    for (i in bytes.indices) {
 | 
			
		||||
        sb.append(Integer.toString((bytes[i] and 0xff.toByte()) + 0x100, 16).substring(1))
 | 
			
		||||
    }
 | 
			
		||||
    return sb.toString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if lock is enabled
 | 
			
		||||
 */
 | 
			
		||||
fun lockEnabled(prefs: PreferencesHelper = Injekt.get())
 | 
			
		||||
    = prefs.lockHash().get() != null
 | 
			
		||||
            && prefs.lockSalt().get() != null
 | 
			
		||||
            && prefs.lockLength().getOrDefault() != -1
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Lock the screen
 | 
			
		||||
 */
 | 
			
		||||
fun showLockActivity(activity: Activity) {
 | 
			
		||||
    activity.startActivity(Intent(activity, LockActivity::class.java))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if the lock will function properly
 | 
			
		||||
 *
 | 
			
		||||
 * @return true if action is required, false if lock is working properly
 | 
			
		||||
 */
 | 
			
		||||
fun notifyLockSecurity(context: Context): Boolean {
 | 
			
		||||
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !hasAccessToUsageStats(context)) {
 | 
			
		||||
        MaterialDialog.Builder(context)
 | 
			
		||||
                .title("Permission required")
 | 
			
		||||
                .content("${context.getString(R.string.app_name)} requires the usage stats permission to detect when you leave the app. " +
 | 
			
		||||
                        "This is required for the application lock to function properly. " +
 | 
			
		||||
                        "Press OK to grant this permission now.")
 | 
			
		||||
                .negativeText("Cancel")
 | 
			
		||||
                .positiveText("Ok")
 | 
			
		||||
                .onPositive { _, _ ->
 | 
			
		||||
                    context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
 | 
			
		||||
                }
 | 
			
		||||
                .autoDismiss(true)
 | 
			
		||||
                .cancelable(false)
 | 
			
		||||
                .show()
 | 
			
		||||
        return true
 | 
			
		||||
    } else {
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
 | 
			
		||||
fun hasAccessToUsageStats(context: Context): Boolean {
 | 
			
		||||
    try {
 | 
			
		||||
        val packageManager = context.packageManager
 | 
			
		||||
        val applicationInfo = packageManager.getApplicationInfo(context.packageName, 0)
 | 
			
		||||
        val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
 | 
			
		||||
        val mode = appOpsManager.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, applicationInfo.uid, applicationInfo.packageName)
 | 
			
		||||
        return (mode == AppOpsManager.MODE_ALLOWED)
 | 
			
		||||
    } catch (e: PackageManager.NameNotFoundException) {
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										218
									
								
								app/src/main/java/exh/ui/login/LoginActivity.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										218
									
								
								app/src/main/java/exh/ui/login/LoginActivity.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,218 @@
 | 
			
		||||
package exh.ui.login
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.MenuItem
 | 
			
		||||
import android.webkit.CookieManager
 | 
			
		||||
import android.webkit.WebView
 | 
			
		||||
import android.webkit.WebViewClient
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.EHentai
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 | 
			
		||||
import exh.EXH_SOURCE_ID
 | 
			
		||||
import kotlinx.android.synthetic.main.eh_activity_login.*
 | 
			
		||||
import kotlinx.android.synthetic.main.toolbar.*
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.net.HttpCookie
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * LoginActivity
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
class LoginActivity : BaseActivity() {
 | 
			
		||||
 | 
			
		||||
    val preferenceManager: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    val sourceManager: SourceManager by injectLazy()
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        setAppTheme()
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        setContentView(R.layout.eh_activity_login)
 | 
			
		||||
 | 
			
		||||
        setup()
 | 
			
		||||
 | 
			
		||||
        setupToolbar(toolbar, backNavigation = false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setup() {
 | 
			
		||||
        btn_cancel.setOnClickListener { onBackPressed() }
 | 
			
		||||
        btn_recheck.setOnClickListener { webview.loadUrl("http://exhentai.org/") }
 | 
			
		||||
 | 
			
		||||
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 | 
			
		||||
            CookieManager.getInstance().removeAllCookies {
 | 
			
		||||
                runOnUiThread {
 | 
			
		||||
                    startWebview()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            CookieManager.getInstance().removeAllCookie()
 | 
			
		||||
            startWebview()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun startWebview() {
 | 
			
		||||
        webview.settings.javaScriptEnabled = true
 | 
			
		||||
        webview.settings.domStorageEnabled = true
 | 
			
		||||
 | 
			
		||||
        webview.loadUrl("https://forums.e-hentai.org/index.php?act=Login")
 | 
			
		||||
 | 
			
		||||
        webview.setWebViewClient(object : WebViewClient() {
 | 
			
		||||
            override fun onPageFinished(view: WebView, url: String) {
 | 
			
		||||
                super.onPageFinished(view, url)
 | 
			
		||||
                Timber.d(url)
 | 
			
		||||
                val parsedUrl = Uri.parse(url)
 | 
			
		||||
                if(parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
 | 
			
		||||
                    //Hide distracting content
 | 
			
		||||
                    view.loadUrl(HIDE_JS)
 | 
			
		||||
 | 
			
		||||
                    //Check login result
 | 
			
		||||
                    if(parsedUrl.getQueryParameter("code")?.toInt() != 0) {
 | 
			
		||||
                        if(checkLoginCookies(url)) view.loadUrl("http://exhentai.org/")
 | 
			
		||||
                    }
 | 
			
		||||
                } else if(parsedUrl.host.equals("exhentai.org", ignoreCase = true)) {
 | 
			
		||||
                    //At ExHentai, check that everything worked out...
 | 
			
		||||
                    if(applyExHentaiCookies(url)) {
 | 
			
		||||
                        preferenceManager.enableExhentai().set(true)
 | 
			
		||||
                        finishLogin()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun finishLogin() {
 | 
			
		||||
        val progressDialog = MaterialDialog.Builder(this)
 | 
			
		||||
                .title("Finalizing login")
 | 
			
		||||
                .progress(true, 0)
 | 
			
		||||
                .content("Please wait...")
 | 
			
		||||
                .cancelable(false)
 | 
			
		||||
                .show()
 | 
			
		||||
 | 
			
		||||
        val eh = sourceManager
 | 
			
		||||
                .getOnlineSources()
 | 
			
		||||
                .find { it.id == EXH_SOURCE_ID } as EHentai
 | 
			
		||||
        Observable.fromCallable {
 | 
			
		||||
            //I honestly have no idea why we need to call this twice, but it works, so whatever
 | 
			
		||||
            try {
 | 
			
		||||
                eh.fetchFavorites()
 | 
			
		||||
            } catch(ignored: Exception) {}
 | 
			
		||||
            try {
 | 
			
		||||
                eh.fetchFavorites()
 | 
			
		||||
            } catch(ignored: Exception) {}
 | 
			
		||||
        }.subscribeOn(Schedulers.io())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe {
 | 
			
		||||
                    progressDialog.dismiss()
 | 
			
		||||
                    onBackPressed()
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if we are logged in
 | 
			
		||||
     */
 | 
			
		||||
    fun checkLoginCookies(url: String): Boolean {
 | 
			
		||||
        getCookies(url)?.let { parsed ->
 | 
			
		||||
            return parsed.filter {
 | 
			
		||||
                (it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true)
 | 
			
		||||
                        || it.name.equals(PASS_HASH_COOKIE, ignoreCase = true))
 | 
			
		||||
                        && it.value.isNotBlank()
 | 
			
		||||
            }.count() >= 2
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse cookies at ExHentai
 | 
			
		||||
     */
 | 
			
		||||
    fun applyExHentaiCookies(url: String): Boolean {
 | 
			
		||||
        getCookies(url)?.let { parsed ->
 | 
			
		||||
 | 
			
		||||
            var memberId: String? = null
 | 
			
		||||
            var passHash: String? = null
 | 
			
		||||
            var igneous: String? = null
 | 
			
		||||
 | 
			
		||||
            parsed.forEach {
 | 
			
		||||
                when (it.name.toLowerCase()) {
 | 
			
		||||
                    MEMBER_ID_COOKIE -> memberId = it.value
 | 
			
		||||
                    PASS_HASH_COOKIE -> passHash = it.value
 | 
			
		||||
                    IGNEOUS_COOKIE -> igneous = it.value
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Missing a cookie
 | 
			
		||||
            if (memberId == null || passHash == null || igneous == null) return false
 | 
			
		||||
 | 
			
		||||
            //Update prefs
 | 
			
		||||
            preferenceManager.memberIdVal().set(memberId)
 | 
			
		||||
            preferenceManager.passHashVal().set(passHash)
 | 
			
		||||
            preferenceManager.igneousVal().set(igneous)
 | 
			
		||||
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getCookies(url: String): List<HttpCookie>?
 | 
			
		||||
            = CookieManager.getInstance().getCookie(url)?.let {
 | 
			
		||||
        it.split("; ").flatMap {
 | 
			
		||||
            HttpCookie.parse(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            android.R.id.home -> onBackPressed()
 | 
			
		||||
            else -> return super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val MEMBER_ID_COOKIE = "ipb_member_id"
 | 
			
		||||
        const val PASS_HASH_COOKIE = "ipb_pass_hash"
 | 
			
		||||
        const val IGNEOUS_COOKIE = "igneous"
 | 
			
		||||
 | 
			
		||||
        const val HIDE_JS = """
 | 
			
		||||
                    javascript:(function () {
 | 
			
		||||
                        document.getElementsByTagName('body')[0].style.visibility = 'hidden';
 | 
			
		||||
                        document.getElementsByName('submit')[0].style.visibility = 'visible';
 | 
			
		||||
                        document.querySelector('td[width="60%"][valign="top"]').style.visibility = 'visible';
 | 
			
		||||
 | 
			
		||||
                        function hide(e) {if(e !== null && e !== undefined) e.style.display = 'none';}
 | 
			
		||||
 | 
			
		||||
                        hide(document.querySelector(".errorwrap"));
 | 
			
		||||
                        hide(document.querySelector('td[width="40%"][valign="top"]'));
 | 
			
		||||
                        var child = document.querySelector(".page").querySelector('div');
 | 
			
		||||
                        child.style.padding = null;
 | 
			
		||||
                        var ft = child.querySelectorAll('table');
 | 
			
		||||
                        var fd = child.parentNode.querySelectorAll('div > div');
 | 
			
		||||
                        var fh = document.querySelector('#border').querySelectorAll('td > table');
 | 
			
		||||
                        hide(ft[0]);
 | 
			
		||||
                        hide(ft[1]);
 | 
			
		||||
                        hide(fd[1]);
 | 
			
		||||
                        hide(fd[2]);
 | 
			
		||||
                        hide(child.querySelector('br'));
 | 
			
		||||
                        var error = document.querySelector(".page > div > .borderwrap");
 | 
			
		||||
                        if(error !== null) error.style.visibility = 'visible';
 | 
			
		||||
                        hide(fh[0]);
 | 
			
		||||
                        hide(fh[1]);
 | 
			
		||||
                        hide(document.querySelector("#gfooter"));
 | 
			
		||||
                        hide(document.querySelector(".copyright"));
 | 
			
		||||
                        document.querySelectorAll("td").forEach(function(e) {
 | 
			
		||||
                            e.style.color = "white";
 | 
			
		||||
                        });
 | 
			
		||||
                        var pc = document.querySelector(".postcolor");
 | 
			
		||||
                        if(pc !== null) pc.style.color = "#26353F";
 | 
			
		||||
                    })()
 | 
			
		||||
                    """
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										136
									
								
								app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										136
									
								
								app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,136 @@
 | 
			
		||||
package exh.ui.migration
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.pm.ActivityInfo
 | 
			
		||||
import android.text.Html
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.EHentai
 | 
			
		||||
import exh.isExSource
 | 
			
		||||
import exh.isLewdSource
 | 
			
		||||
import exh.metadata.MetadataHelper
 | 
			
		||||
import exh.metadata.copyTo
 | 
			
		||||
import exh.metadata.genericCopyTo
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
class MetadataFetchDialog {
 | 
			
		||||
 | 
			
		||||
    val metadataHelper by lazy { MetadataHelper() }
 | 
			
		||||
 | 
			
		||||
    val db: DatabaseHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    val sourceManager: SourceManager by injectLazy()
 | 
			
		||||
 | 
			
		||||
    val preferenceHelper: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    fun show(context: Activity) {
 | 
			
		||||
        //Too lazy to actually deal with orientation changes
 | 
			
		||||
        context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
 | 
			
		||||
 | 
			
		||||
        val progressDialog = MaterialDialog.Builder(context)
 | 
			
		||||
                .title("Fetching library metadata")
 | 
			
		||||
                .content("Preparing library")
 | 
			
		||||
                .progress(false, 0, true)
 | 
			
		||||
                .cancelable(false)
 | 
			
		||||
                .canceledOnTouchOutside(false)
 | 
			
		||||
                .show()
 | 
			
		||||
 | 
			
		||||
        thread {
 | 
			
		||||
            db.deleteMangasNotInLibrary().executeAsBlocking()
 | 
			
		||||
 | 
			
		||||
            val libraryMangas = db.getLibraryMangas()
 | 
			
		||||
                    .executeAsBlocking()
 | 
			
		||||
                    .filter {
 | 
			
		||||
                        isLewdSource(it.source)
 | 
			
		||||
                        && metadataHelper.fetchMetadata(it.url, it.source) == null
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
            context.runOnUiThread {
 | 
			
		||||
                progressDialog.maxProgress = libraryMangas.size
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Actual metadata fetch code
 | 
			
		||||
            libraryMangas.forEachIndexed { i, manga ->
 | 
			
		||||
                context.runOnUiThread {
 | 
			
		||||
                    progressDialog.setContent("Processing: ${manga.title}")
 | 
			
		||||
                    progressDialog.setProgress(i + 1)
 | 
			
		||||
                }
 | 
			
		||||
                try {
 | 
			
		||||
                    val source = sourceManager.get(manga.source)
 | 
			
		||||
                    source?.let {
 | 
			
		||||
                        manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first())
 | 
			
		||||
                        metadataHelper.fetchMetadata(manga.url, manga.source)?.genericCopyTo(manga)
 | 
			
		||||
                    }
 | 
			
		||||
                } catch(t: Throwable) {
 | 
			
		||||
                    Timber.e(t, "Could not migrate manga!")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            context.runOnUiThread {
 | 
			
		||||
                progressDialog.dismiss()
 | 
			
		||||
 | 
			
		||||
                //Enable orientation changes again
 | 
			
		||||
                context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
 | 
			
		||||
 | 
			
		||||
                displayMigrationComplete(context)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun askMigration(activity: Activity) {
 | 
			
		||||
        var extra = ""
 | 
			
		||||
        db.getLibraryMangas().asRxSingle().subscribe {
 | 
			
		||||
            //Not logged in but have ExHentai galleries
 | 
			
		||||
            if(!preferenceHelper.enableExhentai().getOrDefault()) {
 | 
			
		||||
                it.find { isExSource(it.source) }?.let {
 | 
			
		||||
                    extra = "<b><font color='red'>If you use ExHentai, please log in first before fetching your library metadata!</font></b><br><br>"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            activity.runOnUiThread {
 | 
			
		||||
                MaterialDialog.Builder(activity)
 | 
			
		||||
                        .title("Fetch library metadata")
 | 
			
		||||
                        .content(Html.fromHtml("You need to fetch your library's metadata before tag searching in the library will function.<br><br>" +
 | 
			
		||||
                                "This process may take a long time depending on your library size and will also use up a significant amount of internet bandwidth.<br><br>" +
 | 
			
		||||
                                extra +
 | 
			
		||||
                                "This process can be done later if required."))
 | 
			
		||||
                        .positiveText("Migrate")
 | 
			
		||||
                        .negativeText("Later")
 | 
			
		||||
                        .onPositive { _, _ -> show(activity) }
 | 
			
		||||
                        .onNegative({ _, _ -> adviseMigrationLater(activity) })
 | 
			
		||||
                        .cancelable(false)
 | 
			
		||||
                        .canceledOnTouchOutside(false)
 | 
			
		||||
                        .dismissListener {
 | 
			
		||||
                            preferenceHelper.migrateLibraryAsked().set(true)
 | 
			
		||||
                        }.show()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun adviseMigrationLater(activity: Activity) {
 | 
			
		||||
        MaterialDialog.Builder(activity)
 | 
			
		||||
                .title("Metadata fetch canceled")
 | 
			
		||||
                .content("Library metadata fetch has been canceled.\n\n" +
 | 
			
		||||
                        "You can run this operation later by going to: Settings > E-Hentai > Migrate library metadata")
 | 
			
		||||
                .positiveText("Ok")
 | 
			
		||||
                .cancelable(true)
 | 
			
		||||
                .canceledOnTouchOutside(true)
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun displayMigrationComplete(activity: Activity) {
 | 
			
		||||
        MaterialDialog.Builder(activity)
 | 
			
		||||
                .title("Migration complete")
 | 
			
		||||
                .content("${activity.getString(R.string.app_name)} is now ready for use!")
 | 
			
		||||
                .positiveText("Ok")
 | 
			
		||||
                .cancelable(true)
 | 
			
		||||
                .canceledOnTouchOutside(true)
 | 
			
		||||
                .show()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								app/src/main/java/exh/ui/migration/MigrationStatus.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										16
									
								
								app/src/main/java/exh/ui/migration/MigrationStatus.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
package exh.ui.migration
 | 
			
		||||
 | 
			
		||||
class MigrationStatus {
 | 
			
		||||
    companion object {
 | 
			
		||||
        val NOT_INITIALIZED = -1
 | 
			
		||||
        val COMPLETED = 0
 | 
			
		||||
 | 
			
		||||
        //Migration process
 | 
			
		||||
        val NOTIFY_USER = 1
 | 
			
		||||
        val OPEN_BACKUP_MENU = 2
 | 
			
		||||
        val PERFORM_BACKUP = 3
 | 
			
		||||
        val FINALIZE_MIGRATION = 4
 | 
			
		||||
 | 
			
		||||
        val MAX_MIGRATION_STEPS = 2
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										79
									
								
								app/src/main/java/exh/ui/migration/UrlMigrator.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										79
									
								
								app/src/main/java/exh/ui/migration/UrlMigrator.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
package exh.ui.migration
 | 
			
		||||
 | 
			
		||||
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 exh.isExSource
 | 
			
		||||
import exh.isLewdSource
 | 
			
		||||
import exh.metadata.MetadataHelper
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class UrlMigrator {
 | 
			
		||||
    private val db: DatabaseHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val prefs: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val metadataHelper: MetadataHelper by lazy { MetadataHelper() }
 | 
			
		||||
 | 
			
		||||
    fun perform() {
 | 
			
		||||
        db.inTransaction {
 | 
			
		||||
            val dbMangas = db.getMangas()
 | 
			
		||||
                    .executeAsBlocking()
 | 
			
		||||
 | 
			
		||||
            //Find all EX mangas
 | 
			
		||||
            val qualifyingMangas = dbMangas.asSequence().filter {
 | 
			
		||||
                isLewdSource(it.source)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val possibleDups = mutableListOf<Manga>()
 | 
			
		||||
            val badMangas = mutableListOf<Manga>()
 | 
			
		||||
 | 
			
		||||
            qualifyingMangas.forEach {
 | 
			
		||||
                if(it.url.startsWith("g/")) //Missing slash at front so we are bad
 | 
			
		||||
                    badMangas.add(it)
 | 
			
		||||
                else
 | 
			
		||||
                    possibleDups.add(it)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Sort possible dups so we can use binary search on it
 | 
			
		||||
            possibleDups.sortBy { it.url }
 | 
			
		||||
 | 
			
		||||
            badMangas.forEach { manga ->
 | 
			
		||||
                //Build fixed URL
 | 
			
		||||
                val urlWithSlash = "/" + manga.url
 | 
			
		||||
                //Fix metadata if required
 | 
			
		||||
                val metadata = metadataHelper.fetchEhMetadata(manga.url, isExSource(manga.source))
 | 
			
		||||
                metadata?.url?.let {
 | 
			
		||||
                    if(it.startsWith("g/")) { //Check if metadata URL has no slash
 | 
			
		||||
                        metadata.url = urlWithSlash //Fix it
 | 
			
		||||
                        metadataHelper.writeGallery(metadata, manga.source) //Write new metadata to disk
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                //If we have a dup (with the fixed url), use the dup instead
 | 
			
		||||
                val possibleDup = possibleDups.binarySearchBy(urlWithSlash, selector = { it.url })
 | 
			
		||||
                if(possibleDup >= 0) {
 | 
			
		||||
                    //Make sure it is favorited if we are
 | 
			
		||||
                    if(manga.favorite) {
 | 
			
		||||
                        val dup = possibleDups[possibleDup]
 | 
			
		||||
                        dup.favorite = true
 | 
			
		||||
                        db.insertManga(dup).executeAsBlocking() //Update DB with changes
 | 
			
		||||
                    }
 | 
			
		||||
                    //Delete ourself (but the dup is still there)
 | 
			
		||||
                    db.deleteManga(manga).executeAsBlocking()
 | 
			
		||||
                    return@forEach
 | 
			
		||||
                }
 | 
			
		||||
                //No dup, correct URL and reinsert ourselves
 | 
			
		||||
                manga.url = urlWithSlash
 | 
			
		||||
                db.insertManga(manga).executeAsBlocking()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun tryMigration() {
 | 
			
		||||
        if(!prefs.hasPerformedURLMigration().getOrDefault()) {
 | 
			
		||||
            perform()
 | 
			
		||||
            prefs.hasPerformedURLMigration().set(true)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								app/src/main/java/exh/util/UriFilter.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/java/exh/util/UriFilter.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
package exh.util
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Uri filter
 | 
			
		||||
 */
 | 
			
		||||
interface UriFilter {
 | 
			
		||||
    fun addToUri(builder: Uri.Builder)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								app/src/main/java/exh/util/UriGroup.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										15
									
								
								app/src/main/java/exh/util/UriGroup.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
package exh.util
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * UriGroup
 | 
			
		||||
 */
 | 
			
		||||
open class UriGroup<V>(name: String, state: List<V>) : Filter.Group<V>(name, state), UriFilter {
 | 
			
		||||
    override fun addToUri(builder: Uri.Builder) {
 | 
			
		||||
        state.forEach {
 | 
			
		||||
            if(it is UriFilter) it.addToUri(builder)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user