From 1568c09fa2205324c3d3c7a31f2f2677a33219e9 Mon Sep 17 00:00:00 2001 From: sguinetti Date: Sun, 9 May 2021 11:21:03 -0500 Subject: [PATCH 001/138] Add spanish translation --- app/src/main/res/values-es/plurals.xml | 35 +++++ app/src/main/res/values-es/strings.xml | 209 +++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 app/src/main/res/values-es/plurals.xml create mode 100644 app/src/main/res/values-es/strings.xml diff --git a/app/src/main/res/values-es/plurals.xml b/app/src/main/res/values-es/plurals.xml new file mode 100644 index 000000000..d68590bf5 --- /dev/null +++ b/app/src/main/res/values-es/plurals.xml @@ -0,0 +1,35 @@ + + + + Total %1$d página + Total %1$d páginas + + + %1$d elemento + %1$d elementos + + + %1$d nuevo capítulo + %1$d nuevos capítulos + + + %1$d capítulo + %1$d capítulos + + + %1$d capítulo desde %2$d + %1$d capítulos desde %2$d + + + %hace 1$d minuto + hace %1$d minutos + + + hace %1$d hora + hace %1$d horas + + + hace %1$d día + hace %1$d días + + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..3aab6627b --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,209 @@ + + Cerrar menú + Abrir menú + Dispositivo + Favoritos + Historial + Ocurrió un error + Error de red + Detalles + Capítulos + Lista + Lista detallada + Cuadrículas + Vista de lista + Ajustes + Fuentes remotas + Cargando... + Capítulo %1$d de %2$d + Cerrar + Reintentar + Borrar historial + No se encontró nada + Historial vacío + Leer + Añadir marcador + Añade tu manga favorito primero + Añadir a favoritos + Añadir categoría + Añadir + Introduce categoría + Guardar + Compartir + Crear acceso directo… + Compartir %s + Buscar + Buscar manga + Descargar manga… + Procesando… + Descarga completa + Descargas + Por nombre + Popularidad + Actualización + Recientes + Por calificación + Todo + Establecer orden + Género + Filtrar + Tema + Claro + Oscuro + Automático + Páginas + Borrar + ¿Realmente quieres borrar todo tu historial de lectura? Esta acción no se puede deshacer. + Eliminar + \"%s\" retirado del historial + \"%s\" borrado del almacenamiento local + Espera que termine la carga + Guardar página + Página guardada con éxito + Compartir imagen + Importar + Borrar + Esta operación no está admitida + Archivo no válido. Sólo se admiten ZIP y CBZ. + Sin descripción + Historial y caché + Borrar la caché de páginas + Caché + B|kB|MB|GB|TB + Estándar + Webtoon + Modo de lectura + Tamaño de la cuadrícula + Resultados de búsqueda en %s + Buscar en %s + Borrar manga + ¿Realmente quieres borrar \"%s\" del almacenamiento local de tu teléfono? \nEsta operación no se puede deshacer. + Ajustes del lector + Cambiar de página + Tapas en los bordes + Botones de volumen + Continuar + Advertencia + Esta operación puede consumir mucho tráfico de red + No volver a preguntar + Cancelar... + Error + Borrar la caché de miniaturas + Borrar el historial de búsqueda + Historial de búsqueda borrado + Sólo gestos + Almacenamiento interno + Almacenamiento externo + Dominio + Por defecto + Comprobar actualizaciones automáticamente + Una nueva versión de la aplicación está disponible + Mostrar notificación si la actualización está disponible + Abrir en el navegador + Este manga tiene %s. ¿Quieres guardarlo todo? + Guardar manga + Notificaciones + Activado %1$d de %2$d + Nuevos capítulos + Notificar sobre las actualizaciones del manga que estás leyendo + Descargar + Leer desde el principio + Reiniciar + Configuración de las notificaciones + Sonido de las notificaciones + Indicador de luz + Vibración + Categorías favoritas + Categorías... + Renombrar + ¿Realmente quieres eliminar la categoría \"%s\" de tus favoritos? \nTodo el manga en ella se perderá. + Quitar categoría + Puedes usar categorías para organizar tus mangas favoritos. Pulsa «+» para crear una categoría + El manga que estás leyendo se mostrará aquí. Puedes encontrar qué leer en el menú lateral + Todavía no tienes ningún manga guardado. Puedes guardarlo desde fuentes remotas o importarlo desde un archivo + Estante de manga + Manga reciente + Animación de páginas + Ubicación de descarga del manga + No disponible + No se puede encontrar ningún almacenamiento disponible + Otro almacenamiento + Utilizar conexión segura (HTTPS) + Aceptar + Todos los favoritos + Esta categoría está vacía + Leer más tarde + Actualizaciones + Aquí verás los nuevos episodios del manga que estás leyendo + Resultados de la búsqueda + Relación + Nueva versión: %s + Tamaño: %s + Esperando red... + Borrar actualizaciones + Borrar el feed de actualizaciones + Girar pantalla + Actualizar + La actualización de la alimentación comenzará pronto + Comprueba las actualizaciones del manga + No comprobar + Introducir contraseña + Contraseña incorrecta + Proteger aplicación + Pide la contraseña al iniciar la aplicación + Repite la contraseña + Las contraseñas no coinciden + Acerca de + Versión %s + Comprobar actualizaciones + Comprobar si hay actualizaciones... + Fallo en la comprobación de actualizaciones + No hay actualizaciones disponibles + Derecha a izquierda + Preferir lector de derecha a izquierda + Puedes configurar el modo de lectura para cada manga por separado + Nueva categoría + Crear incidencia en GitHub + Modo de escala + Ajuste al centro + Ajuste a la altura + Ajuste a la anchura + Mantener al iniciar + Tema oscuro auténtico + Útil para pantallas AMOLED + Se requiere reinicio + + Crear copia de seguridad de datos + Restaurar desde la copia de seguridad + Datos restaurados + Preparando... + Archivo no encontrado + Todos los datos fueron restaurados con éxito + Los datos fueron restaurados, pero hay errores + Puedes crear una copia de seguridad de tu historial y favoritos para restaurarla + Ahora mismo + Ayer + Hace mucho tiempo + Grupo + Hoy + Toca para volver a intentar + Se recordará la configuración elegida para este manga + Silenciar + El captcha es obligatorio + Resolver + Borrar cookies + Se han eliminado todas las cookies + Buscando nuevos capítulos: %1$d de %2$d + Limpiar feed + Todo el historial de actualizaciones se borrará y esta acción no se puede deshacer. ¿Está seguro? + Comprobación de nuevos capítulos + Invertir + Iniciar sesión + Debes autorizar para ver este contenido + Por defecto: %s + ...y %1$d más + Siguiente + Introduce la contraseña que se requerirá cuando se inicie la aplicación + Confirmar + La contraseña debe tener al menos 4 caracteres + From 3fbec046ba2e2f367386bb35da92fed39d6f8ae7 Mon Sep 17 00:00:00 2001 From: Xtimms Date: Thu, 13 May 2021 20:26:17 +0300 Subject: [PATCH 002/138] Belarusian translation and minor fixes in Russian --- app/src/main/res/values-be/plurals.xml | 44 ++++++ app/src/main/res/values-be/strings.xml | 210 +++++++++++++++++++++++++ app/src/main/res/values-ru/strings.xml | 6 +- 3 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/values-be/plurals.xml create mode 100644 app/src/main/res/values-be/strings.xml diff --git a/app/src/main/res/values-be/plurals.xml b/app/src/main/res/values-be/plurals.xml new file mode 100644 index 000000000..290e7a9a8 --- /dev/null +++ b/app/src/main/res/values-be/plurals.xml @@ -0,0 +1,44 @@ + + + + Усяго %1$d старонка + Усяго %1$d старонкі + Усяго %1$d старонак + + + %1$d элемент + %1$d элемента + %1$d элементаў + + + %1$d новая частка + %1$d новых часткi + %1$d новых частак + + + %1$d частка + %1$d часткi + %1$d частак + + + %1$d частка з %2$d + %1$d часткi з %2$d + %1$d частак з %2$d + + + + %1$d хвіліну таму + %1$d хвіліны таму + %1$d хвілін таму + + + %1$d гадзіну таму + %1$d гадзіны таму + %1$d гадзін таму + + + %1$d дзень таму + %1$d дні таму + %1$d дзён там + + \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml new file mode 100644 index 000000000..e45ea42ec --- /dev/null +++ b/app/src/main/res/values-be/strings.xml @@ -0,0 +1,210 @@ + + + Зачыніць меню + Адкрыць меню + На прыладзе + Абраныя + Гісторыя + Адбылася памылка + Памылка сеткі + Падрабязнасцi + Часткi + Спіс + Падрабязны спіс + Табліца + Выгляд спісу + Наладжвання + Онлайн каталогі + Загрузка… + Частка %1$d з %2$d + Зачыніць + Паўтарыць + Ачысціць гісторыю + Нічога не знойдзена + Гісторыя пустая + Чытаць + Дадаць закладку + Дадайце цікавую для вас мангу ў выбранае, каб не страціць яе + Дадаць у абранае + Стварыць катэгорыю + Дадаць + Увядзіце назву + Захаваць + Падзяліцца + Стварыць ярлык… + Падзялiцца %s + Пошук + Пошук мангі + Загрузка мангі… + Апрацоўка… + Загрузка завершана + Загрузкi + Па імя + Папулярная + Абноўленая + Новая + Па рэйтынгу + Усе + Сартаванне + Жанр + Фільтр + Тэма + Светлая + Цёмная + Аўтаматычна + Старонкi + Ачысціць + Вы ўпэўненыя, што жадаеце ачысціць гісторыю? Гэта дзеянне нельга будзе адмяніць. + Выдаліць + \"%s\" выдалена з гiсторыi + \"%s\" выдалена з прылады + Дачакайцеся заканчэння загрузкі + Захаваць старонку + Старонка захавана + Падзяліцца выявай + Імпарт + Выдаліць + Аперацыя не падтрымліваецца + Падтрымліваюцца толькі ZIP файлы і CBZ. + Няма апісання + Гісторыя і кэш + Ачысціць кэш старонак + Кэш + Б|кБ|МБ|ГБ|ТБ + Стандартны + Манхва + Рэжым чытання + Памер табліцы + Вынікі пошуку па %s + Пошук па %s + Выдаліць мангу + Настаўленні чытання + Гартанне старонак + Вы ўпэўненыя, што жадаеце выдаліць \"%s\" з прылады? Гэта дзеянне нельга будзе адмяніць. + Націску па краях + Кнопкі гучнасці + Прадоўжыць + Папярэджанне + Дадзеная аперацыя можа прывесці да вялікага выдатку трафіку + Больш не пытацца + Адмена… + Памылка + Ачысціць кэш мініяцюр + Гісторыя пошуку ачышчана + Ачысціць гісторыю пошуку + Толькі жэсты + Унутраны назапашвальнік + Знешняе сховішча + Дамен + Безумоўна + Правяраць абнаўленне прыкладання + Даступна абнаўленне прыкладання + Паказваць апавяшчэнне пры наяўнасці новай версіі + Адкрыць у браўзэры + У гэтай манге %s. Вы ўпэўненыя, што хочаце захаваць іх усё? + Захаваць мангу + Паведамлення + Уключана %1$d з %2$d + Новыя часткi + Апавяшчаць аб абнаўленні мангі, якую вы чытаеце + Загрузіць + Чытаць з пачатку + Перазапусціць + Налады апавяшчэнняў + Гук апавяшчэння + Светлавая iндыкацыя + Вібрацыя + Катэгорыі абранага + Катэгорыi… + Перайменаваць + Вы ўпэўненыя, што хочаце выдаліць катэгорыю \"%s\"? Уся манга з дадзенай катэгорыі будзе страчана. + Выдаліць катэгорыю + Катэгорыі дапамагаюць парадкаваць выбраную мангу. Націсніце «+», каб стварыць катэгорыю + Тут будзе оборажаться манга, якую вы чытаеце. Вы можаце знайсці, што пачытаць, у бакавым меню + У вас пакуль няма захаванай мангі. Вы можаце захаваць мангу з онлайн каталога або імпартаваць з файла + Полка з мангай + Нядаўняя манга + Анімацыя гартання + Месца захавання мангі + Недаступна + Не атрымалася знайсці ні аднаго даступнага сховішчы + Іншае сховішча + Абароненае злучэнне (HTTPS) + Гатова + Усё выбранае + У гэтай катэгорыі нічога няма + Прачытаць пазней + Абнаўлення + Тут будуць адлюстроўвацца абнаўлення мангі, якую вы чытаеце + Вынікі пошуку + Падобныя + Новая версія: %s + Памер: %s + Чаканне падлучэння… + Ачысціць стужку абнаўленняў + Стужка абнаўленняў ачышчана + Павярнуць экран + Абнавіць + Абнаўленне хутка пачнецца + Правяраць абнаўлення мангі + Не правяраць + Увядзіце пароль + Няправільны пароль + Абараніць прыкладанне + Запытваць пароль пры запуску прыкладання + Паўтарыце пароль + Паролі не супадаюць + Аб праграме + Версія %s + Праверыць абнаўлення + Праверка абнаўлення… + Памылка пры праверцы абнаўлення + Няма даступных абнаўленняў + Справа налева + Аддаваць перавагу рэжым Справа налева + Вы можаце наладзіць рэжым чытання для кожнай мангі асобна + Стварыць катэгорыю + Стварыць праблему на GitHub + Маштабаванне + Ўпісаць у экран + Падагнаць па вышыні + Падагнаць па шырыні + Зыходны памер + Чорная цёмная тэма + Карысна для AMOLED экранаў + Патрабуецца перазапуск + Рэзервовае капіраванне + Стварыць рэзервовую копію + Аднавіць дадзеныя + Дадзеныя адноўлены + Падрыхтоўка… + Файл не знойдзены + Усе дадзеныя паспяхова адноўлены + Дадзеныя адноўлены, але ўзніклі некаторыя памылкі + Вы можаце стварыць рэзервовую копію абранага і гісторыі і потым аднавіць іх + Толькі што + Учора + Даўно + Групаваць + Сёння + Паспрабаваць яшчэ раз + Абраны рэжым будзе захаваны для бягучай мангі + Без гуку + Неабходна прайсці CAPTCHA + Прайсці + Ачысціць кукi + Усе кукi выдалены + Праверка новых частак: %1$d з %2$d + Ачысціць стужку + Уся гісторыя абнаўленняў будзе ачышчана і яе нельга будзе вярнуць. Вы ўпэўненыя? + Праверка новых частак + У зваротным парадку + Увайсці + Для прагляду гэтага кантэнту патрабуецца аўтарызацыя + Прадвызначаны: %s + …і яшчэ %1$d + Далей + Калі ласка, увядзіце пароль, які спатрэбіцца пры запуску прыкладання + Пацвярджаць + Пароль павінен змяшчаць не менш за 4 сімвалаў + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 68cbcb6e9..36eddaf03 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -63,7 +63,7 @@ Поделиться изображением Импорт Удалить - Операция не пожжерживается + Операция не поддерживается Поддерживаются только файлы ZIP и CBZ. Нет описания История и кэш @@ -202,7 +202,7 @@ По умолчанию: %s …и ещё %1$d Далее - Enter password that will be required when the application starts + Введите пароль, который вам понадобится при запуске приложения Confirm - Password must be at least 4 characters + Пароль должен содержать не менее 4 символов \ No newline at end of file From 0559c13dc6f7f40de2677ee7e5d34ddf04858b84 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko <61558546+ztimms73@users.noreply.github.com> Date: Thu, 13 May 2021 20:29:01 +0300 Subject: [PATCH 003/138] Fix --- app/src/main/res/values-be/plurals.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-be/plurals.xml b/app/src/main/res/values-be/plurals.xml index 290e7a9a8..ca76348da 100644 --- a/app/src/main/res/values-be/plurals.xml +++ b/app/src/main/res/values-be/plurals.xml @@ -39,6 +39,6 @@ %1$d дзень таму %1$d дні таму - %1$d дзён там + %1$d дзён таму - \ No newline at end of file + From d1921193f08efe3106ca72a81dde4d8f05bdf123 Mon Sep 17 00:00:00 2001 From: ztimms73 Date: Fri, 14 May 2021 22:42:43 +0300 Subject: [PATCH 004/138] Add Anibel source --- .../kotatsu/core/model/MangaSource.kt | 3 +- .../kotatsu/core/parser/ParserModule.kt | 1 + .../core/parser/site/AnibelRepository.kt | 182 ++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index ac67f280d..9d35e9df5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -30,7 +30,8 @@ enum class MangaSource( // NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java), MANGAREAD("MangaRead", "en", MangareadRepository::class.java), REMANGA("Remanga", "ru", RemangaRepository::class.java), - HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java); + HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java), + ANIBEL("Anibel", "be", AnibelRepository::class.java); @get:Throws(NoBeanDefFoundException::class) @Deprecated("") diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt index b5b3f431c..09ae0c43c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt @@ -24,4 +24,5 @@ val parserModule factory(named(MangaSource.MANGAREAD)) { MangareadRepository(get()) } factory(named(MangaSource.REMANGA)) { RemangaRepository(get()) } factory(named(MangaSource.HENTAILIB)) { HentaiLibRepository(get()) } + factory(named(MangaSource.ANIBEL)) { AnibelRepository(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt new file mode 100644 index 000000000..574f1a42e --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt @@ -0,0 +1,182 @@ +package org.koitharu.kotatsu.core.parser.site + +import org.koitharu.kotatsu.base.domain.MangaLoaderContext +import org.koitharu.kotatsu.core.exceptions.ParseException +import org.koitharu.kotatsu.core.model.* +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.utils.ext.* +import java.util.* + +class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) { + + override val source = MangaSource.ANIBEL + + override val defaultDomain = "anibel.net" + + override val sortOrders: Set = EnumSet.of( + SortOrder.UPDATED + ) + + override suspend fun getList( + offset: Int, + query: String?, + sortOrder: SortOrder?, + tag: MangaTag? + ): List { + if (!query.isNullOrEmpty()) { + return if (offset == 0) search(query) else emptyList() + } + val page = (offset / 12).inc() + val tagPage = offset.inc() //TODO Load another pages + val link = buildString { + append("https://") + append(getDomain()) + append("/manga") + if (tag != null) { + append("?genre[]=") + append(tag.key) + append("&page=") + append(tagPage) + } else { + append("?page=") + append(page) + } + } + val doc = loaderContext.httpGet(link).parseHtml() + val root = doc.body().select("div.manga-block") ?: throw ParseException("Cannot find root") + val items = root.select("div.anime-card") + return items.mapNotNull { card -> + val href = card.select("a").attr("href") + val url = buildString { + append("https://") + append(getDomain()) + append("/") + } + val status = card.select("tr")[2].text() + Manga( + id = generateUid(href), + title = card.selectFirst("h1.anime-card-title").text(), + coverUrl = url + card.selectFirst("img").attr("data-src"), + altTitle = null, + author = null, + rating = Manga.NO_RATING, + url = url + href, + publicUrl = "", + state = when (status) { + "выпускаецца" -> MangaState.ONGOING + "завершанае" -> MangaState.FINISHED + else -> null + }, + source = source + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = loaderContext.httpGet(manga.url.withDomain()).parseHtml() + val root = doc.body().select("div.container") ?: throw ParseException("Cannot find root") + return manga.copy( + description = root.select("div.manga-block.grid-12")[2].select("p").text(), + largeCoverUrl = manga.coverUrl, + chapters = root.select("ul.series").flatMap { table -> + table.select("li") + }.map { it.selectFirst("a") }.mapIndexedNotNull { i, a -> + val url = buildString { + append("https://") + append(getDomain()) + } + val href = url + a.select("a").first().attr("href") + MangaChapter( + id = generateUid(href), + name = a.select("a").first().text(), + number = i + 1, + url = href, + source = source + ) + } + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.withDomain() + val doc = loaderContext.httpGet(fullUrl).parseHtml() + val scripts = doc.select("script") + for (script in scripts) { + val data = script.html() + val pos = data.indexOf("dataSource") + if (pos == -1) { + continue + } + val json = data.substring(pos).substringAfter('[').substringBefore(']') + val domain = getDomain() + return json.split(",").mapNotNull { + it.trim() + .removeSurrounding('"', '\'') + .toRelativeUrl(domain) + .takeUnless(String::isBlank) + }.map { url -> + MangaPage( + id = generateUid(url), + url = url, + referer = fullUrl, + source = source + ) + } + } + throw ParseException("Pages list not found at ${chapter.url.withDomain()}") + } + + override suspend fun getTags(): Set { + val doc = loaderContext.httpGet("https://${getDomain()}/manga").parseHtml() + val root = doc.body().select("div#tabs-genres").select("ul#list.ul-three-colums") + return root.select("p.menu-tags.tupe").mapToSet { a -> + MangaTag( + title = a.select("a").text().capitalize(Locale.ROOT), + key = a.select("a").attr("data-name"), + source = source + ) + } + } + + private suspend fun search(query: String): List { + val domain = getDomain() + val doc = loaderContext.httpGet("https://$domain/search?q=$query").parseHtml() + val root = doc.body().select("div.manga-block") ?: throw ParseException("Cannot find root") + val items = root.select("div.anime-card") + return items.mapNotNull { card -> + val href = card.select("a").attr("href") + val url = buildString { + append("https://") + append(getDomain()) + } + val status = card.select("tr")[2].text() + Manga( + id = generateUid(href), + title = card.selectFirst("h1.anime-card-title").text(), + coverUrl = url + card.selectFirst("img").attr("src"), + altTitle = null, + author = null, + rating = Manga.NO_RATING, + url = url + href, + publicUrl = "", + tags = runCatching { + card?.select("p.tupe.tag") + ?.mapToSet { + MangaTag( + title = it.select("a").text(), + key = it.attr("href"), + source = source + ) + } + }.getOrNull().orEmpty(), + state = when (status) { + "выпускаецца" -> MangaState.ONGOING + "завершанае" -> MangaState.FINISHED + else -> null + }, + source = source + ) + } + } + +} \ No newline at end of file From 46891aa958114d7d0b78c49fac4b02cb7da19979 Mon Sep 17 00:00:00 2001 From: ztimms73 Date: Fri, 14 May 2021 22:43:14 +0300 Subject: [PATCH 005/138] Minor UI changes --- app/src/main/res/color/stroke_color.xml | 5 +++++ app/src/main/res/layout/item_manga_grid.xml | 6 +++++- app/src/main/res/layout/item_manga_list_details.xml | 5 ++++- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/color/stroke_color.xml diff --git a/app/src/main/res/color/stroke_color.xml b/app/src/main/res/color/stroke_color.xml new file mode 100644 index 000000000..daeffa86f --- /dev/null +++ b/app/src/main/res/color/stroke_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_manga_grid.xml b/app/src/main/res/layout/item_manga_grid.xml index ea10b632e..9727f3b88 100644 --- a/app/src/main/res/layout/item_manga_grid.xml +++ b/app/src/main/res/layout/item_manga_grid.xml @@ -2,8 +2,12 @@ + android:layout_height="wrap_content" + app:strokeWidth="1dp" + app:strokeColor="@color/stroke_color" + app:cardElevation="0dp"> + android:layout_height="@dimen/manga_list_details_item_height" + app:strokeWidth="1dp" + app:strokeColor="@color/stroke_color" + app:cardElevation="0dp"> Date: Sat, 15 May 2021 08:56:03 +0300 Subject: [PATCH 006/138] Added the option to hide or not toolbar when scrolling --- .../java/org/koitharu/kotatsu/base/ui/BaseActivity.kt | 8 ++++++++ .../java/org/koitharu/kotatsu/core/prefs/AppSettings.kt | 3 +++ .../org/koitharu/kotatsu/settings/MainSettingsFragment.kt | 3 +++ app/src/main/res/values-be/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_main.xml | 6 ++++++ 7 files changed, 23 insertions(+) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt index b695d0bb2..d032ec3e9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt @@ -15,6 +15,8 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.Insets import androidx.core.view.* import androidx.viewbinding.ViewBinding +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.appbar.AppBarLayout.LayoutParams.* import org.koin.android.ext.android.get import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R @@ -57,7 +59,13 @@ abstract class BaseActivity : AppCompatActivity(), OnApplyWindo this.binding = binding super.setContentView(binding.root) (binding.root.findViewById(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar) + val params = (binding.root.findViewById(R.id.toolbar) as? Toolbar)?.layoutParams as AppBarLayout.LayoutParams ViewCompat.setOnApplyWindowInsetsListener(binding.root, this) + if (get().isToolbarHideWhenScrolling) { + params.scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS + } else { + params.scrollFlags = SCROLL_FLAG_NO_SCROLL + } } override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 2c2839576..7fc34cc4e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -41,6 +41,8 @@ class AppSettings private constructor(private val prefs: SharedPreferences) : val isAmoledTheme by BoolPreferenceDelegate(KEY_THEME_AMOLED, defaultValue = false) + val isToolbarHideWhenScrolling by BoolPreferenceDelegate(KEY_HIDE_TOOLBAR, defaultValue = true) + var gridSize by IntPreferenceDelegate(KEY_GRID_SIZE, defaultValue = 100) val readerPageSwitch by StringSetPreferenceDelegate( @@ -147,6 +149,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) : const val KEY_APP_SECTION = "app_section" const val KEY_THEME = "theme" const val KEY_THEME_AMOLED = "amoled_theme" + const val KEY_HIDE_TOOLBAR = "hide_toolbar" const val KEY_SOURCES_ORDER = "sources_order" const val KEY_SOURCES_HIDDEN = "sources_hidden" const val KEY_TRAFFIC_WARNING = "traffic_warning" diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt index c1a4a104d..388059e99 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt @@ -73,6 +73,9 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), AppSettings.KEY_THEME_AMOLED -> { findPreference(key)?.setSummary(R.string.restart_required) } + AppSettings.KEY_HIDE_TOOLBAR -> { + findPreference(key)?.setSummary(R.string.restart_required) + } AppSettings.KEY_LOCAL_STORAGE -> { findPreference(key)?.run { summary = settings.getStorageDir(context)?.getStorageName(context) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index e45ea42ec..926f62a66 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -207,4 +207,5 @@ Калі ласка, увядзіце пароль, які спатрэбіцца пры запуску прыкладання Пацвярджаць Пароль павінен змяшчаць не менш за 4 сімвалаў + Схаваць загаловак пры прагортцы \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 36eddaf03..3765820e9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -205,4 +205,5 @@ Введите пароль, который вам понадобится при запуске приложения Confirm Пароль должен содержать не менее 4 символов + Прятать заголовок при прокрутке \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 161e209c5..6f5e7da6f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -207,4 +207,5 @@ Enter password that will be required when the application starts Confirm Password must be at least 4 characters + Hide toolbar when scrolling \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 9d69e6c5f..a8613f904 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -20,6 +20,12 @@ android:title="@string/black_dark_theme" app:iconSpaceReserved="false" /> + + Date: Sat, 15 May 2021 10:28:23 +0300 Subject: [PATCH 007/138] Fix Remanga (closes #28) --- .../koitharu/kotatsu/core/parser/site/RemangaRepository.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt index 79d0d2581..f368db41e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt @@ -18,10 +18,9 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito override val defaultDomain = "remanga.org" override val sortOrders: Set = EnumSet.of( + SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.RATING, - SortOrder.ALPHABETICAL, - SortOrder.UPDATED, SortOrder.NEWEST ) @@ -162,7 +161,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito SortOrder.POPULARITY -> "-rating" SortOrder.RATING -> "-votes" SortOrder.NEWEST -> "-id" - else -> "-rating" + else -> "-chapter_date" } private fun parsePage(jo: JSONObject, referer: String) = MangaPage( From 98147d0a8138a72d66429fd9a1bdc3b22cfed553 Mon Sep 17 00:00:00 2001 From: ztimms73 Date: Sat, 15 May 2021 22:05:32 +0300 Subject: [PATCH 008/138] Fix Anibel issues --- .../core/parser/site/AnibelRepository.kt | 77 +++++++------------ 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt index 574f1a42e..5013c76e5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt @@ -14,7 +14,7 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor override val defaultDomain = "anibel.net" override val sortOrders: Set = EnumSet.of( - SortOrder.UPDATED + SortOrder.NEWEST ) override suspend fun getList( @@ -26,42 +26,33 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor if (!query.isNullOrEmpty()) { return if (offset == 0) search(query) else emptyList() } - val page = (offset / 12).inc() - val tagPage = offset.inc() //TODO Load another pages - val link = buildString { - append("https://") - append(getDomain()) - append("/manga") - if (tag != null) { - append("?genre[]=") - append(tag.key) - append("&page=") - append(tagPage) - } else { - append("?page=") - append(page) - } + val page = (offset / 12f).toIntUp().inc() + val link = when { + tag != null -> "/manga?genre[]=${tag.key}&page=$page".withDomain() + else -> "/manga?page=$page".withDomain() } val doc = loaderContext.httpGet(link).parseHtml() val root = doc.body().select("div.manga-block") ?: throw ParseException("Cannot find root") val items = root.select("div.anime-card") return items.mapNotNull { card -> - val href = card.select("a").attr("href") - val url = buildString { - append("https://") - append(getDomain()) - append("/") - } + val href = card.selectFirst("a").attr("href") val status = card.select("tr")[2].text() Manga( id = generateUid(href), title = card.selectFirst("h1.anime-card-title").text(), - coverUrl = url + card.selectFirst("img").attr("data-src"), + coverUrl = card.selectFirst("img").attr("data-src").withDomain(), altTitle = null, author = null, rating = Manga.NO_RATING, - url = url + href, - publicUrl = "", + url = href, + publicUrl = href.withDomain(), + tags = card.select("p.tupe.tag")?.select("a")?.mapNotNullToSet tags@{ x -> + MangaTag( + title = x.text(), + key = x.attr("href") ?: return@tags null, + source = source + ) + }.orEmpty(), state = when (status) { "выпускаецца" -> MangaState.ONGOING "завершанае" -> MangaState.FINISHED @@ -73,19 +64,14 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor } override suspend fun getDetails(manga: Manga): Manga { - val doc = loaderContext.httpGet(manga.url.withDomain()).parseHtml() + val doc = loaderContext.httpGet(manga.publicUrl).parseHtml() val root = doc.body().select("div.container") ?: throw ParseException("Cannot find root") return manga.copy( description = root.select("div.manga-block.grid-12")[2].select("p").text(), - largeCoverUrl = manga.coverUrl, chapters = root.select("ul.series").flatMap { table -> table.select("li") }.map { it.selectFirst("a") }.mapIndexedNotNull { i, a -> - val url = buildString { - append("https://") - append(getDomain()) - } - val href = url + a.select("a").first().attr("href") + val href = a.select("a").first().attr("href").toRelativeUrl(getDomain()) MangaChapter( id = generateUid(href), name = a.select("a").first().text(), @@ -145,30 +131,23 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor val items = root.select("div.anime-card") return items.mapNotNull { card -> val href = card.select("a").attr("href") - val url = buildString { - append("https://") - append(getDomain()) - } val status = card.select("tr")[2].text() Manga( id = generateUid(href), title = card.selectFirst("h1.anime-card-title").text(), - coverUrl = url + card.selectFirst("img").attr("src"), + coverUrl = card.selectFirst("img").attr("src").withDomain(), altTitle = null, author = null, rating = Manga.NO_RATING, - url = url + href, - publicUrl = "", - tags = runCatching { - card?.select("p.tupe.tag") - ?.mapToSet { - MangaTag( - title = it.select("a").text(), - key = it.attr("href"), - source = source - ) - } - }.getOrNull().orEmpty(), + url = href, + publicUrl = href.withDomain(), + tags = card.select("p.tupe.tag")?.select("a")?.mapNotNullToSet tags@{ x -> + MangaTag( + title = x.text(), + key = x.attr("href") ?: return@tags null, + source = source + ) + }.orEmpty(), state = when (status) { "выпускаецца" -> MangaState.ONGOING "завершанае" -> MangaState.FINISHED From 4d535cef4186422fb0e11a7b1d00a6f679f74eec Mon Sep 17 00:00:00 2001 From: ztimms73 Date: Sat, 15 May 2021 22:51:45 +0300 Subject: [PATCH 009/138] Avoid searching for anime on Anibel --- .../org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt index 5013c76e5..318fcad02 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt @@ -127,7 +127,7 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor private suspend fun search(query: String): List { val domain = getDomain() val doc = loaderContext.httpGet("https://$domain/search?q=$query").parseHtml() - val root = doc.body().select("div.manga-block") ?: throw ParseException("Cannot find root") + val root = doc.body().select("div.manga-block").select("article.tab-2") ?: throw ParseException("Cannot find root") val items = root.select("div.anime-card") return items.mapNotNull { card -> val href = card.select("a").attr("href") From b009a6423de12ad7e5852b44b10dafa387b890ea Mon Sep 17 00:00:00 2001 From: ztimms73 Date: Tue, 18 May 2021 11:17:00 +0300 Subject: [PATCH 010/138] Fix some problems from issue #23 --- app/src/main/res/layout/dialog_chapters.xml | 1 + app/src/main/res/layout/fragment_details.xml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/dialog_chapters.xml b/app/src/main/res/layout/dialog_chapters.xml index e53b0af80..58fdf931d 100644 --- a/app/src/main/res/layout/dialog_chapters.xml +++ b/app/src/main/res/layout/dialog_chapters.xml @@ -7,6 +7,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" + android:paddingTop="16dp" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/item_chapter" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index e632e28f8..fbb55de96 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -129,7 +129,6 @@ android:id="@+id/textView_description" android:layout_width="0dp" android:layout_height="wrap_content" - android:justificationMode="inter_word" android:lineSpacingMultiplier="1.2" android:padding="12dp" android:textIsSelectable="true" From d65158b7b99773e0c0fd9eec48b91295efab58aa Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 20 May 2021 08:09:35 +0300 Subject: [PATCH 011/138] Update Kotlin to 1.5 and dependencies --- app/build.gradle | 20 +++++++++---------- .../kotatsu/browser/BrowserActivity.kt | 2 +- .../kotatsu/details/ui/DetailsActivity.kt | 2 +- .../kotatsu/reader/ui/ReaderActivity.kt | 2 +- .../widget/shelf/ShelfConfigActivity.kt | 2 +- build.gradle | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c206ffcb3..9e18f5bc5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -61,12 +61,12 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' - implementation 'androidx.core:core-ktx:1.5.0-rc01' - implementation 'androidx.activity:activity-ktx:1.2.2' - implementation 'androidx.fragment:fragment-ktx:1.3.3' + implementation 'androidx.core:core-ktx:1.5.0' + implementation 'androidx.activity:activity-ktx:1.2.3' + implementation 'androidx.fragment:fragment-ktx:1.3.4' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' @@ -93,15 +93,15 @@ dependencies { implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.0' - implementation 'io.insert-koin:koin-android:3.0.1' - implementation 'io.insert-koin:koin-android-ext:3.0.1' - implementation 'io.coil-kt:coil-base:1.1.1' + implementation 'io.insert-koin:koin-android:3.0.2' + implementation 'io.insert-koin:koin-android-ext:3.0.2' + implementation 'io.coil-kt:coil-base:1.2.1' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.2' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.json:json:20201115' - testImplementation 'io.insert-koin:koin-test-junit4:3.0.1' + testImplementation 'org.json:json:20210307' + testImplementation 'io.insert-koin:koin-test-junit4:3.0.2' } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt b/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt index 867c5e3fc..e5be6808c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt @@ -41,7 +41,7 @@ class BrowserActivity : BaseActivity(), BrowserCallback } } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.opt_browser, menu) return super.onCreateOptionsMenu(menu) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index eacf132f9..8cc025234 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -113,7 +113,7 @@ class DetailsActivity : BaseActivity(), } } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.opt_details, menu) return super.onCreateOptionsMenu(menu) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 5b53b41ff..22d66a6c6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -124,7 +124,7 @@ class ReaderActivity : BaseFullscreenActivity(), } } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.opt_reader_top, menu) return super.onCreateOptionsMenu(menu) } diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt index fabf1fbba..1946fb0dd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt @@ -58,7 +58,7 @@ class ShelfConfigActivity : BaseActivity(), OnListIte viewModel.onError.observe(this, this::onError) } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.opt_config, menu) return super.onCreateOptionsMenu(menu) } diff --git a/build.gradle b/build.gradle index c92340660..144344709 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:4.1.3' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From f0c9c61b49055c9f311b16b582a9469379499bc2 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 20 May 2021 08:12:09 +0300 Subject: [PATCH 012/138] Fix issues with spanish translation --- app/src/main/res/values-es/strings.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3aab6627b..8786b537d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,4 +1,4 @@ - + Cerrar menú Abrir menú Dispositivo @@ -14,7 +14,7 @@ Vista de lista Ajustes Fuentes remotas - Cargando... + Cargando… Capítulo %1$d de %2$d Cerrar Reintentar @@ -86,7 +86,7 @@ Advertencia Esta operación puede consumir mucho tráfico de red No volver a preguntar - Cancelar... + Cancelar… Error Borrar la caché de miniaturas Borrar el historial de búsqueda @@ -112,9 +112,9 @@ Configuración de las notificaciones Sonido de las notificaciones Indicador de luz - Vibración - Categorías favoritas - Categorías... + Vibración + Categorías favoritas + Categorías… Renombrar ¿Realmente quieres eliminar la categoría \"%s\" de tus favoritos? \nTodo el manga en ella se perderá. Quitar categoría @@ -139,7 +139,7 @@ Relación Nueva versión: %s Tamaño: %s - Esperando red... + Esperando red… Borrar actualizaciones Borrar el feed de actualizaciones Girar pantalla @@ -156,7 +156,7 @@ Acerca de Versión %s Comprobar actualizaciones - Comprobar si hay actualizaciones... + Comprobar si hay actualizaciones… Fallo en la comprobación de actualizaciones No hay actualizaciones disponibles Derecha a izquierda @@ -176,7 +176,7 @@ Crear copia de seguridad de datos Restaurar desde la copia de seguridad Datos restaurados - Preparando... + Preparando… Archivo no encontrado Todos los datos fueron restaurados con éxito Los datos fueron restaurados, pero hay errores @@ -201,7 +201,7 @@ Iniciar sesión Debes autorizar para ver este contenido Por defecto: %s - ...y %1$d más + …y %1$d más Siguiente Introduce la contraseña que se requerirá cuando se inicie la aplicación Confirmar From 91619cc2599c18fe574a0e449cc07c8261abb9d3 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 23 May 2021 14:52:21 +0300 Subject: [PATCH 013/138] NineManga sources #14 --- .idea/codeStyles/Project.xml | 1 + .../kotatsu/base/domain/MangaLoaderContext.kt | 5 +- .../browser/cloudflare/CloudFlareClient.kt | 4 +- .../kotatsu/core/model/MangaSource.kt | 10 +- .../{CookieJar.kt => AndroidCookieJar.kt} | 6 +- .../core/network/CurlLoggingInterceptor.kt | 59 +++++ .../kotatsu/core/network/NetworkModule.kt | 6 +- .../kotatsu/core/parser/ParserModule.kt | 7 + .../core/parser/site/NineMangaRepository.kt | 201 ++++++++++++++++++ .../settings/HistorySettingsFragment.kt | 4 +- .../koitharu/kotatsu/utils/ext/StringExt.kt | 34 ++- .../kotatsu/parsers/TemporaryCookieJar.kt | 2 +- 12 files changed, 325 insertions(+), 14 deletions(-) rename app/src/main/java/org/koitharu/kotatsu/core/network/{CookieJar.kt => AndroidCookieJar.kt} (90%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/network/CurlLoggingInterceptor.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 87bd3a768..bda8001d6 100755 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -23,6 +23,7 @@ + diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt index 6d9c92ed3..1dbf9de34 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt @@ -12,10 +12,13 @@ open class MangaLoaderContext( private val cookieJar: CookieJar ) : KoinComponent { - suspend fun httpGet(url: String): Response { + suspend fun httpGet(url: String, headers: Headers? = null): Response { val request = Request.Builder() .get() .url(url) + if (headers != null) { + request.headers(headers) + } return okHttp.newCall(request.build()).await() } diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt index ba9ab7851..fe514b6d8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt +++ b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt @@ -3,11 +3,11 @@ package org.koitharu.kotatsu.browser.cloudflare import android.graphics.Bitmap import android.webkit.WebView import okhttp3.HttpUrl.Companion.toHttpUrl -import org.koitharu.kotatsu.core.network.CookieJar +import org.koitharu.kotatsu.core.network.AndroidCookieJar import org.koitharu.kotatsu.core.network.WebViewClientCompat class CloudFlareClient( - private val cookieJar: CookieJar, + private val cookieJar: AndroidCookieJar, private val callback: CloudFlareCallback, private val targetUrl: String ) : WebViewClientCompat() { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index 9d35e9df5..41476a63b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -31,7 +31,15 @@ enum class MangaSource( MANGAREAD("MangaRead", "en", MangareadRepository::class.java), REMANGA("Remanga", "ru", RemangaRepository::class.java), HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java), - ANIBEL("Anibel", "be", AnibelRepository::class.java); + ANIBEL("Anibel", "be", AnibelRepository::class.java), + NINEMANGA_EN("NineManga English", "en", NineMangaRepository.English::class.java), + NINEMANGA_ES("NineManga Español", "es", NineMangaRepository.Spanish::class.java), + NINEMANGA_RU("NineManga Русский", "ru", NineMangaRepository.Russian::class.java), + NINEMANGA_DE("NineManga Deutsch", "de", NineMangaRepository.Deutsch::class.java), + NINEMANGA_IT("NineManga Italiano", "it", NineMangaRepository.Italiano::class.java), + NINEMANGA_BR("NineManga Brasil", "br", NineMangaRepository.Brazil::class.java), + NINEMANGA_FR("NineManga Français", "fr", NineMangaRepository.Francais::class.java), + ; @get:Throws(NoBeanDefFoundException::class) @Deprecated("") diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/CookieJar.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/AndroidCookieJar.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/core/network/CookieJar.kt rename to app/src/main/java/org/koitharu/kotatsu/core/network/AndroidCookieJar.kt index 4740e8b13..fb806bda1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/CookieJar.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/AndroidCookieJar.kt @@ -7,7 +7,7 @@ import okhttp3.HttpUrl import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -class CookieJar : CookieJar { +class AndroidCookieJar : CookieJar { private val cookieManager = CookieManager.getInstance() @@ -28,10 +28,6 @@ class CookieJar : CookieJar { } } - fun clearAsync() { - cookieManager.removeAllCookies(null) - } - suspend fun clear() = suspendCoroutine { continuation -> cookieManager.removeAllCookies(continuation::resume) } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/CurlLoggingInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/CurlLoggingInterceptor.kt new file mode 100644 index 000000000..b3fa833cd --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/CurlLoggingInterceptor.kt @@ -0,0 +1,59 @@ +package org.koitharu.kotatsu.core.network + +import android.util.Log +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import okio.Buffer +import java.io.IOException +import java.nio.charset.StandardCharsets + +class CurlLoggingInterceptor( + private val extraCurlOptions: String? = null, +) : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request: Request = chain.request() + var compressed = false + val curlCmd = StringBuilder("curl") + if (extraCurlOptions != null) { + curlCmd.append(" ").append(extraCurlOptions) + } + curlCmd.append(" -X ").append(request.method) + val headers = request.headers + var i = 0 + val count = headers.size + while (i < count) { + val name = headers.name(i) + val value = headers.value(i) + if ("Accept-Encoding".equals(name, ignoreCase = true) && "gzip".equals(value, + ignoreCase = true) + ) { + compressed = true + } + curlCmd.append(" -H " + "\"").append(name).append(": ").append(value).append("\"") + i++ + } + val requestBody = request.body + if (requestBody != null) { + val buffer = Buffer() + requestBody.writeTo(buffer) + val contentType = requestBody.contentType() + val charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8 + curlCmd.append(" --data $'") + .append(buffer.readString(charset).replace("\n", "\\n")) + .append("'") + } + curlCmd.append(if (compressed) " --compressed " else " ").append(request.url) + Log.d(TAG, "╭--- cURL (" + request.url + ")") + Log.d(TAG, curlCmd.toString()) + Log.d(TAG, "╰--- (copy and paste the above line to a terminal)") + return chain.proceed(request) + } + + private companion object { + + const val TAG = "CURL" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt index 6691dd50c..4d5c26d3b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt @@ -6,12 +6,13 @@ import org.koin.android.ext.koin.androidContext import org.koin.core.qualifier.named import org.koin.dsl.bind import org.koin.dsl.module +import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.utils.CacheUtils import java.util.concurrent.TimeUnit val networkModule get() = module { - single { CookieJar() } bind CookieJar::class + single { AndroidCookieJar() } bind CookieJar::class single(named(CacheUtils.QUALIFIER_HTTP)) { CacheUtils.createHttpCache(androidContext()) } single { OkHttpClient.Builder().apply { @@ -22,6 +23,9 @@ val networkModule cache(get(named(CacheUtils.QUALIFIER_HTTP))) addInterceptor(UserAgentInterceptor()) addInterceptor(CloudFlareInterceptor()) + if (BuildConfig.DEBUG) { + addNetworkInterceptor(CurlLoggingInterceptor()) + } }.build() } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt index 09ae0c43c..baf3156e3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt @@ -25,4 +25,11 @@ val parserModule factory(named(MangaSource.REMANGA)) { RemangaRepository(get()) } factory(named(MangaSource.HENTAILIB)) { HentaiLibRepository(get()) } factory(named(MangaSource.ANIBEL)) { AnibelRepository(get()) } + factory(named(MangaSource.NINEMANGA_EN)) { NineMangaRepository.English(get()) } + factory(named(MangaSource.NINEMANGA_BR)) { NineMangaRepository.Brazil(get()) } + factory(named(MangaSource.NINEMANGA_DE)) { NineMangaRepository.Deutsch(get()) } + factory(named(MangaSource.NINEMANGA_ES)) { NineMangaRepository.Spanish(get()) } + factory(named(MangaSource.NINEMANGA_RU)) { NineMangaRepository.Russian(get()) } + factory(named(MangaSource.NINEMANGA_IT)) { NineMangaRepository.Italiano(get()) } + factory(named(MangaSource.NINEMANGA_FR)) { NineMangaRepository.Francais(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt new file mode 100644 index 000000000..aca8baca3 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt @@ -0,0 +1,201 @@ +package org.koitharu.kotatsu.core.parser.site + +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.koitharu.kotatsu.base.domain.MangaLoaderContext +import org.koitharu.kotatsu.core.exceptions.ParseException +import org.koitharu.kotatsu.core.model.* +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.utils.ext.* +import java.util.* + +abstract class NineMangaRepository( + loaderContext: MangaLoaderContext, + override val source: MangaSource, + override val defaultDomain: String, +) : RemoteMangaRepository(loaderContext) { + + init { + loaderContext.insertCookies(getDomain(), "ninemanga_template_desk=yes") + } + + override val sortOrders: Set = EnumSet.of( + SortOrder.POPULARITY, + ) + + override suspend fun getList( + offset: Int, + query: String?, + sortOrder: SortOrder?, + tag: MangaTag?, + ): List { + val page = (offset / PAGE_SIZE.toFloat()).toIntUp() + 1 + val url = buildString { + append("https://") + append(getDomain()) + if (query.isNullOrEmpty()) { + append("/category/") + if (tag != null) { + append(tag.key) + } else { + append("index") + } + append("_") + append(page) + append(".html") + } else { + append("/search/?name_sel=&wd=") + append(query.urlEncoded()) + append("&page=") + append(page) + append(".html") + } + } + val doc = loaderContext.httpGet(url, PREDEFINED_HEADERS).parseHtml() + val root = doc.body().selectFirst("ul.direlist") + ?: throw ParseException("Cannot find root") + val baseHost = root.baseUri().toHttpUrl().host + return root.select("li").map { node -> + val href = node.selectFirst("a").absUrl("href") + val relUrl = href.toRelativeUrl(baseHost) + val dd = node.selectFirst("dd") + Manga( + id = generateUid(relUrl), + url = relUrl, + publicUrl = href, + title = dd.selectFirst("a.bookname").text().toCamelCase(), + altTitle = null, + coverUrl = node.selectFirst("img").absUrl("src"), + rating = Manga.NO_RATING, + author = null, + tags = emptySet(), + state = null, + source = source, + description = dd.selectFirst("p").html(), + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = loaderContext.httpGet( + manga.url.withDomain() + "?waring=1", + PREDEFINED_HEADERS + ).parseHtml() + val root = doc.body().selectFirst("div.manga") + ?: throw ParseException("Cannot find root") + val infoRoot = root.selectFirst("div.bookintro") + ?: throw ParseException("Cannot find info") + return manga.copy( + tags = infoRoot.getElementsByAttributeValue("itemprop", "genre")?.first() + ?.select("a")?.mapToSet { a -> + MangaTag( + title = a.text(), + key = a.attr("href").substringBetween("/", "."), + source = source, + ) + }.orEmpty(), + author = infoRoot.getElementsByAttributeValue("itemprop", "author")?.first()?.text(), + description = infoRoot.getElementsByAttributeValue("itemprop", "description")?.first() + ?.html()?.substringAfter(""), + chapters = root.selectFirst("div.chapterbox")?.selectFirst("ul") + ?.select("li")?.asReversed()?.mapIndexed { i, li -> + val a = li.selectFirst("a") + val href = a.relUrl("href") + MangaChapter( + id = generateUid(href), + name = a.text(), + number = i + 1, + url = href, + branch = null, + source = source, + ) + } + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = loaderContext.httpGet(chapter.url.withDomain(), PREDEFINED_HEADERS).parseHtml() + return doc.body().getElementById("page")?.select("option")?.map { option -> + val url = option.attr("value") + MangaPage( + id = generateUid(url), + url = url, + referer = chapter.url.withDomain(), + preview = null, + source = source, + ) + } ?: throw ParseException("Pages list not found at ${chapter.url}") + } + + override suspend fun getPageUrl(page: MangaPage): String { + val doc = loaderContext.httpGet(page.url.withDomain(), PREDEFINED_HEADERS).parseHtml() + val root = doc.body() + return root.selectFirst("a.pic_download")?.absUrl("href") + ?: throw ParseException("Page image not found") + } + + override suspend fun getTags(): Set { + val doc = loaderContext.httpGet("https://${getDomain()}/category/", PREDEFINED_HEADERS) + .parseHtml() + val root = doc.body().selectFirst("ul.genreidex") + return root.select("li").mapToSet { li -> + val a = li.selectFirst("a") + MangaTag( + title = a.text(), + key = a.attr("href").substringBetweenLast("/", "."), + source = source + ) + } + } + + class English(loaderContext: MangaLoaderContext) : NineMangaRepository( + loaderContext, + MangaSource.NINEMANGA_EN, + "www.ninemanga.com", + ) + + class Spanish(loaderContext: MangaLoaderContext) : NineMangaRepository( + loaderContext, + MangaSource.NINEMANGA_ES, + "es.ninemanga.com", + ) + + class Russian(loaderContext: MangaLoaderContext) : NineMangaRepository( + loaderContext, + MangaSource.NINEMANGA_RU, + "ru.ninemanga.com", + ) + + class Deutsch(loaderContext: MangaLoaderContext) : NineMangaRepository( + loaderContext, + MangaSource.NINEMANGA_DE, + "de.ninemanga.com", + ) + + class Brazil(loaderContext: MangaLoaderContext) : NineMangaRepository( + loaderContext, + MangaSource.NINEMANGA_BR, + "br.ninemanga.com", + ) + + class Italiano(loaderContext: MangaLoaderContext) : NineMangaRepository( + loaderContext, + MangaSource.NINEMANGA_IT, + "it.ninemanga.com", + ) + + class Francais(loaderContext: MangaLoaderContext) : NineMangaRepository( + loaderContext, + MangaSource.NINEMANGA_FR, + "fr.ninemanga.com", + ) + + private companion object { + + const val PAGE_SIZE = 26 + + val PREDEFINED_HEADERS = Headers.Builder() + .add("Accept-Language", "en-US;q=0.7,en;q=0.3") + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index cf380cffe..dfff7115f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -11,7 +11,7 @@ import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment -import org.koitharu.kotatsu.core.network.CookieJar +import org.koitharu.kotatsu.core.network.AndroidCookieJar import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.local.data.Cache import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider @@ -75,7 +75,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach } AppSettings.KEY_COOKIES_CLEAR -> { viewLifecycleScope.launch { - val cookieJar = get() + val cookieJar = get() cookieJar.clear() Snackbar.make( listView ?: return@launch, diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index 05ec233ae..4cfa1c808 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -29,6 +29,25 @@ fun String.removeSurrounding(vararg chars: Char): String { return this } +fun String.toCamelCase(): String { + if (isEmpty()) { + return this + } + val result = StringBuilder(length) + var capitalize = true + for (char in this) { + result.append( + if (capitalize) { + char.uppercase() + } else { + char.lowercase() + } + ) + capitalize = char.isWhitespace() + } + return result.toString() +} + fun String.transliterate(skipMissing: Boolean): String { val cyr = charArrayOf( 'a', 'б', 'в', 'г', 'д', 'ё', 'ж', 'з', 'и', 'к', 'л', 'м', 'н', @@ -92,7 +111,7 @@ fun String.md5(): String { .padStart(32, '0') } -fun String.substringBetween(from: String, to: String, fallbackValue: String): String { +fun String.substringBetween(from: String, to: String, fallbackValue: String = this): String { val fromIndex = indexOf(from) if (fromIndex == -1) { return fallbackValue @@ -105,6 +124,19 @@ fun String.substringBetween(from: String, to: String, fallbackValue: String): St } } +fun String.substringBetweenLast(from: String, to: String, fallbackValue: String = this): String { + val fromIndex = lastIndexOf(from) + if (fromIndex == -1) { + return fallbackValue + } + val toIndex = lastIndexOf(to) + return if (toIndex == -1) { + fallbackValue + } else { + substring(fromIndex + from.length, toIndex) + } +} + fun String.find(regex: Regex) = regex.find(this)?.value fun String.levenshteinDistance(other: String): Int { diff --git a/app/src/test/java/org/koitharu/kotatsu/parsers/TemporaryCookieJar.kt b/app/src/test/java/org/koitharu/kotatsu/parsers/TemporaryCookieJar.kt index c04ae731d..09bdf00ed 100644 --- a/app/src/test/java/org/koitharu/kotatsu/parsers/TemporaryCookieJar.kt +++ b/app/src/test/java/org/koitharu/kotatsu/parsers/TemporaryCookieJar.kt @@ -10,7 +10,7 @@ class TemporaryCookieJar : CookieJar { override fun loadForRequest(url: HttpUrl): List { val time = System.currentTimeMillis() - return cache.values.filter { it.matches(url) && it.expiresAt < time } + return cache.values.filter { it.matches(url) && it.expiresAt >= time } } override fun saveFromResponse(url: HttpUrl, cookies: List) { From bc0c5ac71a3f7f7db199fe75e7daedb94b24ae14 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 23 May 2021 17:17:08 +0300 Subject: [PATCH 014/138] Fix Anibel titles --- .../core/parser/site/AnibelRepository.kt | 18 ++++++++++++------ .../koitharu/kotatsu/utils/ext/StringExt.kt | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt index 318fcad02..35c5e6187 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt @@ -37,11 +37,14 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor return items.mapNotNull { card -> val href = card.selectFirst("a").attr("href") val status = card.select("tr")[2].text() + val fullTitle = card.selectFirst("h1.anime-card-title").text() + .substringBeforeLast('[') + val titleParts = fullTitle.splitTwoParts('/') Manga( id = generateUid(href), - title = card.selectFirst("h1.anime-card-title").text(), + title = titleParts?.first?.trim() ?: fullTitle, coverUrl = card.selectFirst("img").attr("data-src").withDomain(), - altTitle = null, + altTitle = titleParts?.second?.trim(), author = null, rating = Manga.NO_RATING, url = href, @@ -49,7 +52,7 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor tags = card.select("p.tupe.tag")?.select("a")?.mapNotNullToSet tags@{ x -> MangaTag( title = x.text(), - key = x.attr("href") ?: return@tags null, + key = x.attr("href")?.substringAfterLast("=") ?: return@tags null, source = source ) }.orEmpty(), @@ -132,11 +135,14 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor return items.mapNotNull { card -> val href = card.select("a").attr("href") val status = card.select("tr")[2].text() + val fullTitle = card.selectFirst("h1.anime-card-title").text() + .substringBeforeLast('[') + val titleParts = fullTitle.splitTwoParts('/') Manga( id = generateUid(href), - title = card.selectFirst("h1.anime-card-title").text(), + title = titleParts?.first?.trim() ?: fullTitle, coverUrl = card.selectFirst("img").attr("src").withDomain(), - altTitle = null, + altTitle = titleParts?.second?.trim(), author = null, rating = Manga.NO_RATING, url = href, @@ -144,7 +150,7 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor tags = card.select("p.tupe.tag")?.select("a")?.mapNotNullToSet tags@{ x -> MangaTag( title = x.text(), - key = x.attr("href") ?: return@tags null, + key = x.attr("href")?.substringAfterLast("=") ?: return@tags null, source = source ) }.orEmpty(), diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index 4cfa1c808..834a4158f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -77,6 +77,20 @@ fun String.ellipsize(maxLength: Int) = if (this.length > maxLength) { this.take(maxLength - 1) + Typography.ellipsis } else this +fun String.splitTwoParts(delimiter: Char): Pair? { + val indices = ArrayList(4) + for ((i, c) in this.withIndex()) { + if (c == delimiter) { + indices += i + } + } + if (indices.isEmpty() || indices.size and 1 == 0) { + return null + } + val index = indices[indices.size / 2] + return substring(0, index) to substring(index + 1) +} + fun String.urlEncoded(): String = URLEncoder.encode(this, Charsets.UTF_8.name()) fun String.toUriOrNull(): Uri? = if (isEmpty()) { From cd7d6d7674cb2ed1a8408791b4ca49d550791693 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 31 May 2021 20:50:15 +0300 Subject: [PATCH 015/138] New search suggestion UI --- .idea/misc.xml | 10 ++ .../koitharu/kotatsu/core/db/dao/MangaDao.kt | 8 + .../koitharu/kotatsu/main/ui/MainActivity.kt | 145 +++++++++++++----- .../koitharu/kotatsu/search/SearchModule.kt | 8 +- .../search/domain/MangaSearchRepository.kt | 88 ++++++++++- .../search/ui/MangaSuggestionsProvider.kt | 98 +----------- .../kotatsu/search/ui/SearchActivity.kt | 36 ++--- .../kotatsu/search/ui/SearchHelper.kt | 53 ------- .../ui/suggestion/SearchSuggestionFragment.kt | 57 +++++++ .../SearchSuggestionItemCallback.kt | 42 +++++ .../ui/suggestion/SearchSuggestionListener.kt | 14 ++ .../suggestion/SearchSuggestionViewModel.kt | 95 ++++++++++++ .../kotatsu/search/ui/suggestion/SearchUI.kt | 51 ++++++ .../adapter/SearchSuggestionAdapter.kt | 51 ++++++ .../adapter/SearchSuggestionHeaderAD.kt | 29 ++++ .../adapter/SearchSuggestionMangaAD.kt | 46 ++++++ .../adapter/SearchSuggestionQueryAD.kt | 26 ++++ .../suggestion/model/SearchSuggestionItem.kt | 21 +++ .../settings/HistorySettingsFragment.kt | 7 +- .../org/koitharu/kotatsu/utils/ext/ViewExt.kt | 5 + app/src/main/res/drawable/ic_clear_all.xml | 11 ++ app/src/main/res/drawable/ic_complete.xml | 11 ++ .../res/layout/fragment_search_suggestion.xml | 13 ++ .../layout/item_search_suggestion_header.xml | 28 ++++ .../layout/item_search_suggestion_manga.xml | 46 ++++++ .../layout/item_search_suggestion_query.xml | 35 +++++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 + 28 files changed, 831 insertions(+), 206 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/SearchHelper.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionHeaderAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionMangaAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt create mode 100644 app/src/main/res/drawable/ic_clear_all.xml create mode 100644 app/src/main/res/drawable/ic_complete.xml create mode 100644 app/src/main/res/layout/fragment_search_suggestion.xml create mode 100644 app/src/main/res/layout/item_search_suggestion_header.xml create mode 100644 app/src/main/res/layout/item_search_suggestion_manga.xml create mode 100644 app/src/main/res/layout/item_search_suggestion_query.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 7f598c7c9..4fd703491 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -9,6 +9,11 @@ + + + + + @@ -23,6 +28,7 @@ + @@ -31,7 +37,11 @@ + + + + diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt index 51b7571db..ee8255dfc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt @@ -13,6 +13,14 @@ abstract class MangaDao { @Query("SELECT * FROM manga WHERE manga_id = :id") abstract suspend fun find(id: Long): MangaWithTags? + @Transaction + @Query("SELECT * FROM manga WHERE title LIKE :query OR alt_title LIKE :query LIMIT :limit") + abstract suspend fun searchByTitle(query: String, limit: Int): List + + @Transaction + @Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source LIMIT :limit") + abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List + @Insert(onConflict = OnConflictStrategy.IGNORE) abstract suspend fun insert(manga: MangaEntity): Long diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 443ab118f..cd95e7e8a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -11,9 +11,12 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.ActionBarDrawerToggle +import androidx.appcompat.app.AlertDialog import androidx.core.graphics.Insets import androidx.core.view.* import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentTransaction +import androidx.fragment.app.commit import androidx.swiperefreshlayout.widget.CircularProgressDrawable import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar @@ -24,34 +27,41 @@ import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.prefs.AppSection import org.koitharu.kotatsu.databinding.ActivityMainBinding +import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.favourites.ui.FavouritesContainerFragment import org.koitharu.kotatsu.history.ui.HistoryListFragment import org.koitharu.kotatsu.local.ui.LocalListFragment import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment -import org.koitharu.kotatsu.search.ui.SearchHelper +import org.koitharu.kotatsu.search.ui.SearchActivity +import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel +import org.koitharu.kotatsu.search.ui.suggestion.SearchUI import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.resolveDp -import java.io.Closeable class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener, - View.OnClickListener { + View.OnClickListener, SearchSuggestionListener, MenuItem.OnActionExpandListener { private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) + private val searchSuggestionViewModel by viewModel( + mode = LazyThreadSafetyMode.NONE + ) private lateinit var drawerToggle: ActionBarDrawerToggle - private var closeable: Closeable? = null + private var searchUi: SearchUI? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater)) - drawerToggle = - ActionBarDrawerToggle( + drawerToggle = ActionBarDrawerToggle( this, binding.drawer, binding.toolbar, @@ -68,7 +78,7 @@ class MainActivity : BaseActivity(), setOnClickListener(this@MainActivity) } - supportFragmentManager.findFragmentById(R.id.container)?.let { + supportFragmentManager.findFragmentByTag(TAG_PRIMARY)?.let { binding.fab.isVisible = it is HistoryListFragment } ?: run { openDefaultSection() @@ -84,11 +94,6 @@ class MainActivity : BaseActivity(), viewModel.remoteSources.observe(this, this::updateSideMenu) } - override fun onDestroy() { - closeable?.close() - super.onDestroy() - } - override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) drawerToggle.syncState() @@ -109,8 +114,10 @@ class MainActivity : BaseActivity(), override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.opt_main, menu) - menu.findItem(R.id.action_search)?.let { menuItem -> - closeable = SearchHelper.setupSearchView(menuItem) + searchUi = menu.findItem(R.id.action_search)?.let { menuItem -> + onMenuItemActionCollapse(menuItem) + menuItem.setOnActionExpandListener(this) + SearchUI.from(menuItem, this) } return super.onCreateOptionsMenu(menu) } @@ -131,28 +138,32 @@ class MainActivity : BaseActivity(), if (item.groupId == R.id.group_remote_sources) { val source = MangaSource.values().getOrNull(item.itemId) ?: return false setPrimaryFragment(RemoteListFragment.newInstance(source)) - } else when (item.itemId) { - R.id.nav_history -> { - viewModel.defaultSection = AppSection.HISTORY - setPrimaryFragment(HistoryListFragment.newInstance()) + searchSuggestionViewModel.onSourceChanged(source) + } else { + searchSuggestionViewModel.onSourceChanged(null) + when (item.itemId) { + R.id.nav_history -> { + viewModel.defaultSection = AppSection.HISTORY + setPrimaryFragment(HistoryListFragment.newInstance()) + } + R.id.nav_favourites -> { + viewModel.defaultSection = AppSection.FAVOURITES + setPrimaryFragment(FavouritesContainerFragment.newInstance()) + } + R.id.nav_local_storage -> { + viewModel.defaultSection = AppSection.LOCAL + setPrimaryFragment(LocalListFragment.newInstance()) + } + R.id.nav_feed -> { + viewModel.defaultSection = AppSection.FEED + setPrimaryFragment(FeedFragment.newInstance()) + } + R.id.nav_action_settings -> { + startActivity(SettingsActivity.newIntent(this)) + return true + } + else -> return false } - R.id.nav_favourites -> { - viewModel.defaultSection = AppSection.FAVOURITES - setPrimaryFragment(FavouritesContainerFragment.newInstance()) - } - R.id.nav_local_storage -> { - viewModel.defaultSection = AppSection.LOCAL - setPrimaryFragment(LocalListFragment.newInstance()) - } - R.id.nav_feed -> { - viewModel.defaultSection = AppSection.FEED - setPrimaryFragment(FeedFragment.newInstance()) - } - R.id.nav_action_settings -> { - startActivity(SettingsActivity.newIntent(this)) - return true - } - else -> return false } binding.drawer.closeDrawers() return true @@ -171,6 +182,62 @@ class MainActivity : BaseActivity(), } } + override fun onMangaClick(manga: Manga) { + startActivity(DetailsActivity.newIntent(this, manga)) + } + + override fun onQueryClick(query: String, submit: Boolean) { + if (submit) { + if (query.isNotEmpty()) { + val source = searchSuggestionViewModel.getLocalSearchSource() + if (source != null) { + startActivity(SearchActivity.newIntent(this, source, query)) + } else { + startActivity(GlobalSearchActivity.newIntent(this, query)) + } + searchSuggestionViewModel.saveQuery(query) + } + } else { + searchUi?.query = query + } + } + + override fun onQueryChanged(query: String) { + searchSuggestionViewModel.onQueryChanged(query) + } + + override fun onClearSearchHistory() { + AlertDialog.Builder(this) + .setTitle(R.string.clear_search_history) + .setMessage(R.string.text_clear_search_history_prompt) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.clear) { _, _ -> + searchSuggestionViewModel.clearSearchHistory() + }.show() + } + + override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH) + if (fragment == null) { + supportFragmentManager.commit { + add(R.id.container, SearchSuggestionFragment.newInstance(), TAG_SEARCH) + setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + } + } + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH) + if (fragment != null) { + supportFragmentManager.commit { + remove(fragment) + setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + } + } + return true + } + private fun onOpenReader(manga: Manga) { val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { ActivityOptions.makeClipRevealAnimation( @@ -234,8 +301,14 @@ class MainActivity : BaseActivity(), private fun setPrimaryFragment(fragment: Fragment) { supportFragmentManager.beginTransaction() - .replace(R.id.container, fragment) + .replace(R.id.container, fragment, TAG_PRIMARY) .commit() binding.fab.isVisible = fragment is HistoryListFragment } + + private companion object { + + const val TAG_PRIMARY = "primary" + const val TAG_SEARCH = "search" + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt index a6a7db5c5..da6239420 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt @@ -1,17 +1,22 @@ package org.koitharu.kotatsu.search +import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.search.domain.MangaSearchRepository +import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.search.ui.SearchViewModel import org.koitharu.kotatsu.search.ui.global.GlobalSearchViewModel +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel val searchModule get() = module { - single { MangaSearchRepository(get()) } + single { MangaSearchRepository(get(), get(), androidContext(), get()) } + + factory { MangaSuggestionsProvider.createSuggestions(androidContext()) } viewModel { (source: MangaSource, query: String) -> SearchViewModel(get(named(source)), query, get()) @@ -19,4 +24,5 @@ val searchModule viewModel { (query: String) -> GlobalSearchViewModel(query, get(), get()) } + viewModel { SearchSuggestionViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 1efec6723..63e774884 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -1,14 +1,29 @@ package org.koitharu.kotatsu.search.domain import android.annotation.SuppressLint +import android.app.SearchManager +import android.content.Context +import android.provider.SearchRecentSuggestions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.* +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext import org.koitharu.kotatsu.base.domain.MangaProviderFactory +import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.utils.ext.levenshteinDistance -class MangaSearchRepository(private val settings: AppSettings) { +class MangaSearchRepository( + private val settings: AppSettings, + private val db: MangaDatabase, + private val context: Context, + private val recentSuggestions: SearchRecentSuggestions, +) { fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow = MangaProviderFactory.getSources(settings, includeHidden = false).asFlow() @@ -22,16 +37,81 @@ class MangaSearchRepository(private val settings: AppSettings) { match(it, query) } + suspend fun getMangaSuggestion(query: String, limit: Int, source: MangaSource?): List { + if (query.isEmpty()) { + return emptyList() + } + return if (source != null) { + db.mangaDao.searchByTitle("%$query%", source.name, limit) + } else { + db.mangaDao.searchByTitle("%$query%", limit) + }.map { it.toManga() } + .sortedBy { x -> x.title.levenshteinDistance(query) } + } + + suspend fun getQuerySuggestion( + query: String, + limit: Int, + ): List = withContext(Dispatchers.IO) { + context.contentResolver.query( + MangaSuggestionsProvider.QUERY_URI, + SUGGESTION_PROJECTION, + "${SearchManager.SUGGEST_COLUMN_QUERY} LIKE ?", + arrayOf("%$query%"), + "date DESC" + )?.use { cursor -> + val count = minOf(cursor.count, limit) + if (count == 0) { + return@withContext emptyList() + } + val result = ArrayList(count) + if (cursor.moveToFirst()) { + val index = cursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_QUERY) + do { + result += cursor.getString(index) + } while (currentCoroutineContext().isActive && cursor.moveToNext()) + } + result + }.orEmpty() + } + + fun saveSearchQuery(query: String) { + recentSuggestions.saveRecentQuery(query, null) + } + + suspend fun clearSearchHistory(): Unit = withContext(Dispatchers.IO) { + recentSuggestions.clearHistory() + } + + suspend fun deleteSearchQuery(query: String) = withContext(Dispatchers.IO) { + context.contentResolver.delete( + MangaSuggestionsProvider.URI, + "display1 = ?", + arrayOf(query), + ) + } + + suspend fun getSearchHistoryCount(): Int = withContext(Dispatchers.IO) { + context.contentResolver.query( + MangaSuggestionsProvider.QUERY_URI, + SUGGESTION_PROJECTION, + null, + null, + null + )?.use { cursor -> cursor.count } ?: 0 + } + private companion object { private val REGEX_SPACE = Regex("\\s+") + val SUGGESTION_PROJECTION = arrayOf(SearchManager.SUGGEST_COLUMN_QUERY) @SuppressLint("DefaultLocale") fun match(manga: Manga, query: String): Boolean { val words = HashSet() - words += manga.title.toLowerCase().split(REGEX_SPACE) - words += manga.altTitle?.toLowerCase()?.split(REGEX_SPACE).orEmpty() - val words2 = query.toLowerCase().split(REGEX_SPACE).toSet() + words += manga.title.lowercase().split(REGEX_SPACE) + words += manga.altTitle?.lowercase()?.split(REGEX_SPACE).orEmpty() + val words2 = query.lowercase().split(REGEX_SPACE).toSet() for (w in words) { for (w2 in words2) { val diff = w.levenshteinDistance(w2) / ((w.length + w2.length) / 2f) diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt index 2889a24cf..c708db286 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt @@ -4,49 +4,14 @@ import android.app.SearchManager import android.content.ContentResolver import android.content.Context import android.content.SearchRecentSuggestionsProvider -import android.database.Cursor import android.net.Uri import android.provider.SearchRecentSuggestions -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.cursoradapter.widget.CursorAdapter -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.koitharu.kotatsu.BuildConfig -import org.koitharu.kotatsu.R class MangaSuggestionsProvider : SearchRecentSuggestionsProvider() { init { - setupSuggestions( - AUTHORITY, - MODE - ) - } - - private class SearchSuggestionAdapter(context: Context, cursor: Cursor) : CursorAdapter( - context, cursor, - FLAG_REGISTER_CONTENT_OBSERVER - ) { - - override fun newView(context: Context, cursor: Cursor?, parent: ViewGroup?): View { - return LayoutInflater.from(context) - .inflate(R.layout.item_search_complete, parent, false) - } - - override fun bindView(view: View, context: Context, cursor: Cursor) { - if (view !is TextView) return - view.text = cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY)) - } - - override fun convertToString(cursor: Cursor?): CharSequence { - return cursor?.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY)) - .orEmpty() - } + setupSuggestions(AUTHORITY, MODE) } companion object { @@ -54,65 +19,16 @@ class MangaSuggestionsProvider : SearchRecentSuggestionsProvider() { private const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.MangaSuggestionsProvider" private const val MODE = DATABASE_MODE_QUERIES - private val uri = Uri.Builder() + fun createSuggestions(context: Context): SearchRecentSuggestions { + return SearchRecentSuggestions(context, AUTHORITY, MODE) + } + + val QUERY_URI: Uri = Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(AUTHORITY) .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY) .build() - private val projection = arrayOf("_id", SearchManager.SUGGEST_COLUMN_QUERY) - - fun saveQueryAsync(context: Context, query: String) { - GlobalScope.launch(Dispatchers.IO) { - saveQuery(context, query) - } - } - - fun saveQuery(context: Context, query: String) { - runCatching { - SearchRecentSuggestions( - context, - AUTHORITY, - MODE - ).saveRecentQuery(query, null) - }.onFailure { - if (BuildConfig.DEBUG) { - it.printStackTrace() - } - } - } - - suspend fun clearHistory(context: Context) = withContext(Dispatchers.IO) { - SearchRecentSuggestions( - context, - AUTHORITY, - MODE - ).clearHistory() - } - - suspend fun getItemsCount(context: Context) = withContext(Dispatchers.IO) { - getCursor(context)?.use { it.count } ?: 0 - } - - private fun getCursor(context: Context): Cursor? { - return context.contentResolver?.query(uri, projection, null, arrayOf(""), null) - } - - @Deprecated("Need async implementation") - fun getSuggestionAdapter(context: Context): CursorAdapter? = getCursor( - context - )?.let { cursor -> - SearchSuggestionAdapter(context, cursor).also { - it.setFilterQueryProvider { q -> - context.contentResolver?.query( - uri, - projection, - " ?", - arrayOf(q?.toString().orEmpty()), - null - ) - } - } - } + val URI: Uri = Uri.parse("content://$AUTHORITY/suggestions") } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt index 05092438f..7221f6d11 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt @@ -7,14 +7,20 @@ import android.os.Parcelable import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets import androidx.core.view.updatePadding +import androidx.fragment.app.commit +import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.databinding.ActivitySearchBinding +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel import org.koitharu.kotatsu.utils.ext.showKeyboard class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener { + private val searchSuggestionViewModel by viewModel( + mode = LazyThreadSafetyMode.NONE + ) private lateinit var source: MangaSource override fun onCreate(savedInstanceState: Bundle?) { @@ -28,8 +34,6 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery supportActionBar?.setDisplayHomeAsUpEnabled(true) with(binding.searchView) { queryHint = getString(R.string.search_on_s, source.title) - suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(this@SearchActivity) - setOnSuggestionListener(SearchHelper.SuggestionListener(this)) setOnQueryTextListener(this@SearchActivity) if (query.isNullOrBlank()) { @@ -41,11 +45,6 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery } } - override fun onDestroy() { - binding.searchView.suggestionsAdapter.changeCursor(null) //close cursor - super.onDestroy() - } - override fun onWindowInsetsChanged(insets: Insets) { binding.toolbar.updatePadding( top = insets.top, @@ -55,19 +54,20 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery } override fun onQueryTextSubmit(query: String?): Boolean { - return if (!query.isNullOrBlank()) { - title = query - supportFragmentManager - .beginTransaction() - .replace(R.id.container, SearchFragment.newInstance(source, query)) - .commit() - binding.searchView.clearFocus() - MangaSuggestionsProvider.saveQueryAsync(applicationContext, query) - true - } else false + val q = query?.trim() + if (q.isNullOrEmpty()) { + return false + } + title = query + supportFragmentManager.commit { + replace(R.id.container, SearchFragment.newInstance(source, q)) + } + binding.searchView.clearFocus() + searchSuggestionViewModel.saveQuery(q) + return true } - override fun onQueryTextChange(newText: String?) = false + override fun onQueryTextChange(newText: String?): Boolean = false companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchHelper.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchHelper.kt deleted file mode 100644 index 6f937b2eb..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchHelper.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.koitharu.kotatsu.search.ui - -import android.app.SearchManager -import android.content.Context -import android.database.Cursor -import android.view.MenuItem -import androidx.appcompat.widget.SearchView -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity -import java.io.Closeable - -object SearchHelper { - - fun setupSearchView(menuItem: MenuItem): Closeable? { - val view = menuItem.actionView as? SearchView ?: return null - val context = view.context - val adapter = MangaSuggestionsProvider.getSuggestionAdapter(context) - view.queryHint = context.getString(R.string.search_manga) - view.suggestionsAdapter = adapter - view.setOnQueryTextListener(QueryListener(context)) - view.setOnSuggestionListener(SuggestionListener(view)) - return adapter?.cursor - } - - private class QueryListener(private val context: Context) : - SearchView.OnQueryTextListener { - - override fun onQueryTextSubmit(query: String?): Boolean { - return if (!query.isNullOrBlank()) { - context.startActivity(GlobalSearchActivity.newIntent(context, query.trim())) - MangaSuggestionsProvider.saveQueryAsync(context.applicationContext, query) - true - } else false - } - - override fun onQueryTextChange(newText: String?) = false - } - - class SuggestionListener(private val view: SearchView) : - SearchView.OnSuggestionListener { - - override fun onSuggestionSelect(position: Int) = false - - override fun onSuggestionClick(position: Int): Boolean { - val query = runCatching { - val c = view.suggestionsAdapter.getItem(position) as? Cursor - c?.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY)) - }.getOrNull() ?: return false - view.setQuery(query, true) - return true - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt new file mode 100644 index 000000000..e053df0fc --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt @@ -0,0 +1,57 @@ +package org.koitharu.kotatsu.search.ui.suggestion + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.graphics.Insets +import androidx.core.view.updatePadding +import androidx.recyclerview.widget.ItemTouchHelper +import org.koin.android.ext.android.get +import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import org.koitharu.kotatsu.base.ui.BaseFragment +import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding +import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter + +class SearchSuggestionFragment : BaseFragment(), + SearchSuggestionItemCallback.SuggestionItemListener { + + private val viewModel by sharedViewModel() + + override fun onInflateView( + inflater: LayoutInflater, + container: ViewGroup?, + ) = FragmentSearchSuggestionBinding.inflate(inflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val adapter = SearchSuggestionAdapter( + coil = get(), + lifecycleOwner = viewLifecycleOwner, + listener = requireActivity() as SearchSuggestionListener, + ) + binding.root.adapter = adapter + viewModel.suggestion.observe(viewLifecycleOwner) { + adapter.items = it + } + ItemTouchHelper(SearchSuggestionItemCallback(this)) + .attachToRecyclerView(binding.root) + } + + override fun onWindowInsetsChanged(insets: Insets) { + binding.root.updatePadding( + left = insets.left, + right = insets.right, + bottom = insets.bottom, + ) + } + + override fun onRemoveQuery(query: String) { + viewModel.deleteQuery(query) + } + + companion object { + + fun newInstance() = SearchSuggestionFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt new file mode 100644 index 000000000..e983be92b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.search.ui.suggestion + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter +import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem +import org.koitharu.kotatsu.utils.ext.getItem + +class SearchSuggestionItemCallback( + private val listener: SuggestionItemListener, +) : ItemTouchHelper.Callback() { + + private val movementFlags = makeMovementFlags( + 0, + ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + ) + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ): Int = if (viewHolder.itemViewType == SearchSuggestionAdapter.ITEM_TYPE_QUERY) { + movementFlags + } else { + 0 + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean = false + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val item = viewHolder.getItem() ?: return + listener.onRemoveQuery(item.query) + } + + interface SuggestionItemListener { + + fun onRemoveQuery(query: String) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt new file mode 100644 index 000000000..d82a47bb0 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.search.ui.suggestion + +import org.koitharu.kotatsu.core.model.Manga + +interface SearchSuggestionListener { + + fun onMangaClick(manga: Manga) + + fun onQueryClick(query: String, submit: Boolean) + + fun onQueryChanged(query: String) + + fun onClearSearchHistory() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt new file mode 100644 index 000000000..d05382e72 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt @@ -0,0 +1,95 @@ +package org.koitharu.kotatsu.search.ui.suggestion + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.plus +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.search.domain.MangaSearchRepository +import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem + +class SearchSuggestionViewModel( + private val repository: MangaSearchRepository, +) : BaseViewModel() { + + private val query = MutableStateFlow("") + private val source = MutableStateFlow(null) + private val isLocalSearch = MutableStateFlow(false) + private var suggestionJob: Job? = null + + val suggestion = MutableLiveData>() + + init { + setupSuggestion() + } + + fun onQueryChanged(newQuery: String) { + query.value = newQuery + } + + fun onSourceChanged(newSource: MangaSource?) { + source.value = newSource + } + + fun saveQuery(query: String) { + repository.saveSearchQuery(query) + } + + fun getLocalSearchSource(): MangaSource? { + return source.value?.takeIf { isLocalSearch.value } + } + + fun clearSearchHistory() { + launchJob { + repository.clearSearchHistory() + setupSuggestion() + } + } + + fun deleteQuery(query: String) { + launchJob { + repository.deleteSearchQuery(query) + setupSuggestion() + } + } + + private fun setupSuggestion() { + suggestionJob?.cancel() + suggestionJob = combine( + query + .debounce(DEBOUNCE_TIMEOUT) + .mapLatest { q -> + q to repository.getQuerySuggestion(q, MAX_QUERY_ITEMS) + }, + source, + isLocalSearch + ) { (q, queries), src, srcOnly -> + val result = ArrayList(MAX_SUGGESTION_ITEMS) + if (src != null) { + result += SearchSuggestionItem.Header(src, isLocalSearch) + } + if (q.length >= SEARCH_THRESHOLD) { + repository.getMangaSuggestion(q, MAX_MANGA_ITEMS, src.takeIf { srcOnly }) + .mapTo(result) { + SearchSuggestionItem.MangaItem(it) + } + } + queries.mapTo(result) { SearchSuggestionItem.RecentQuery(it) } + result + }.onEach { + suggestion.postValue(it) + }.launchIn(viewModelScope + Dispatchers.Default) + } + + private companion object { + + const val DEBOUNCE_TIMEOUT = 500L + const val SEARCH_THRESHOLD = 3 + const val MAX_MANGA_ITEMS = 3 + const val MAX_QUERY_ITEMS = 120 + const val MAX_SUGGESTION_ITEMS = MAX_MANGA_ITEMS + MAX_QUERY_ITEMS + 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt new file mode 100644 index 000000000..fb9f79847 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt @@ -0,0 +1,51 @@ +package org.koitharu.kotatsu.search.ui.suggestion + +import android.view.MenuItem +import androidx.appcompat.widget.SearchView +import org.koitharu.kotatsu.R + +class SearchUI( + private val searchView: SearchView, + listener: SearchSuggestionListener, + hint: String? = null, +) { + + init { + val context = searchView.context + searchView.queryHint = hint ?: context.getString(R.string.search_manga) + searchView.setOnQueryTextListener(QueryListener(listener)) + } + + var query: String + get() = searchView.query.toString() + set(value) { + searchView.setQuery(value, false) + } + + private class QueryListener( + private val listener: SearchSuggestionListener, + ) : SearchView.OnQueryTextListener { + + override fun onQueryTextSubmit(query: String?): Boolean { + return if (!query.isNullOrBlank()) { + listener.onQueryClick(query.trim(), submit = true) + true + } else false + } + + override fun onQueryTextChange(newText: String?): Boolean { + listener.onQueryChanged(newText?.trim().orEmpty()) + return true + } + } + + companion object { + + fun from( + menuItem: MenuItem, + listener: SearchSuggestionListener, + ): SearchUI? = (menuItem.actionView as? SearchView)?.let { + SearchUI(it, listener) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt new file mode 100644 index 000000000..06e6af858 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt @@ -0,0 +1,51 @@ +package org.koitharu.kotatsu.search.ui.suggestion.adapter + +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener +import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem +import kotlin.jvm.internal.Intrinsics + +class SearchSuggestionAdapter( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + listener: SearchSuggestionListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + delegatesManager.addDelegate(ITEM_TYPE_MANGA, searchSuggestionMangaAD(coil, lifecycleOwner, listener)) + .addDelegate(ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener)) + .addDelegate(ITEM_TYPE_HEADER, searchSuggestionHeaderAD(listener)) + } + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame( + oldItem: SearchSuggestionItem, + newItem: SearchSuggestionItem, + ): Boolean = when { + oldItem is SearchSuggestionItem.MangaItem && newItem is SearchSuggestionItem.MangaItem -> { + oldItem.manga.id == newItem.manga.id + } + oldItem is SearchSuggestionItem.RecentQuery && newItem is SearchSuggestionItem.RecentQuery -> { + oldItem.query == newItem.query + } + oldItem is SearchSuggestionItem.Header && newItem is SearchSuggestionItem.Header -> true + else -> false + } + + override fun areContentsTheSame( + oldItem: SearchSuggestionItem, + newItem: SearchSuggestionItem, + ): Boolean = Intrinsics.areEqual(oldItem, newItem) + } + + companion object { + + const val ITEM_TYPE_MANGA = 0 + const val ITEM_TYPE_QUERY = 1 + const val ITEM_TYPE_HEADER = 2 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionHeaderAD.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionHeaderAD.kt new file mode 100644 index 000000000..be60708cd --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionHeaderAD.kt @@ -0,0 +1,29 @@ +package org.koitharu.kotatsu.search.ui.suggestion.adapter + +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.databinding.ItemSearchSuggestionHeaderBinding +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener +import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem + +fun searchSuggestionHeaderAD( + listener: SearchSuggestionListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemSearchSuggestionHeaderBinding.inflate(inflater, parent, false) } + ) { + + binding.switchLocal.setOnCheckedChangeListener { _, isChecked -> + item.isChecked.value = isChecked + } + binding.buttonClear.setOnClickListener { + listener.onClearSearchHistory() + } + + bind { + binding.switchLocal.text = getString( + R.string.search_only_on_s, + item.source.title, + ) + binding.switchLocal.isChecked = item.isChecked.value + } + } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionMangaAD.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionMangaAD.kt new file mode 100644 index 000000000..2eed6b932 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionMangaAD.kt @@ -0,0 +1,46 @@ +package org.koitharu.kotatsu.search.ui.suggestion.adapter + +import androidx.lifecycle.LifecycleOwner +import coil.ImageLoader +import coil.request.Disposable +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.databinding.ItemSearchSuggestionMangaBinding +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener +import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem +import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.newImageRequest +import org.koitharu.kotatsu.utils.ext.textAndVisible + +fun searchSuggestionMangaAD( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + listener: SearchSuggestionListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemSearchSuggestionMangaBinding.inflate(inflater, parent, false) } +) { + + var imageRequest: Disposable? = null + + itemView.setOnClickListener { + listener.onMangaClick(item.manga) + } + + bind { + imageRequest?.dispose() + imageRequest = binding.imageViewCover.newImageRequest(item.manga.coverUrl) + .placeholder(R.drawable.ic_placeholder) + .fallback(R.drawable.ic_placeholder) + .error(R.drawable.ic_placeholder) + .allowRgb565(true) + .lifecycle(lifecycleOwner) + .enqueueWith(coil) + binding.textViewTitle.text = item.manga.title + binding.textViewSubtitle.textAndVisible = item.manga.altTitle + } + + onViewRecycled { + imageRequest?.dispose() + binding.imageViewCover.setImageDrawable(null) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt new file mode 100644 index 000000000..70854cb2f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt @@ -0,0 +1,26 @@ +package org.koitharu.kotatsu.search.ui.suggestion.adapter + +import android.view.View +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.databinding.ItemSearchSuggestionQueryBinding +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener +import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem + +fun searchSuggestionQueryAD( + listener: SearchSuggestionListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemSearchSuggestionQueryBinding.inflate(inflater, parent, false) } +) { + + val viewClickListener = View.OnClickListener { v -> + listener.onQueryClick(item.query, v.id != R.id.button_complete) + } + + binding.root.setOnClickListener(viewClickListener) + binding.buttonComplete.setOnClickListener(viewClickListener) + + bind { + binding.textViewTitle.text = item.query + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt new file mode 100644 index 000000000..d2d9d04d7 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt @@ -0,0 +1,21 @@ +package org.koitharu.kotatsu.search.ui.suggestion.model + +import kotlinx.coroutines.flow.MutableStateFlow +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaSource + +sealed class SearchSuggestionItem { + + data class MangaItem( + val manga: Manga, + ) : SearchSuggestionItem() + + data class RecentQuery( + val query: String, + ) : SearchSuggestionItem() + + data class Header( + val source: MangaSource, + val isChecked: MutableStateFlow, + ) : SearchSuggestionItem() +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index dfff7115f..a2710540e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -14,7 +14,7 @@ import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.network.AndroidCookieJar import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.local.data.Cache -import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider +import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.FileSizeUtils @@ -24,6 +24,7 @@ import org.koitharu.kotatsu.utils.ext.viewLifecycleScope class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) { private val trackerRepo by inject(mode = LazyThreadSafetyMode.NONE) + private val searchRepository by inject(mode = LazyThreadSafetyMode.NONE) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_history) @@ -49,7 +50,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach } findPreference(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref -> viewLifecycleScope.launchWhenResumed { - val items = MangaSuggestionsProvider.getItemsCount(pref.context) + val items = searchRepository.getSearchHistoryCount() pref.summary = pref.context.resources.getQuantityString(R.plurals.items, items, items) } @@ -87,7 +88,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach } AppSettings.KEY_SEARCH_HISTORY_CLEAR -> { viewLifecycleScope.launch { - MangaSuggestionsProvider.clearHistory(preference.context) + searchRepository.clearSearchHistory() preference.summary = preference.context.resources .getQuantityString(R.plurals.items, 0, 0) Snackbar.make( diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index 9a0fe450b..c73cff93f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -16,6 +16,7 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 +import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder fun View.hideKeyboard() { val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager @@ -153,4 +154,8 @@ fun RecyclerView.findCenterViewPosition(): Int { val centerY = height / 2f val view = findChildViewUnder(centerX, centerY) ?: return RecyclerView.NO_POSITION return getChildAdapterPosition(view) +} + +inline fun RecyclerView.ViewHolder.getItem(): T? { + return ((this as? AdapterDelegateViewBindingViewHolder<*, *>)?.item as? T) } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_clear_all.xml b/app/src/main/res/drawable/ic_clear_all.xml new file mode 100644 index 000000000..99c92f173 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_all.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_complete.xml b/app/src/main/res/drawable/ic_complete.xml new file mode 100644 index 000000000..3735578eb --- /dev/null +++ b/app/src/main/res/drawable/ic_complete.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_suggestion.xml b/app/src/main/res/layout/fragment_search_suggestion.xml new file mode 100644 index 000000000..a2ff52eda --- /dev/null +++ b/app/src/main/res/layout/fragment_search_suggestion.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_suggestion_header.xml b/app/src/main/res/layout/item_search_suggestion_header.xml new file mode 100644 index 000000000..62b29b12d --- /dev/null +++ b/app/src/main/res/layout/item_search_suggestion_header.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_suggestion_manga.xml b/app/src/main/res/layout/item_search_suggestion_manga.xml new file mode 100644 index 000000000..a92340c02 --- /dev/null +++ b/app/src/main/res/layout/item_search_suggestion_manga.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_suggestion_query.xml b/app/src/main/res/layout/item_search_suggestion_query.xml new file mode 100644 index 000000000..ace877682 --- /dev/null +++ b/app/src/main/res/layout/item_search_suggestion_query.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3765820e9..b20c30211 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -206,4 +206,5 @@ Confirm Пароль должен содержать не менее 4 символов Прятать заголовок при прокрутке + Поиск только по %s \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6f5e7da6f..64fb9d0bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -208,4 +208,6 @@ Confirm Password must be at least 4 characters Hide toolbar when scrolling + Search only on %s + Do you really want to remove all recent search queries? This action cannot be undone. \ No newline at end of file From 09412719b7725387491be1cbe0143cbf77171d54 Mon Sep 17 00:00:00 2001 From: ztimms73 Date: Thu, 17 Jun 2021 00:46:27 +0300 Subject: [PATCH 016/138] Redesign (#24) --- .../koitharu/kotatsu/base/ui/BaseActivity.kt | 8 +- .../ui/list/decor/SectionItemDecoration.kt | 2 +- .../kotatsu/base/ui/widgets/ChipsView.kt | 13 +- .../ui/widgets/search/MaterialSearchView.kt | 231 ++++++ .../ui/widgets/search/SearchArrowDrawable.kt | 62 ++ .../base/ui/widgets/search/SearchBehavior.kt | 54 ++ .../widgets/search/internal/SearchEditText.kt | 40 + .../widgets/search/internal/SearchLayout.kt | 725 ++++++++++++++++++ .../search/internal/SearchViewSavedState.kt | 21 + .../ui/widgets/search/util/SearchUtils.kt | 37 + .../kotatsu/details/ui/DetailsFragment.kt | 61 +- .../kotatsu/list/ui/MangaListFragment.kt | 17 +- .../list/ui/model/ListModelConversionExt.kt | 3 +- .../koitharu/kotatsu/main/ui/MainActivity.kt | 23 +- .../kotatsu/reader/ui/ReaderActivity.kt | 2 +- .../ListPaginationListener.kt | 2 +- .../{wetoon => webtoon}/WebtoonAdapter.kt | 2 +- .../{wetoon => webtoon}/WebtoonFrameLayout.kt | 2 +- .../{wetoon => webtoon}/WebtoonHolder.kt | 2 +- .../{wetoon => webtoon}/WebtoonImageView.kt | 2 +- .../WebtoonReaderFragment.kt | 2 +- .../WebtoonRecyclerView.kt | 2 +- .../kotatsu/tracker/ui/FeedFragment.kt | 5 +- .../kotatsu/tracker/ui/adapter/FeedItemAD.kt | 2 +- .../ui/model/ListModelConversionExt.kt | 14 +- .../org/koitharu/kotatsu/utils/ext/CoilExt.kt | 1 + .../org/koitharu/kotatsu/utils/ext/UiExt.kt | 21 + app/src/main/res/color/divider.xml | 4 + .../color/navigation_item_background_tint.xml | 5 + app/src/main/res/color/selector_overlay.xml | 4 + app/src/main/res/drawable-hdpi/totoro.webp | Bin 0 -> 1654 bytes app/src/main/res/drawable-mdpi/totoro.webp | Bin 0 -> 1250 bytes .../res/drawable-w600dp/tab_indicator.xml | 11 + app/src/main/res/drawable-xhdpi/totoro.webp | Bin 0 -> 2524 bytes app/src/main/res/drawable-xxhdpi/totoro.webp | Bin 0 -> 3120 bytes app/src/main/res/drawable-xxxhdpi/totoro.webp | Bin 0 -> 5222 bytes app/src/main/res/drawable/badge.xml | 6 + app/src/main/res/drawable/divider.xml | 5 + app/src/main/res/drawable/gradient.xml | 10 + app/src/main/res/drawable/ic_arrow_back.xml | 11 + app/src/main/res/drawable/ic_book_page.xml | 21 +- app/src/main/res/drawable/ic_clear.xml | 10 + app/src/main/res/drawable/ic_eye.xml | 22 +- app/src/main/res/drawable/ic_eye_off.xml | 23 +- app/src/main/res/drawable/ic_lock.xml | 2 +- app/src/main/res/drawable/ic_menu.xml | 10 + app/src/main/res/drawable/ic_mic_none.xml | 10 + app/src/main/res/drawable/ic_person.xml | 10 + app/src/main/res/drawable/ic_settings.xml | 19 +- app/src/main/res/drawable/ic_share.xml | 14 +- app/src/main/res/drawable/ic_star.xml | 10 + .../main/res/drawable/ic_star_manga_info.xml | 11 + app/src/main/res/drawable/ic_web.xml | 10 + app/src/main/res/drawable/list_selector.xml | 42 + .../drawable/navigation_item_background.xml | 19 + app/src/main/res/drawable/tab_indicator.xml | 2 +- app/src/main/res/drawable/tabs_background.xml | 2 +- app/src/main/res/drawable/thumb.xml | 2 +- .../layout-w600dp-land/fragment_details.xml | 295 +++++++ .../layout-w600dp-port/fragment_details.xml | 303 ++++++++ .../res/layout-w600dp/activity_details.xml | 16 +- .../res/layout-w600dp/activity_settings.xml | 6 +- .../res/layout-w600dp/fragment_details.xml | 145 ---- app/src/main/res/layout/activity_browser.xml | 8 +- .../main/res/layout/activity_categories.xml | 22 +- app/src/main/res/layout/activity_details.xml | 17 +- app/src/main/res/layout/activity_main.xml | 10 +- app/src/main/res/layout/activity_reader.xml | 10 +- app/src/main/res/layout/activity_search.xml | 8 +- .../res/layout/activity_search_global.xml | 8 +- app/src/main/res/layout/activity_settings.xml | 10 +- .../res/layout/activity_settings_simple.xml | 8 +- app/src/main/res/layout/dialog_input.xml | 9 +- app/src/main/res/layout/dialog_list_mode.xml | 6 +- .../main/res/layout/dialog_reader_config.xml | 6 +- app/src/main/res/layout/fragment_chapters.xml | 6 +- app/src/main/res/layout/fragment_details.xml | 367 ++++++--- .../res/layout/fragment_reader_webtoon.xml | 2 +- .../main/res/layout/item_filter_header.xml | 16 + app/src/main/res/layout/item_header.xml | 6 +- app/src/main/res/layout/item_manga_grid.xml | 54 +- app/src/main/res/layout/item_manga_list.xml | 37 +- .../res/layout/item_manga_list_details.xml | 89 +-- app/src/main/res/layout/item_page_webtoon.xml | 14 +- .../main/res/layout/item_search_complete.xml | 6 +- app/src/main/res/layout/item_tracklog.xml | 118 +-- .../main/res/layout/layout_search_view.xml | 113 +++ app/src/main/res/layout/navigation_header.xml | 25 + app/src/main/res/values-be/strings.xml | 5 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-night/colors.xml | 27 +- app/src/main/res/values-night/styles.xml | 8 - app/src/main/res/values-night/themes.xml | 11 +- .../main/res/values-notnight-v23/bools.xml | 4 + .../main/res/values-notnight-v23/colors.xml | 5 + .../main/res/values-notnight-v27/bools.xml | 4 + .../main/res/values-notnight-v27/colors.xml | 5 + app/src/main/res/values-ru/strings.xml | 6 +- app/src/main/res/values-v23/themes.xml | 8 + app/src/main/res/values-v29/themes.xml | 13 +- app/src/main/res/values-w600dp/dimens.xml | 2 +- app/src/main/res/values/attrs.xml | 53 ++ app/src/main/res/values/bools.xml | 2 + app/src/main/res/values/colors.xml | 37 + app/src/main/res/values/dimens.xml | 51 +- app/src/main/res/values/integers.xml | 6 + app/src/main/res/values/strings.xml | 3 +- app/src/main/res/values/styles.xml | 91 ++- app/src/main/res/values/themes.xml | 65 +- 109 files changed, 3114 insertions(+), 685 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/MaterialSearchView.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/SearchArrowDrawable.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/SearchBehavior.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchEditText.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchLayout.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchViewSavedState.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/util/SearchUtils.kt rename app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/{wetoon => webtoon}/ListPaginationListener.kt (95%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/{wetoon => webtoon}/WebtoonAdapter.kt (94%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/{wetoon => webtoon}/WebtoonFrameLayout.kt (91%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/{wetoon => webtoon}/WebtoonHolder.kt (97%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/{wetoon => webtoon}/WebtoonImageView.kt (97%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/{wetoon => webtoon}/WebtoonReaderFragment.kt (98%) rename app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/{wetoon => webtoon}/WebtoonRecyclerView.kt (97%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ext/UiExt.kt create mode 100644 app/src/main/res/color/divider.xml create mode 100644 app/src/main/res/color/navigation_item_background_tint.xml create mode 100644 app/src/main/res/color/selector_overlay.xml create mode 100644 app/src/main/res/drawable-hdpi/totoro.webp create mode 100644 app/src/main/res/drawable-mdpi/totoro.webp create mode 100644 app/src/main/res/drawable-w600dp/tab_indicator.xml create mode 100644 app/src/main/res/drawable-xhdpi/totoro.webp create mode 100644 app/src/main/res/drawable-xxhdpi/totoro.webp create mode 100644 app/src/main/res/drawable-xxxhdpi/totoro.webp create mode 100644 app/src/main/res/drawable/badge.xml create mode 100644 app/src/main/res/drawable/divider.xml create mode 100644 app/src/main/res/drawable/gradient.xml create mode 100644 app/src/main/res/drawable/ic_arrow_back.xml create mode 100644 app/src/main/res/drawable/ic_clear.xml create mode 100644 app/src/main/res/drawable/ic_menu.xml create mode 100644 app/src/main/res/drawable/ic_mic_none.xml create mode 100644 app/src/main/res/drawable/ic_person.xml create mode 100644 app/src/main/res/drawable/ic_star.xml create mode 100644 app/src/main/res/drawable/ic_star_manga_info.xml create mode 100644 app/src/main/res/drawable/ic_web.xml create mode 100644 app/src/main/res/drawable/list_selector.xml create mode 100644 app/src/main/res/drawable/navigation_item_background.xml create mode 100644 app/src/main/res/layout-w600dp-land/fragment_details.xml create mode 100644 app/src/main/res/layout-w600dp-port/fragment_details.xml delete mode 100644 app/src/main/res/layout-w600dp/fragment_details.xml create mode 100644 app/src/main/res/layout/item_filter_header.xml create mode 100644 app/src/main/res/layout/layout_search_view.xml create mode 100644 app/src/main/res/layout/navigation_header.xml delete mode 100644 app/src/main/res/values-night/styles.xml create mode 100644 app/src/main/res/values-notnight-v23/bools.xml create mode 100644 app/src/main/res/values-notnight-v23/colors.xml create mode 100644 app/src/main/res/values-notnight-v27/bools.xml create mode 100644 app/src/main/res/values-notnight-v27/colors.xml create mode 100644 app/src/main/res/values-v23/themes.xml create mode 100644 app/src/main/res/values/integers.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt index d032ec3e9..e0b2cdef8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt @@ -37,7 +37,7 @@ abstract class BaseActivity : AppCompatActivity(), OnApplyWindo override fun onCreate(savedInstanceState: Bundle?) { if (get().isAmoledTheme) { - setTheme(R.style.AppTheme_Amoled) + setTheme(R.style.AppTheme_AMOLED) } super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) @@ -59,12 +59,12 @@ abstract class BaseActivity : AppCompatActivity(), OnApplyWindo this.binding = binding super.setContentView(binding.root) (binding.root.findViewById(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar) - val params = (binding.root.findViewById(R.id.toolbar) as? Toolbar)?.layoutParams as AppBarLayout.LayoutParams + val params = (binding.root.findViewById(R.id.toolbar) as? Toolbar)?.layoutParams as? AppBarLayout.LayoutParams ViewCompat.setOnApplyWindowInsetsListener(binding.root, this) if (get().isToolbarHideWhenScrolling) { - params.scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS + params?.scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS } else { - params.scrollFlags = SCROLL_FLAG_NO_SCROLL + params?.scrollFlags = SCROLL_FLAG_NO_SCROLL } } diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/SectionItemDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/SectionItemDecoration.kt index b5ecb78a2..d8181e5c7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/SectionItemDecoration.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/SectionItemDecoration.kt @@ -37,7 +37,7 @@ class SectionItemDecoration( override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { super.onDrawOver(c, parent, state) - val textView = headerView ?: parent.inflate(R.layout.item_header).also { + val textView = headerView ?: parent.inflate(R.layout.item_filter_header).also { headerView = it } fixLayoutSize(textView, parent) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt index 52ca53616..b11f96c9e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt @@ -4,17 +4,18 @@ import android.content.Context import android.util.AttributeSet import android.view.View.OnClickListener import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat import androidx.core.view.children -import com.google.android.material.R import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.utils.ext.getThemeColor class ChipsView @JvmOverloads constructor( context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.chipGroupStyle -) : ChipGroup(context, attrs, defStyleAttr) { + attrs: AttributeSet? = null +) : ChipGroup(context, attrs) { private var isLayoutSuppressedCompat = false private var isLayoutCalledOnSuppressed = false @@ -64,7 +65,9 @@ class ChipsView @JvmOverloads constructor( private fun addChip(): Chip { val chip = Chip(context) - chip.setTextColor(context.getThemeColor(android.R.attr.textColorPrimary)) + val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip) + chip.setChipDrawable(drawable) + chip.setTextColor(ContextCompat.getColor(context, R.color.blue_primary)) chip.isCloseIconVisible = false chip.setEnsureMinTouchTargetSize(false) chip.setOnClickListener(chipOnClickListener) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/MaterialSearchView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/MaterialSearchView.kt new file mode 100644 index 000000000..fcb18c4de --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/MaterialSearchView.kt @@ -0,0 +1,231 @@ +/*https://github.com/lapism/search*/ + +package org.koitharu.kotatsu.base.ui.widgets.search + +import android.animation.LayoutTransition +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.ContextCompat +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.widgets.search.internal.SearchLayout + +class MaterialSearchView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : SearchLayout(context, attrs, defStyleAttr, defStyleRes), CoordinatorLayout.AttachedBehavior { + + // ********************************************************************************************* + private var mBehavior: CoordinatorLayout.Behavior<*> = SearchBehavior() + private var mTransition: LayoutTransition? = null + private var mStrokeWidth: Int = 0 + private var mRadius: Float = 0f + private var mElevation: Float = 0f + + // ********************************************************************************************* + init { + inflate(context, R.layout.layout_search_view, this) + init() + setTransition() + + val a = context.obtainStyledAttributes( + attrs, R.styleable.MaterialSearchView, defStyleAttr, defStyleRes + ) + + if (a.hasValue(R.styleable.MaterialSearchView_search_navigationIconSupport)) { + navigationIconSupport = a.getInt( + R.styleable.MaterialSearchView_search_navigationIconSupport, + NavigationIconSupport.NONE + ) + } + + if (a.hasValue(R.styleable.MaterialSearchView_search_navigationIcon)) { + setNavigationIconImageDrawable(a.getDrawable(R.styleable.MaterialSearchView_search_navigationIcon)) + } + + if (a.hasValue(R.styleable.MaterialSearchView_search_clearIcon)) { + setClearIconImageDrawable(a.getDrawable(R.styleable.MaterialSearchView_search_clearIcon)) + } else { + setClearIconImageDrawable( + ContextCompat.getDrawable( + context, + R.drawable.ic_clear + ) + ) + } + + if (a.hasValue(R.styleable.MaterialSearchView_search_micIcon)) { + setMicIconImageDrawable(a.getDrawable(R.styleable.MaterialSearchView_search_micIcon)) + } else { + setMicIconImageDrawable( + ContextCompat.getDrawable( + context, + R.drawable.ic_mic_none + ) + ) + } + + if (a.hasValue(R.styleable.MaterialSearchView_search_menuIcon)) { + setMicIconImageDrawable(a.getDrawable(R.styleable.MaterialSearchView_search_menuIcon)) + } else { + setMicIconImageDrawable( + ContextCompat.getDrawable( + context, + R.drawable.ic_more + ) + ) + } + + if (a.hasValue(R.styleable.MaterialSearchView_search_dividerColor)) { + setDividerColor(a.getInt(R.styleable.MaterialSearchView_search_dividerColor, 0)) + } + + val defaultShadowColor = ContextCompat.getColor(context, R.color.shadow) + setShadowColor( + a.getInt( + R.styleable.MaterialSearchView_search_shadowColor, + defaultShadowColor + ) + ) + + if (a.hasValue(R.styleable.MaterialSearchView_search_textHint)) { + setTextHint(a.getText(R.styleable.MaterialSearchView_search_textHint)) + } + + if (a.hasValue(R.styleable.MaterialSearchView_search_strokeColor)) { + setBackgroundStrokeColor(a.getInt(R.styleable.MaterialSearchView_search_strokeColor, 0)) + } + + if (a.hasValue(R.styleable.MaterialSearchView_search_strokeWidth)) { + setBackgroundStrokeWidth(a.getInt(R.styleable.MaterialSearchView_search_strokeWidth, 0)) + } + + val defaultTransitionDuration = + context.resources.getInteger(R.integer.search_animation_duration) + setTransitionDuration( + a.getInt( + R.styleable.MaterialSearchView_search_transitionDuration, + defaultTransitionDuration + ).toLong() + ) + + val defaultRadius = context.resources.getDimensionPixelSize(R.dimen.search_radius) + setBackgroundRadius( + a.getInt(R.styleable.MaterialSearchView_search_radius, defaultRadius).toFloat() + ) + + val defaultElevation = context.resources.getDimensionPixelSize(R.dimen.search_elevation) + elevation = + a.getInt(R.styleable.MaterialSearchView_android_elevation, defaultElevation).toFloat() + + val imeOptions = a.getInt(R.styleable.MaterialSearchView_android_imeOptions, -1) + if (imeOptions != -1) { + setTextImeOptions(imeOptions) + } + + val inputType = a.getInt(R.styleable.MaterialSearchView_android_inputType, -1) + if (inputType != -1) { + setTextInputType(inputType) + } + + a.recycle() + } + + // ********************************************************************************************* + override fun addFocus() { + mOnFocusChangeListener?.onFocusChange(true) + showKeyboard() + + mStrokeWidth = getBackgroundStrokeWidth() + mRadius = getBackgroundRadius() + mElevation = elevation + + setBackgroundStrokeWidth(context.resources.getDimensionPixelSize(R.dimen.search_stroke_width_focus)) + setBackgroundRadius(resources.getDimensionPixelSize(R.dimen.search_radius_focus).toFloat()) + elevation = + context.resources.getDimensionPixelSize(R.dimen.search_elevation_focus).toFloat() + + val left = context.resources.getDimensionPixelSize(R.dimen.search_dp_16) + val params = mSearchEditText?.layoutParams as LinearLayout.LayoutParams + params.setMargins(left, 0, 0, 0) + mSearchEditText?.layoutParams = params + + margins = Margins.FOCUS + setLayoutHeight(context.resources.getDimensionPixelSize(R.dimen.search_layout_height_focus)) + + mViewShadow?.visibility = View.VISIBLE + + mViewDivider?.visibility = View.VISIBLE + mViewAnim?.visibility = View.VISIBLE + mRecyclerView?.visibility = View.VISIBLE + + // layoutTransition = null + } + + override fun removeFocus() { + // layoutTransition = mTransition + + mOnFocusChangeListener?.onFocusChange(false) + hideKeyboard() + + val params = mSearchEditText?.layoutParams as LinearLayout.LayoutParams + params.setMargins(0, 0, 0, 0) + mSearchEditText?.layoutParams = params + + setBackgroundStrokeWidth(mStrokeWidth) + setBackgroundRadius(mRadius) + elevation = mElevation + + setLayoutHeight(context.resources.getDimensionPixelSize(R.dimen.search_layout_height)) + margins = Margins.NO_FOCUS + + mViewShadow?.visibility = View.GONE + + mRecyclerView?.visibility = View.GONE + mViewAnim?.visibility = View.GONE + mViewDivider?.visibility = View.GONE + } + + override fun getBehavior(): CoordinatorLayout.Behavior<*> { + return mBehavior + } + + fun setBehavior(behavior: CoordinatorLayout.Behavior<*>) { + mBehavior = behavior + } + + fun setTransitionDuration(duration: Long) { + mTransition?.setDuration(duration) + layoutTransition = mTransition + } + + private fun setTransition() { + mTransition = LayoutTransition() + mTransition?.enableTransitionType(LayoutTransition.CHANGING) + mTransition?.addTransitionListener(object : LayoutTransition.TransitionListener { + override fun startTransition( + transition: LayoutTransition?, + container: ViewGroup?, + view: View?, + transitionType: Int + ) { + + } + + override fun endTransition( + transition: LayoutTransition?, + container: ViewGroup?, + view: View?, + transitionType: Int + ) { + + } + }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/SearchArrowDrawable.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/SearchArrowDrawable.kt new file mode 100644 index 000000000..752c0071d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/SearchArrowDrawable.kt @@ -0,0 +1,62 @@ +/*https://github.com/lapism/search*/ + +package org.koitharu.kotatsu.base.ui.widgets.search + +import android.animation.ObjectAnimator +import android.content.Context +import android.util.Property +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.appcompat.graphics.drawable.DrawerArrowDrawable +import androidx.core.content.ContextCompat + +class SearchArrowDrawable constructor(context: Context) : DrawerArrowDrawable(context) { + + var position: Float + get() = progress + set(position) { + progress = position + } + + init { + color = ContextCompat.getColor(context, android.R.color.white) + } + + fun animate(state: Float, duration: Long) { + val anim: ObjectAnimator = if (state == ARROW) { + ObjectAnimator.ofFloat( + this, + PROGRESS, + MENU, + state + ) + } else { + ObjectAnimator.ofFloat( + this, + PROGRESS, + ARROW, + state + ) + } + anim.interpolator = AccelerateDecelerateInterpolator() + anim.duration = duration + anim.start() + } + + companion object { + + const val MENU = 0.0f + const val ARROW = 1.0f + + private val PROGRESS = + object : Property(Float::class.java, "progress") { + override fun set(obj: SearchArrowDrawable, value: Float?) { + obj.progress = value!! + } + + override fun get(obj: SearchArrowDrawable): Float { + return obj.progress + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/SearchBehavior.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/SearchBehavior.kt new file mode 100644 index 000000000..04e897837 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/SearchBehavior.kt @@ -0,0 +1,54 @@ +/*https://github.com/lapism/search*/ + +package org.koitharu.kotatsu.base.ui.widgets.search + +import android.view.View +import android.widget.LinearLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.ViewCompat +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.bottomnavigation.BottomNavigationView +import org.koitharu.kotatsu.base.ui.widgets.search.internal.SearchLayout + +class SearchBehavior : CoordinatorLayout.Behavior() { + + override fun layoutDependsOn( + parent: CoordinatorLayout, + child: S, + dependency: View + ): Boolean { + return if (dependency is AppBarLayout) { + true + } else + if (dependency is LinearLayout || dependency is BottomNavigationView) { + dependency.z = child.z + 1 + true + } else { + super.layoutDependsOn(parent, child, dependency) + } + } + + override fun onDependentViewChanged( + parent: CoordinatorLayout, + child: S, + dependency: View + ): Boolean { + if (dependency is AppBarLayout) { + child.translationY = dependency.getY() + return true + } + return super.onDependentViewChanged(parent, child, dependency) + } + + override fun onStartNestedScroll( + coordinatorLayout: CoordinatorLayout, + child: S, + directTargetChild: View, + target: View, + axes: Int, + type: Int + ): Boolean { + return axes == ViewCompat.SCROLL_AXIS_VERTICAL + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchEditText.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchEditText.kt new file mode 100644 index 000000000..312e59e9f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchEditText.kt @@ -0,0 +1,40 @@ +/*https://github.com/lapism/search*/ + +package org.koitharu.kotatsu.base.ui.widgets.search.internal + +import android.content.Context +import android.util.AttributeSet +import android.view.KeyEvent +import androidx.annotation.AttrRes +import androidx.appcompat.widget.AppCompatEditText + +class SearchEditText : AppCompatEditText { + + var clearFocusOnBackPressed: Boolean = false + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP && clearFocusOnBackPressed) { + if (hasFocus()) { + clearFocus() + return true + } + } + return super.onKeyPreIme(keyCode, event) + } + + override fun clearFocus() { + super.clearFocus() + text?.clear() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchLayout.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchLayout.kt new file mode 100644 index 000000000..0c00f32de --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchLayout.kt @@ -0,0 +1,725 @@ +/*https://github.com/lapism/search*/ + +package org.koitharu.kotatsu.base.ui.widgets.search.internal + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.ColorFilter +import android.graphics.PorterDuff +import android.graphics.Rect +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.os.Parcelable +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.LinearLayout +import androidx.annotation.* +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DefaultItemAnimator +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.card.MaterialCardView +import org.koitharu.kotatsu.R + +abstract class SearchLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), View.OnClickListener { + + // ********************************************************************************************* + // Better way than enum class :-) + @IntDef( + NavigationIconSupport.NONE, + NavigationIconSupport.MENU, + NavigationIconSupport.ARROW, + NavigationIconSupport.SEARCH + ) + @Retention(AnnotationRetention.SOURCE) + annotation class NavigationIconSupport { + companion object { + const val NONE = 0 + const val MENU = 1 + const val ARROW = 2 + const val SEARCH = 3 + } + } + + @IntDef( + Margins.NO_FOCUS, + Margins.FOCUS + ) + @Retention(AnnotationRetention.SOURCE) + internal annotation class Margins { + companion object { + const val NO_FOCUS = 4 + const val FOCUS = 5 + } + } + + // ********************************************************************************************* + private var mImageViewMic: ImageButton? = null + private var mImageViewMenu: ImageButton? = null + protected var mRecyclerView: RecyclerView? = null + private var mMaterialCardView: MaterialCardView? = null + var mSearchEditText: SearchEditText? = null + protected var mViewShadow: View? = null + protected var mViewDivider: View? = null + protected var mViewAnim: View? = null + protected var mOnFocusChangeListener: OnFocusChangeListener? = null + + private var mLinearLayout: LinearLayout? = null + private var mImageViewNavigation: ImageButton? = null + private var mImageViewClear: ImageButton? = null + private var mOnQueryTextListener: OnQueryTextListener? = null + private var mOnNavigationClickListener: OnNavigationClickListener? = null + private var mOnMicClickListener: OnMicClickListener? = null + private var mOnMenuClickListener: OnMenuClickListener? = null + private var mOnClearClickListener: OnClearClickListener? = null + + // ********************************************************************************************* + @NavigationIconSupport + @get:NavigationIconSupport + var navigationIconSupport: Int = 0 + set(@NavigationIconSupport navigationIconSupport) { + field = navigationIconSupport + + when (navigationIconSupport) { + NavigationIconSupport.NONE + -> { + setNavigationIconImageDrawable(null) + } + NavigationIconSupport.MENU -> { + setNavigationIconImageDrawable( + ContextCompat.getDrawable( + context, + R.drawable.ic_menu + ) + ) + } + NavigationIconSupport.ARROW -> { + setNavigationIconImageDrawable( + ContextCompat.getDrawable( + context, + R.drawable.ic_arrow_back + ) + ) + } + NavigationIconSupport.SEARCH -> { + setNavigationIconImageDrawable( + ContextCompat.getDrawable( + context, + R.drawable.ic_search + ) + ) + } + } + } + + @Margins + @get:Margins + protected var margins: Int = 0 + set(@Margins margins) { + field = margins + + val left: Int + val top: Int + val right: Int + val bottom: Int + val params = mMaterialCardView?.layoutParams as LayoutParams? + + when (margins) { + Margins.NO_FOCUS -> { + left = + context.resources.getDimensionPixelSize(R.dimen.search_margins_left_right) + top = + context.resources.getDimensionPixelSize(R.dimen.search_margins_top_bottom) + right = + context.resources.getDimensionPixelSize(R.dimen.search_margins_left_right) + bottom = + context.resources.getDimensionPixelSize(R.dimen.search_margins_top_bottom) + + params?.width = ViewGroup.LayoutParams.MATCH_PARENT + params?.height = ViewGroup.LayoutParams.WRAP_CONTENT + params?.setMargins(left, top, right, bottom) + mMaterialCardView?.layoutParams = params + } + Margins.FOCUS -> { + left = + context.resources.getDimensionPixelSize(R.dimen.search_margins_focus) + top = + context.resources.getDimensionPixelSize(R.dimen.search_margins_focus) + right = + context.resources.getDimensionPixelSize(R.dimen.search_margins_focus) + bottom = + context.resources.getDimensionPixelSize(R.dimen.search_margins_focus) + + params?.width = ViewGroup.LayoutParams.MATCH_PARENT + params?.height = ViewGroup.LayoutParams.MATCH_PARENT + params?.setMargins(left, top, right, bottom) + mMaterialCardView?.layoutParams = params + } + } + } + + // ********************************************************************************************* + protected abstract fun addFocus() + + protected abstract fun removeFocus() + + // ********************************************************************************************* + protected fun init() { + mLinearLayout = findViewById(R.id.search_linear_layout) + + mImageViewNavigation = findViewById(R.id.search_image_view_navigation) + mImageViewNavigation?.setOnClickListener(this) + + mImageViewMic = findViewById(R.id.search_image_view_mic) + mImageViewMic?.setOnClickListener(this) + + mImageViewMenu = findViewById(R.id.search_image_view_menu) + mImageViewMenu?.setOnClickListener(this) + + mImageViewClear = findViewById(R.id.search_image_view_clear) + mImageViewClear?.visibility = View.GONE + mImageViewClear?.setOnClickListener(this) + + mSearchEditText = findViewById(R.id.search_search_edit_text) + mSearchEditText?.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + this@SearchLayout.onTextChanged(s) + } + + override fun afterTextChanged(s: Editable?) { + + } + }) + mSearchEditText?.setOnEditorActionListener { _, _, _ -> + onSubmitQuery() + return@setOnEditorActionListener true // true + } + mSearchEditText?.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + addFocus() + } else { + removeFocus() + } + } + + mRecyclerView = findViewById(R.id.search_recycler_view) + mRecyclerView?.visibility = View.GONE + mRecyclerView?.layoutManager = LinearLayoutManager(context) + mRecyclerView?.isNestedScrollingEnabled = false + mRecyclerView?.itemAnimator = DefaultItemAnimator() + mRecyclerView?.overScrollMode = View.OVER_SCROLL_NEVER + mRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { + hideKeyboard() + } + } + }) + + mViewShadow = findViewById(R.id.search_view_shadow) + mViewShadow?.visibility = View.GONE + + mViewDivider = findViewById(R.id.search_view_divider) + mViewDivider?.visibility = View.GONE + + mViewAnim = findViewById(R.id.search_view_anim) + mViewAnim?.visibility = View.GONE + + mMaterialCardView = findViewById(R.id.search_material_card_view) + margins = Margins.NO_FOCUS + + isClickable = true + isFocusable = true + isFocusableInTouchMode = true + } + + // ********************************************************************************************* + fun setNavigationIconVisibility(visibility: Int) { + mImageViewNavigation?.visibility = visibility + } + + fun setNavigationIconImageResource(@DrawableRes resId: Int) { + mImageViewNavigation?.setImageResource(resId) + } + + fun setNavigationIconImageDrawable(@Nullable drawable: Drawable?) { + mImageViewNavigation?.setImageDrawable(drawable) + } + + fun setNavigationIconColorFilter(color: Int) { + mImageViewNavigation?.setColorFilter(color) + } + + fun setNavigationIconColorFilter(color: Int, mode: PorterDuff.Mode) { + mImageViewNavigation?.setColorFilter(color, mode) + } + + fun setNavigationIconColorFilter(cf: ColorFilter?) { + mImageViewNavigation?.colorFilter = cf + } + + fun clearNavigationIconColorFilter() { + mImageViewNavigation?.clearColorFilter() + } + + fun setNavigationIconContentDescription(contentDescription: CharSequence) { + mImageViewNavigation?.contentDescription = contentDescription + } + + // ********************************************************************************************* + fun setMicIconImageResource(@DrawableRes resId: Int) { + mImageViewMic?.setImageResource(resId) + } + + fun setMicIconImageDrawable(@Nullable drawable: Drawable?) { + mImageViewMic?.setImageDrawable(drawable) + } + + fun setMicIconColorFilter(color: Int) { + mImageViewMic?.setColorFilter(color) + } + + fun setMicIconColorFilter(color: Int, mode: PorterDuff.Mode) { + mImageViewMic?.setColorFilter(color, mode) + } + + fun setMicIconColorFilter(cf: ColorFilter?) { + mImageViewMic?.colorFilter = cf + } + + fun clearMicIconColorFilter() { + mImageViewMic?.clearColorFilter() + } + + fun setMicIconContentDescription(contentDescription: CharSequence) { + mImageViewMic?.contentDescription = contentDescription + } + + // ********************************************************************************************* + fun setMenuIconImageResource(@DrawableRes resId: Int) { + mImageViewMenu?.setImageResource(resId) + } + + fun setMenuIconImageDrawable(@Nullable drawable: Drawable?) { + mImageViewMenu?.setImageDrawable(drawable) + } + + fun setMenuIconColorFilter(color: Int) { + mImageViewMenu?.setColorFilter(color) + } + + fun setMenuIconColorFilter(color: Int, mode: PorterDuff.Mode) { + mImageViewMenu?.setColorFilter(color, mode) + } + + fun setMenuIconColorFilter(cf: ColorFilter?) { + mImageViewMenu?.colorFilter = cf + } + + fun clearMenuIconColorFilter() { + mImageViewMenu?.clearColorFilter() + } + + fun setMenuIconContentDescription(contentDescription: CharSequence) { + mImageViewMenu?.contentDescription = contentDescription + } + + // ********************************************************************************************* + fun setClearIconImageResource(@DrawableRes resId: Int) { + mImageViewClear?.setImageResource(resId) + } + + fun setClearIconImageDrawable(@Nullable drawable: Drawable?) { + mImageViewClear?.setImageDrawable(drawable) + } + + fun setClearIconColorFilter(color: Int) { + mImageViewClear?.setColorFilter(color) + } + + fun setClearIconColorFilter(color: Int, mode: PorterDuff.Mode) { + mImageViewClear?.setColorFilter(color, mode) + } + + fun setClearIconColorFilter(cf: ColorFilter?) { + mImageViewClear?.colorFilter = cf + } + + fun clearClearIconColorFilter() { + mImageViewClear?.clearColorFilter() + } + + fun setClearIconContentDescription(contentDescription: CharSequence) { + mImageViewClear?.contentDescription = contentDescription + } + + // ********************************************************************************************* + fun setAdapterLayoutManager(@Nullable layout: RecyclerView.LayoutManager?) { + mRecyclerView?.layoutManager = layout + } + + // only when height == match_parent + fun setAdapterHasFixedSize(hasFixedSize: Boolean) { + mRecyclerView?.setHasFixedSize(hasFixedSize) + } + + fun addAdapterItemDecoration(@NonNull decor: RecyclerView.ItemDecoration) { + mRecyclerView?.addItemDecoration(decor) + } + + fun removeAdapterItemDecoration(@NonNull decor: RecyclerView.ItemDecoration) { + mRecyclerView?.removeItemDecoration(decor) + } + + fun setAdapter(@Nullable adapter: RecyclerView.Adapter<*>?) { + mRecyclerView?.adapter = adapter + } + + @Nullable + fun getAdapter(): RecyclerView.Adapter<*>? { + return mRecyclerView?.adapter + } + + // ********************************************************************************************* + /** + * Typeface.NORMAL + * Typeface.BOLD + * Typeface.ITALIC + * Typeface.BOLD_ITALIC + * + * Typeface.DEFAULT + * Typeface.DEFAULT_BOLD + * Typeface.SANS_SERIF + * Typeface.SERIF + * Typeface.MONOSPACE + * + * Typeface.create(Typeface.NORMAL, Typeface.DEFAULT) + */ + fun setTextTypeface(@Nullable tf: Typeface?) { + mSearchEditText?.typeface = tf + } + + fun getTextTypeface(): Typeface? { + return mSearchEditText?.typeface + } + + fun setTextInputType(type: Int) { + mSearchEditText?.inputType = type + } + + fun getTextInputType(): Int? { + return mSearchEditText?.inputType + } + + fun setTextImeOptions(imeOptions: Int) { + mSearchEditText?.imeOptions = imeOptions + } + + fun getTextImeOptions(): Int? { + return mSearchEditText?.imeOptions + } + + fun setTextQuery(query: CharSequence?, submit: Boolean) { + mSearchEditText?.setText(query) + if (query != null) { + mSearchEditText?.setSelection(mSearchEditText?.length()!!) + } + if (submit && !TextUtils.isEmpty(query)) { + onSubmitQuery() + } + } + + @Nullable + fun getTextQuery(): CharSequence? { + return mSearchEditText?.text + } + + fun setTextHint(hint: CharSequence?) { + mSearchEditText?.hint = hint + } + + fun getTextHint(): CharSequence? { + return mSearchEditText?.hint + } + + fun setTextColor(@ColorInt color: Int) { + mSearchEditText?.setTextColor(color) + } + + fun setTextSize(size: Float) { + mSearchEditText?.textSize = size + } + + fun setTextGravity(gravity: Int) { + mSearchEditText?.gravity = gravity + } + + fun setTextHint(@StringRes resid: Int) { + mSearchEditText?.setHint(resid) + } + + fun setTextHintColor(@ColorInt color: Int) { + mSearchEditText?.setHintTextColor(color) + } + + fun setClearFocusOnBackPressed(clearFocusOnBackPressed: Boolean) { + mSearchEditText?.clearFocusOnBackPressed = clearFocusOnBackPressed + } + + // ********************************************************************************************* + override fun setBackgroundColor(@ColorInt color: Int) { + mMaterialCardView?.setCardBackgroundColor(color) + } + + fun setBackgroundColor(@Nullable color: ColorStateList?) { + mMaterialCardView?.setCardBackgroundColor(color) + } + + override fun setElevation(elevation: Float) { + mMaterialCardView?.cardElevation = elevation + mMaterialCardView?.maxCardElevation = elevation + } + + override fun getElevation(): Float { + return mMaterialCardView?.elevation!! + } + + fun setBackgroundRadius(radius: Float) { + mMaterialCardView?.radius = radius + } + + fun getBackgroundRadius(): Float { + return mMaterialCardView?.radius!! + } + + fun setBackgroundRippleColor(@ColorRes rippleColorResourceId: Int) { + mMaterialCardView?.setRippleColorResource(rippleColorResourceId) + } + + fun setBackgroundRippleColorResource(@Nullable rippleColor: ColorStateList?) { + mMaterialCardView?.rippleColor = rippleColor + } + + fun setBackgroundStrokeColor(@ColorInt strokeColor: Int) { + mMaterialCardView?.strokeColor = strokeColor + } + + fun setBackgroundStrokeColor(strokeColor: ColorStateList) { + mMaterialCardView?.setStrokeColor(strokeColor) + } + + fun setBackgroundStrokeWidth(@Dimension strokeWidth: Int) { + mMaterialCardView?.strokeWidth = strokeWidth + } + + @Dimension + fun getBackgroundStrokeWidth(): Int { + return mMaterialCardView?.strokeWidth!! + } + + // ********************************************************************************************* + fun setDividerColor(@ColorInt color: Int) { + mViewDivider?.setBackgroundColor(color) + } + + fun setShadowColor(@ColorInt color: Int) { + mViewShadow?.setBackgroundColor(color) + } + + // ********************************************************************************************* + fun setOnFocusChangeListener(listener: OnFocusChangeListener) { + mOnFocusChangeListener = listener + } + + fun setOnQueryTextListener(listener: OnQueryTextListener) { + mOnQueryTextListener = listener + } + + fun setOnNavigationClickListener(listener: OnNavigationClickListener) { + mOnNavigationClickListener = listener + } + + fun setOnMicClickListener(listener: OnMicClickListener) { + mOnMicClickListener = listener + } + + fun setOnMenuClickListener(listener: OnMenuClickListener) { + mOnMenuClickListener = listener + } + + fun setOnClearClickListener(listener: OnClearClickListener) { + mOnClearClickListener = listener + } + + // ********************************************************************************************* + fun showKeyboard() { + if (!isInEditMode) { + val inputMethodManager = + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.showSoftInput( + mSearchEditText, + InputMethodManager.RESULT_UNCHANGED_SHOWN + ) + } + } + + fun hideKeyboard() { + if (!isInEditMode) { + val inputMethodManager = + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow( + windowToken, + InputMethodManager.RESULT_UNCHANGED_SHOWN + ) + } + } + + // ********************************************************************************************* + protected fun setLayoutHeight(height: Int) { + val params = mLinearLayout?.layoutParams + params?.height = height + params?.width = ViewGroup.LayoutParams.MATCH_PARENT + mLinearLayout?.layoutParams = params + } + + // ********************************************************************************************* + private fun onTextChanged(newText: CharSequence) { + if (!TextUtils.isEmpty(newText)) { + mImageViewMic?.visibility = View.GONE + mImageViewClear?.visibility = View.VISIBLE + } else { + mImageViewClear?.visibility = View.GONE + if (mSearchEditText?.hasFocus()!!) { + mImageViewMic?.visibility = View.VISIBLE + } else { + mImageViewMic?.visibility = View.GONE + } + } + + if (mOnQueryTextListener != null) { + mOnQueryTextListener?.onQueryTextChange(newText) + } + } + + private fun onSubmitQuery() { + val query = mSearchEditText?.text + if (query != null && TextUtils.getTrimmedLength(query) > 0) { + if (mOnQueryTextListener == null || !mOnQueryTextListener!!.onQueryTextSubmit(query.toString())) { + mSearchEditText?.text = query + } + } + } + + // ********************************************************************************************* + override fun onSaveInstanceState(): Parcelable? { + val superState = super.onSaveInstanceState() + val ss = SearchViewSavedState(superState!!) + if (mSearchEditText?.text!!.isNotEmpty()) { + ss.query = mSearchEditText?.text + } + ss.hasFocus = mSearchEditText?.hasFocus()!! + return ss + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state !is SearchViewSavedState) { + super.onRestoreInstanceState(state) + return + } + super.onRestoreInstanceState(state.superState) + if (state.hasFocus) { + mSearchEditText?.requestFocus() + } + if (state.query != null) { + setTextQuery(state.query, false) + } + requestLayout() + } + + override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean { + return if (!isFocusable) { + false + } else { + mSearchEditText?.requestFocus(direction, previouslyFocusedRect)!! + } + } + + override fun clearFocus() { + super.clearFocus() + mSearchEditText?.clearFocus() + } + + override fun onClick(view: View?) { + if (view === mImageViewNavigation) { + if (mOnNavigationClickListener != null) { + mOnNavigationClickListener?.onNavigationClick(mSearchEditText?.hasFocus()!!) + } + } else if (view === mImageViewMic) { + if (mOnMicClickListener != null) { + mOnMicClickListener?.onMicClick() + } + } else if (view === mImageViewMenu) { + if (mOnMenuClickListener != null) { + mOnMenuClickListener?.onMenuClick() + } + } else if (view === mImageViewClear) { + if (mSearchEditText?.text!!.isNotEmpty()) { + mSearchEditText?.text!!.clear() + } + if (mOnClearClickListener != null) { + mOnClearClickListener?.onClearClick() + } + } + } + + // ********************************************************************************************* + interface OnFocusChangeListener { + + fun onFocusChange(hasFocus: Boolean) + } + + interface OnQueryTextListener { + + fun onQueryTextChange(newText: CharSequence): Boolean + + fun onQueryTextSubmit(query: CharSequence): Boolean + } + + interface OnNavigationClickListener { + + fun onNavigationClick(hasFocus: Boolean) + } + + interface OnMicClickListener { + + fun onMicClick() + } + + interface OnMenuClickListener { + + fun onMenuClick() + } + + interface OnClearClickListener { + + fun onClearClick() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchViewSavedState.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchViewSavedState.kt new file mode 100644 index 000000000..394a770c9 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/internal/SearchViewSavedState.kt @@ -0,0 +1,21 @@ +/*https://github.com/lapism/search*/ + +package org.koitharu.kotatsu.base.ui.widgets.search.internal + +import android.os.Parcel +import android.os.Parcelable +import android.text.TextUtils +import android.view.View + +internal class SearchViewSavedState(superState: Parcelable) : View.BaseSavedState(superState) { + + var query: CharSequence? = null + var hasFocus: Boolean = false + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + TextUtils.writeToParcel(query, out, flags) + out.writeInt(if (hasFocus) 1 else 0) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/util/SearchUtils.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/util/SearchUtils.kt new file mode 100644 index 000000000..ed93e0848 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/search/util/SearchUtils.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.base.ui.widgets.search.util + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.speech.RecognizerIntent + +object SearchUtils { + + const val SPEECH_REQUEST_CODE = 300 + + @JvmStatic + fun setVoiceSearch(activity: Activity, text: String) { + val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) + // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra( + RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_FREE_FORM + ) + intent.putExtra(RecognizerIntent.EXTRA_PROMPT, text) + intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1) + + activity.startActivityForResult( + intent, + SPEECH_REQUEST_CODE + ) + } + + @JvmStatic + fun isVoiceSearchAvailable(context: Context): Boolean { + val pm = context.packageManager + val activities = + pm.queryIntentActivities(Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0) + return activities.size != 0 + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 50a5d7302..e63c7d987 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -60,17 +60,41 @@ class DetailsFragment : BaseFragment(), View.OnClickList .lifecycle(viewLifecycleOwner) .enqueueWith(coil) textViewTitle.text = manga.title - textViewSubtitle.textAndVisible = manga.altTitle + textViewAuthor.textAndVisible = manga.author + textViewSource.text = manga.source.title textViewDescription.text = manga.description?.parseAsHtml()?.takeUnless(Spanned::isBlank) ?: getString(R.string.no_description) + if (manga.chapters?.isNotEmpty() == true) { + chaptersContainer.isVisible = true + textViewChapters.text = manga.chapters.let { + resources.getQuantityString( + R.plurals.chapters, + it.size, + manga.chapters.size + ) + } + } else { + chaptersContainer.isVisible = false + } if (manga.rating == Manga.NO_RATING) { ratingBar.isVisible = false + ratingContainer.isVisible = false } else { ratingBar.progress = (ratingBar.max * manga.rating).roundToInt() ratingBar.isVisible = true + textViewRating.text = String.format("%.1f", manga.rating * 5) + ratingContainer.isVisible = true } - imageViewFavourite.setOnClickListener(this@DetailsFragment) + val file = manga.url.toUri().toFileOrNull() + if (file != null) { + val size = file.length() + textViewSize.text = FileSizeUtils.formatBytes(requireContext(), size) + sizeContainer.isVisible = true + } else { + sizeContainer.isVisible = false + } + buttonFavorite.setOnClickListener(this@DetailsFragment) buttonRead.setOnClickListener(this@DetailsFragment) buttonRead.setOnLongClickListener(this@DetailsFragment) buttonRead.isEnabled = !manga.chapters.isNullOrEmpty() @@ -91,13 +115,13 @@ class DetailsFragment : BaseFragment(), View.OnClickList } private fun onFavouriteChanged(isFavourite: Boolean) { - binding.imageViewFavourite.setImageResource( + with(binding.buttonFavorite) { if (isFavourite) { - R.drawable.ic_heart + this?.setIconResource(R.drawable.ic_heart) } else { - R.drawable.ic_heart_outline + this?.setIconResource(R.drawable.ic_heart_outline) } - ) + } } private fun onLoadingStateChanged(isLoading: Boolean) { @@ -107,7 +131,7 @@ class DetailsFragment : BaseFragment(), View.OnClickList override fun onClick(v: View) { val manga = viewModel.manga.value when (v.id) { - R.id.imageView_favourite -> { + R.id.button_favorite -> { FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return) } R.id.button_read -> { @@ -163,31 +187,10 @@ class DetailsFragment : BaseFragment(), View.OnClickList tagsJob?.cancel() tagsJob = viewLifecycleScope.launch { val tags = ArrayList(manga.tags.size + 2) - if (manga.author != null) { - tags += ChipsView.ChipModel( - title = manga.author, - icon = R.drawable.ic_chip_user - ) - } for (tag in manga.tags) { tags += ChipsView.ChipModel( title = tag.title, - icon = R.drawable.ic_chip_tag - ) - } - val file = manga.url.toUri().toFileOrNull() - if (file != null) { - val size = withContext(Dispatchers.IO) { - file.length() - } - tags += ChipsView.ChipModel( - title = FileSizeUtils.formatBytes(requireContext(), size), - icon = R.drawable.ic_chip_storage - ) - } else { - tags += ChipsView.ChipModel( - title = manga.source.title, - icon = R.drawable.ic_chip_web + icon = 0 ) } binding.chipsTags.setChips(tags) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index da163f722..d75a627df 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -4,16 +4,15 @@ import android.os.Bundle import android.view.* import androidx.annotation.CallSuper import androidx.appcompat.widget.PopupMenu +import androidx.core.content.ContextCompat import androidx.core.graphics.Insets import androidx.core.view.GravityCompat import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.drawerlayout.widget.DrawerLayout -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch @@ -81,6 +80,10 @@ abstract class MangaListFragment : BaseFragment(), addOnScrollListener(paginationListener!!) } with(binding.swipeRefreshLayout) { + setColorSchemeColors( + ContextCompat.getColor(context, R.color.color_primary), + ContextCompat.getColor(context, R.color.color_primary_variant) + ) setOnRefreshListener(this@MangaListFragment) isEnabled = isSwipeRefreshEnabled } @@ -246,13 +249,9 @@ abstract class MangaListFragment : BaseFragment(), when (mode) { ListMode.LIST -> { layoutManager = LinearLayoutManager(context) - addItemDecoration( - DividerItemDecoration( - context, - RecyclerView.VERTICAL - ) - ) - updatePadding(left = 0, right = 0) + val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) + addItemDecoration(SpacingItemDecoration(spacing)) + updatePadding(left = spacing, right = spacing) } ListMode.DETAILED_LIST -> { layoutManager = LinearLayoutManager(context) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt index 9f3b2f561..b7a367591 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt @@ -6,7 +6,6 @@ import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.prefs.ListMode -import kotlin.math.roundToInt fun Manga.toListModel() = MangaListModel( id = id, @@ -20,7 +19,7 @@ fun Manga.toListDetailedModel() = MangaListDetailedModel( id = id, title = title, subtitle = altTitle, - rating = if (rating == Manga.NO_RATING) null else "${(rating * 10).roundToInt()}/10", + rating = if (rating == Manga.NO_RATING) null else String.format("%.1f", rating * 5), tags = tags.joinToString(", ") { it.title }, coverUrl = coverUrl, manga = this diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index cd95e7e8a..58a5e82a5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -6,10 +6,7 @@ import android.content.res.Configuration import android.graphics.Color import android.os.Build import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AlertDialog import androidx.core.graphics.Insets @@ -17,6 +14,7 @@ import androidx.core.view.* import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit +import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.CircularProgressDrawable import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar @@ -27,6 +25,7 @@ import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.prefs.AppSection import org.koitharu.kotatsu.databinding.ActivityMainBinding +import org.koitharu.kotatsu.databinding.NavigationHeaderBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.favourites.ui.FavouritesContainerFragment import org.koitharu.kotatsu.history.ui.HistoryListFragment @@ -44,6 +43,7 @@ import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.navigationItemBackground import org.koitharu.kotatsu.utils.ext.resolveDp class MainActivity : BaseActivity(), @@ -55,12 +55,14 @@ class MainActivity : BaseActivity(), mode = LazyThreadSafetyMode.NONE ) + private lateinit var navHeaderBinding: NavigationHeaderBinding private lateinit var drawerToggle: ActionBarDrawerToggle private var searchUi: SearchUI? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater)) + navHeaderBinding = NavigationHeaderBinding.inflate(layoutInflater) drawerToggle = ActionBarDrawerToggle( this, binding.drawer, @@ -71,7 +73,18 @@ class MainActivity : BaseActivity(), binding.drawer.addDrawerListener(drawerToggle) supportActionBar?.setDisplayHomeAsUpEnabled(true) - binding.navigationView.setNavigationItemSelectedListener(this) + binding.navigationView.apply { + val menuView = findViewById(com.google.android.material.R.id.design_navigation_view) + navHeaderBinding.root.setOnApplyWindowInsetsListener { v, insets -> + v.updatePadding(top = insets.systemWindowInsetTop) + // NavigationView doesn't dispatch insets to the menu view, so pad the bottom here. + menuView.updatePadding(bottom = insets.systemWindowInsetBottom) + insets + } + addHeaderView(navHeaderBinding.root) + itemBackground = navigationItemBackground(context) + setNavigationItemSelectedListener(this@MainActivity) + } with(binding.fab) { imageTintList = ColorStateList.valueOf(Color.WHITE) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 22d66a6c6..e34239b56 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -38,7 +38,7 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReader import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment -import org.koitharu.kotatsu.reader.ui.pager.wetoon.WebtoonReaderFragment +import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.utils.GridTouchHelper diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/ListPaginationListener.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/ListPaginationListener.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/ListPaginationListener.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/ListPaginationListener.kt index bcd6ec292..a3879a762 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/ListPaginationListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/ListPaginationListener.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.pager.wetoon +package org.koitharu.kotatsu.reader.ui.pager.webtoon import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonAdapter.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt index 34e3b4713..a089d4d17 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.pager.wetoon +package org.koitharu.kotatsu.reader.ui.pager.webtoon import android.view.LayoutInflater import android.view.ViewGroup diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonFrameLayout.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonFrameLayout.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt index cf0879e4c..2fe267156 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonFrameLayout.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.pager.wetoon +package org.koitharu.kotatsu.reader.ui.pager.webtoon import android.content.Context import android.util.AttributeSet diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonHolder.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt index f7ac4b541..fcb81c602 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.pager.wetoon +package org.koitharu.kotatsu.reader.ui.pager.webtoon import android.net.Uri import android.view.View diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonImageView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonImageView.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt index 88845b512..09cb044c0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonImageView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.pager.wetoon +package org.koitharu.kotatsu.reader.ui.pager.webtoon import android.content.Context import android.graphics.PointF diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonReaderFragment.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt index e1b1ab879..1cb0b1079 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonReaderFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.pager.wetoon +package org.koitharu.kotatsu.reader.ui.pager.webtoon import android.os.Bundle import android.view.LayoutInflater diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonRecyclerView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonRecyclerView.kt rename to app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt index 16287fbeb..f4535d7be 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/wetoon/WebtoonRecyclerView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.reader.ui.pager.wetoon +package org.koitharu.kotatsu.reader.ui.pager.webtoon import android.content.Context import android.util.AttributeSet diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt index 4abb468a9..99fd2bcd1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt @@ -47,9 +47,6 @@ class FeedFragment : BaseFragment(), PaginationScrollListen feedAdapter = FeedAdapter(get(), viewLifecycleOwner, this) with(binding.recyclerView) { adapter = feedAdapter - addItemDecoration( - SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)) - ) setHasFixedSize(true) addOnScrollListener(PaginationScrollListener(4, this@FeedFragment)) } @@ -134,7 +131,7 @@ class FeedFragment : BaseFragment(), PaginationScrollListen return } val summaryText = getString( - R.string.chapers_checking_progress, + R.string.chapters_checking_progress, progress.value + 1, progress.total ) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt index 75ffadd6d..2e3bdf446 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt @@ -37,7 +37,7 @@ fun feedItemAD( .lifecycle(lifecycleOwner) .enqueueWith(coil) binding.textViewTitle.text = item.title - binding.textViewSubtitle.text = item.subtitle + binding.badge.text = item.subtitle binding.textViewChapters.text = item.chapters } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt index 87c7efd2f..29ebb4790 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/model/ListModelConversionExt.kt @@ -1,10 +1,8 @@ package org.koitharu.kotatsu.tracker.ui.model import android.content.res.Resources -import android.text.format.DateUtils import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.TrackingLogItem -import org.koitharu.kotatsu.utils.ext.formatRelative fun TrackingLogItem.toFeedItem(resources: Resources): FeedItem { val chaptersString = if (chapters.size > MAX_CHAPTERS) { @@ -23,17 +21,7 @@ fun TrackingLogItem.toFeedItem(resources: Resources): FeedItem { id = id, imageUrl = manga.coverUrl, title = manga.title, - subtitle = buildString { - append(createdAt.formatRelative(DateUtils.DAY_IN_MILLIS)) - append(" ") - append( - resources.getQuantityString( - R.plurals.new_chapters, - chapters.size, - chapters.size - ) - ) - }, + subtitle = chapters.size.toString(), chapters = chaptersString, manga = manga ) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt index 5b3081d8e..e31e3e20b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt @@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.network.CommonHeaders @Suppress("NOTHING_TO_INLINE") inline fun ImageView.newImageRequest(url: String) = ImageRequest.Builder(context) .data(url) + .crossfade(true) .target(this) @Suppress("NOTHING_TO_INLINE") diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/UiExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/UiExt.kt new file mode 100644 index 000000000..f66e9f880 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/UiExt.kt @@ -0,0 +1,21 @@ +package org.koitharu.kotatsu.utils.ext + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.DrawableCompat +import org.koitharu.kotatsu.R + +fun navigationItemBackground(context: Context): Drawable? { + // Need to inflate the drawable and CSL via AppCompatResources to work on Lollipop + // From Google I/O repo (https://github.com/google/iosched) + var background = AppCompatResources.getDrawable(context, R.drawable.navigation_item_background) + if (background != null) { + val tint = AppCompatResources.getColorStateList( + context, R.color.navigation_item_background_tint + ) + background = DrawableCompat.wrap(background.mutate()) + background.setTintList(tint) + } + return background +} \ No newline at end of file diff --git a/app/src/main/res/color/divider.xml b/app/src/main/res/color/divider.xml new file mode 100644 index 000000000..1915480d2 --- /dev/null +++ b/app/src/main/res/color/divider.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/color/navigation_item_background_tint.xml b/app/src/main/res/color/navigation_item_background_tint.xml new file mode 100644 index 000000000..06c5673d1 --- /dev/null +++ b/app/src/main/res/color/navigation_item_background_tint.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_overlay.xml b/app/src/main/res/color/selector_overlay.xml new file mode 100644 index 000000000..c99447ac2 --- /dev/null +++ b/app/src/main/res/color/selector_overlay.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/totoro.webp b/app/src/main/res/drawable-hdpi/totoro.webp new file mode 100644 index 0000000000000000000000000000000000000000..0430efcfc8fe16ad55cbeaaef597fb47776f2629 GIT binary patch literal 1654 zcmV-+28sDnNk&F)1^@t8MM6+kP&iCt1^@srN5ByfFXkYUWLN3^WDBUpBr*^X-~Ip| z;5L%vNQyfCufP8up8jmoaU@BOlD&6`KmNOg6TGx$+l^$qZDT+-6G-P=y7&FB&B$RGAb-a?}V z|2#BOM8x6>MF>J|K+;D7;Q$)J5aSR_4XqFqs;;DuBBYXyD}a(B96%WVK%E5?y}3ce zQ6Pa41BlTAE`SH2f&t z=fY|5AlEdd^x8J{+3Og`@k$G;GHxDxV{g^`J-hSYK3AmQ|JUWi;BhdY4}1J<$(w(^ z&&|~pcPA()TH9twlA>yC+qONk-W}U*yL+m#8e`iwTVva-svGN!_%kB+2J-tOqW=)w zwvi;+DIM2z@`R=2u=MoL2n|{`*zT;kuSI2?q|_SMc3?9A>TwQy^?zl0ER- z{i7}|FQG`R1QHG4vv*0f9GR?wOpeGTyM)iKWyS4VNwz9StT zW**Z{ql8yA_v1lk<~wnL15_$|0jp(%LtkfCF*8RSwO_}wq>*MzloteWx6?XR{|TV^ zgQibZu(GTx!oZ=Cc~J%Uq6BhOm6b3qDiqQD$;Rj4(+hd_d4o&wvD`@+H=2>+lm{-y z2AQes#V29z)&hAj06ToL^kQ2VV2tBoS>ES!oD0+T zV(H1Sx0=;Cyy;xGZ{0mfc~#T1gH_%%-M@AFXA^Us^;YO6GjpsYmA$<-$!j%Ws)LyO zq{CW|@4Ls*bt$xgw)7QxPQLuN*74DqM>;2jzQ1Q{aK`F#5LgYIY{N1!)B9&PUjA@- z_~l{mAI&t=1`osw-Nw11A=UM4HUFUMpc`Y%_r=PVs|;NIvzff|(DJ zBJs4ynIbSlCaDPN=(&s(dk-v{5SLIsjixhNE;{iE*MaxS`ajpvahWI$NM^X0ok9X1 zXjvfC$sJgFQb76ggpQOuai73z{V1~IMa7iY}{pqbnr42X+{B14^9FqxxMsKyQ`+pR)_SUZk znaH;`qV@?;|SLB=5;l=sP7i7c7O-T(Qf38?*V6W&rs=AOsvrn(Na zQO@H^jS#JXTOUkC)d&7VN9|-Hxn7xttXKR-N9t5d_0vqV1yMf(tzeKz{(z=t4oHfA1sCgg{J3sKWV z&%-Cj2kxhF^G36!Id%>R{1Sl1_5){~+rGQHup(0PP=IZuqOf}R_H(lac4*Wk1#=HH AApigX literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/totoro.webp b/app/src/main/res/drawable-mdpi/totoro.webp new file mode 100644 index 0000000000000000000000000000000000000000..bfc0242b21c4b449ff341df27b22a3a95e19f897 GIT binary patch literal 1250 zcmV<81ReWQNk&H61ONb6MM6+kP&iD@1ONapFTe{BKjkWtBw5LR?-vEA#UwHia8AEV zgri2194V^j{QvkfTmZHlN0Q_y*?Wh6;=fxs!Aomsx3%rIjlpOUrcF??n2~j>gY?qrkj@ui@WN~%HHpN4 z8xgDh0TI9|P@n>+Lx4vh1_mAu0fbakAk?9P6mmmQi1-L8e@JL4AcZ2N$|*;Gfglxv zNCA#&;@TumLlHwnBF!IKcBf3Nz_|tfMyy0A3f3$Z3Idv8kdcc5JD^3QBVL*o8!?3i zfnYCe2h5@G55Vk8u*U~$ZFypi+KuE);Y)MP%$+12uG{sT`9e?2iUIVPOO^#%Df|C(L?x%#n770Uobo{Sh{5aAy7#8`{v;A;B~zz7z~C^qKcLnby7{V?ix?y9~C zhi1DOP++CGkbk908(U+vH@S*0k&5J!n%=^4zn?y$PcLo&oEN=eUd@8KP6xJZ` zI78Di_xV|o0q4Q~=*Q#Dtfo*8B`v$jcY)Gn4vny;U$zN@M9Z4Em+v>1>Aw(BAekSu zIka*#z4GCRc}VG_p+ka|WLG=3|7Yrhq}eTsv#7iuFdHfrHZIO4JxKZ4uOf6LQgdQY z3sWfbKCH?`%Xz9XM?%WC;mcXfvrwtYDouD6S#L=9=ghS#l`kqTAX^nt7@_Mkj$RvO zQys^)UVR*IEG``HkiBg5Q4_h6rGL*>Nv*w@WKtwExf6Qs*9(Sfca8-@uMgyOAp6Us zP+)9ltzf7AzP4s}2&1dtO$BeIiI!pLlAU^24FKMbSsii0j_ckot~%7n6f)md(Ocf^ zNLj7@F6{TqTyVdzUZkm1oV`CdcTjjf>vix8gvuB5yM?98K0jHv-3V*%o_`@USho(L zi!Qj=g*D8Y35O=uh#s!H7639eHN>}55*R<`l}EQzAOCVMUYaOn);l&HDB&9%Qgd5) zM$XfJpVvS5+{)UT0Bb;>*JJy?r;hL}H>ugjfQBp1G z3@9I0v8J5?*)J9gnJ*c7k<9>`pkiqo{sV1R#aa&k;tvye)yi3`Vnrpuhn(@pX>mxU zQL&KEz!!S&`6)}{LV1fS=nDYeC#8NMsZS0lO9%m>5B(tZp)Lw0`BFlk>&skow*o-r zq#D`*K)Od7Deln@P0=$U#STXAsiqY7wCb6T;iCZjMrzE~Bp*d|r&92w%3(St<#5Fv zUq!)vOp=uIiX%N2^al;3ckI&Ly^Y9 MzlT5ETLO(W04@ewtN;K2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-w600dp/tab_indicator.xml b/app/src/main/res/drawable-w600dp/tab_indicator.xml new file mode 100644 index 000000000..e2b1ad5c9 --- /dev/null +++ b/app/src/main/res/drawable-w600dp/tab_indicator.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/totoro.webp b/app/src/main/res/drawable-xhdpi/totoro.webp new file mode 100644 index 0000000000000000000000000000000000000000..2b0dde6ca33b3eff8f375eac39984bc4fe8849a0 GIT binary patch literal 2524 zcmV<22_yDWNk&H02><|BMM6+kP&iD-2><{uU%(d-7Y8?zB-u*#|DQw0VXQ?(yJ0$J z!3f8VBq>r~ec12jxSY$QpMbk)n-hgO{3=N11O zz*IB0RIRXW+a`M_?HO26JOlS@|J7{FUz2)q85P&q5a7Yf_`yg5v54iwA;kX!{ub;c z=uA3s2;;eA;}8<2^$;cCutbR*0E8fQDlnFTG7`iw)x$|HzyS^q0Yg_nT6i_6fdQ&QjjHm7S1L6I zfE7q3>dS`=R_2Qa;r@t>0u$6_lhur%8O`#QZl?9inn4%S zjn_l#W)5A(`rnaM0{}4C#>+4MQb0li4QLJ1z5z9yb#8q{L*~@AjaQRoUZVWnsoMCD zBo4XTkFKA8IrHJe9NWj=XLx_A-f{^S!_#kv^FtnYy*T03O$rO!wkb}t&898{tp;BB z>hA7B-QC^Y-Q8W0x)-RC7Lsh;-TS42BtM0neRgK{59~7&(SHYSBPmkV^)Y0q2kr-i z-@&x%@>7U~C-kYBObZhG{bz6`I~*zWmnrIhl~S!7q{YZYSTQbkl5@`Q8d4N_YKDRCQUd|pNPHIUg>6!%!6>`NB~D^#8xknfUC}f zaAZ}8N1?4gE+O|Xon^9eK%9NVR&N0d5S5r@a-sxrwh}v?|6Uhhm5(;s-CTtFe?=Sp zrJo3?FKDB$HV`3nh>gDVNQ4|DHoB8vgd7xH6wUqPYY=IKg8_;h;~rOHC{8R^G!X<2 znlOW3f4GrfN18Pl(C$QO7eUPO(BLoETscwEGMcVg{LyD(@MIB0SLs>p1~h24k~{uA zG)NyL4!=i&ipH*>$2Gof(ZAvepy7YZ-6hpI#%rOv08p9 z7t>5qmrm_tf7KjlQZ-sifWwF4tEc{-Zn-HslTEt7#x#XO((a~Ki$4@*RmL-2=C;_I zjfg6qjlI)t15b5gV@jZK_xAAW@>vF}vR_CkWlYW8=kvO;q08Li2HuEVMyS^L=i1ev z#!0ViR%!bloAPKCo!M=_qr=Wrnf@mKwTAU&t*-g-AZZ7FaRAQ!tMdqwK1VM z<3Q4G&$O=L0d69|XWv?&<_yZDa0+iPn%L%PWuJpm&0Kt&-p+g(VD+K5XGLhc1*vxr zmFx1)k9igY(Kx-voTHYzxA%B0Ft-Z`R}#z_OYclGhI#@P{1U5nrm|ZwEf&T}a`7t- z#;h{OK>3xDqcb^orejbYvuMY;+)>yUg!HX7DnFuAv}Cku6G0I3PdXq=<3e6tgjb?7 z@(hyK9E(%u$wfrWn*pO-rgcvo9DrkM3%k`*Yv;j*#PUF+B!4P5Hn~0e4&f?b? z=7o0-K~!}ike0vrw5TRTxHgP<=Y>Uh)_uP>-eSrQytqR!oC1N=T*Rl9&@g&h^rnK% zU=e(YJE_&sw`mIQNn;8K<~;>4yes)M3gC;qKoCDa0;6LoQAtjlNNQ zg33SCC`5PgxoTx0ys~c~oEZ ztAHE7nqgA()#iqIGQOE?t>o(_r114N#zN5zO`_M>^$|ep zRrKjgum3?v7p&z#O#hx-BB=EOkkZ+;9zfef5cl6NQaECrlNn-FD0}?6lHnp%+)ofU z0Z8lFwF^L-kBDPrn+2CP*7fUkHkMt$@LdySeUp>uaUL$EdlLD;}Q3FzDi(jnz_R>sN z*}&+d3v?eu#L8xhm)}@b5}1=88yMB3K%>D4Y$;R(tCpIxs_kMwiCR+61ZMjKM{&!p zx0t&&hrUCnaP$`mwAB~A`p&9sD7DbzTA;1QwUfcBpU{e(%1eG-m>sLUG~g$z3ZT3} z<<)p8%+5lB+6w?K5Jasrz!9Z* zq(PY@whTc$m#7P{I){kmpxXu-j}Y8;Q0rI1r1mw--f>>=osogBgQwO5d5><^K!kp3U$&?|pOD&@4Fmi48qUB!Z$Rov zdB%lrU%3bo^Cp2PZ~pKKSAYirrhRc+_I+_DYG1q-Tr+tE{7o4KUpDRQ|E^^`NXto~ z)<1LLL&$+)e&KxhKa!Z0R6Q?aIhRmFso5*IG6tlnG*3X_UyEZ~?`Zm5Wr4IfS9w0> zJlnZSr@tbEH!aqnIfK4XG=;aX%xu%ZI2UTrNnT_<7xLXCV!YO(F1XIMCR+6e?^+_r z;pBi7@4J~S=W_C1N>i)y2a>}?l*_!YJ#E69zjlW;?!fUF;zCf0<$fb>VKMP^@p%6CKVC!GYSRN@Bx7muO*lc?FSC;Y=89 z@R2?h8GbP~NjcUmQ5E|fL0jzlvU*0>W^dr!5TBYQQ{m-s&YPxPCa?bDB6e9ux>c!F mr$%;fQahnSuD`}%7eRSgIk^=QYQOA0^3iTsIo zaMfDmiXPjlz>MMvypQ9zS(tw`m9bv|EHi|0trU`Q0Q;0=fI@;8254YD5+b4M0DwRg zSQQvXfkUbS(!s9paK8@1OQY3001BW0H^=}009650f25%U>F5P!GwPpM$s^&!2t{qO^6I*sTLrF z1r~|KXuV;HC5)w{i#Axac@SUpQ86SuAOePIKmn?OLyqN0LO7bz2o?@bSO+TyLj%}{ zAlr!a0Yl6iK;4yuVHi%wnGUCmkN39A+}|pV1FdEb!V-6Vr3tEhCc>3?w4QU6~x zYxE~ew$(15IDYjaz3y${=29+v&YdA7Tzczv@EW%r z;Kl&59daAkX4;t#%k#<33_Q=6#vSJUChOOL98NR5t~Ym_e^^dH+;MM@tITjl<11tY znZrMHzv0JKuZY63|E1{yf&hqyHj{JF3hs0Z)Xx89;m3HOX++pm^ zBkXdfJipT=R+i<6u-vY4-{Z50=#7MWZzob-Q*4PPEo64CEh|DKWu%X=H zn<)HFRu?8*51<#=@2{EMse0JSbV5XLlEcUm(a{^<{+ub}H__3)>xO1Ip*P97*@-Ei zAk&%SB6M0Rqg-Syo96^*c1);;c=O`vyI(JG>#7Xj31-NM4ZZvH#apOvwdSd?P>GY{ zGJbVZ5`m?*sD|1nzAA!HC&NJ(LH2B6AV3F3XMz@em0+HjJRB0MVRyce6Gb*NTR%cX zh1KkW803S4=#@huRP?{-{A^A{TbRQwL`1WyiT)1=IuyO~&IpL2U4AXUdb~>^x1M!! z)++_(a`i{;A*~M*dsj-|BQFLdS-EQvoHYb5w z^tYW~Kgx+{xfic<#t3gwbQq$T*2ZSmcGTLU}}>$rg5h$)VTU($bqtvZD%p?N%_#1 zSJgl~IyJ1Cp1}-c>Q??c&HJVuC}&DneEmy-m*v0vbtS4_ZR007-jwq}P#3kG;6=!E zz7p~xEYZk#h1`#f8}!ZY6NgS#^8-9V-wYTRagRPV^tl*w$(%T-@1G1@(!b3_)1}x& zeOi&LX+Ixl($55Gu25@jN=!F$o5h){@fN-t<|SRWarv=_uQAszuP=~CTXb0@&3yYT zQ}XN^ByEu<|B&pI>jraZoa+DMm!n_I4WUn?-}xcv=NC2GRe-lr&V2Oi9fW%>KXWF)r0lIc|rE=8`hCXi8G^8YY-NgHC zFldx7g}&DyFY!~g#0qq&TxqX5=_`2kCY93 z0VW1XpjxUuDq$3gw}-gG*hhdkQbISy;_0xWE23i|t~4GHoy&lw#nt+U;9F7nXD(OV zuP(psU0sgpNXF+}tqnk0pS zN(+{npqG_890s*UVGojMF8eOdY~$b0HziOYz?Pu!VU->xXnLjKkBEd=PlGu&dioL~ zX9Ynzgx09E0MrtN&yvI+8F-@vdQX6@Knqk#fh;K(+#Qlp0=pt3^tGUTDP*QXB@sw_ zW{uA$7Y;V2WEW^k+=5*okrH(tCSbU>pr$Ehqb0d-Hm9IrvgHRU;XQ)d0f5%1Gas@5 ztx0o8YHX9idgX9oPH!}N`ci0Ck_A72OjhQ|Jjgb|UnE@8`z-dE>3}FJ${j|LJ59a! zEZ#p;K!IQzWZn@~PKF?d1Z^$LHkUC(JowP(4x~@sbPy4-Z0%%AS3xNxc2br71e}D{ zvK&s!HZc2m(LAaHX;p1&D^O(N7!@GxYR zDz5;-bp(TGodLmZ@&be!5AuP6ePO^TMJfvb`BwxlM-2u(D-S_vL|+zM$p`oqYMdot zw8~OeV0UW^CX4=X?cf%?X_tT1QWXWzSvB@UhNJasq6ZCzn0(J1PSNd0EZgKYwj8-9 z%4yCw$qdnG!s(a=M?j9Nu>;aV(3(@^K?5JiF*y;LnY|45y)VyiibjxWaw0NwdKGvG*!PqR1ENSC=gB5FA&85AvNX!=$hbT{YDE)2sl@bbjZJg zFZ3HGI0i{mV-lo=ptXLh1l=K@t1$vHTyQ|YdV&#G- z`ByCXvTmO->$!Z$YBlbKgzE?f>efcEFATX?jlUtZLC{Y(6g*7OztuPonj|R8(W}@O zECy9m!$^ftsRi5W)fKH52%2K3fscc>3I5V65Hto2V2YjrEf6Hl)@fEC_yAN(mBR&) z@M#O)73dTE#jJi9QgB$6_$&x@5yYqHgK7a3_a0?K%6ySGy1Soujz|B;;oC7jT z1(mPr@M<9NO;GV=mGDatJr}oN$sG02XV&--0>8tQJ_e&L0{k;qHUAt6w-UUbT8Xi+ zjB^647gH;Aj0ykDfQ?D0g$-adN`yN?DxuHBqX>Qj6+e;zqrnLQ_AgW??*#7CPh|+&KF7FJb zarp~;z3_C7JR)=Apc{X}O8T~$XmS^eYct)yal;xJH{ibACrVFN^Mg{P`v#1QaF@3; z{hxP9`RCCCE0%c)23~+CQ@vx!QRa>P3cUjRGp}4{dV~2OdZzJ{H?&6Yz=~hI;FkbB zb(nr%7|dG}9hSP2KHDdEFi0@(tub(EUPRXT3WmXWS$^J}_BEJCCocW5Yx$@`37=Fj zL=2viQ1EisT{SKxn8(NK*6iP_`JkaszC2|~R$&2UEH7h1Kggtl!mK4zUVie0LCt&h Kt64YBGz0*Se!AiS literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/totoro.webp b/app/src/main/res/drawable-xxxhdpi/totoro.webp new file mode 100644 index 0000000000000000000000000000000000000000..4193ce397dbdc2b2cc3e3cc252c02694bbd02f7d GIT binary patch literal 5222 zcmV-s6q)N%Nk&Fq6aWBMMM6+kP&iCc6aWA(zrZgL@8>p>BuU9$r1KhUQPIEcm<1!? zMv`PJ%3l8H`0o{FoO&ANIFcks$=*YY7yi4&69X_n!!}&owpBHpgR^n3A@cuqej_z~ zD&O~JJlR`e+cwCU;SCIE+zFrS;#F48UwcJ?O%*7)F?ett-)M;8LG&jM!T$&R7R)5* zOgeE0&AH?+jt(t^0;517KqFMB#2ies6lu=YF%q;XL%a%t0g_+|0m)!#K&nlEI0IB6 zfMAOttTsLiib+Zalf++$PJ^Z@DrFj7m|bvLVuCRy0DvXNQ#X<<5eOu5;-*Du0>AhL zq4_3MYJklB7zLyiCN+4T7-@tZfCLtrI96CS9Tr$v9^u2PVpKv<8mp+VECz^_RcHct zZjLt)#Ak4zItt9trWzCo3Y2W)zsK16YiMB_xzx&Mj!7Dyy?nL%&0Fx1!n)=+k(N|( z)5}s5>c6u2i1Kx&_xZ5b{q%c1Rez?}`MdkWCR}q}|F8G!rFFl^`yt=E9?tP}lffJR z|EiVv|F)q7s3MgJ8v`+PID zENjz?+}^Z)t?E6qKleoc4U~Tnh~2QCN>hLY3Z3V@q! zz>>dKAk%Uw4ty%Cmaza@!W}1FLZ;>45G)0u3YnJvSQ73d)A9wDgg23CS%xKHDKagq zup~T=OiO1h3Db~iX@eyp7MYe9ED0@;X_2Jbv8+&JR%$Y_BxEXNPHLaS((t!QGnm&S zu{1;~WJZeC=vW$bRfJ5)E9qDwY_A}hDQ`09YBPY%wJ5{b_0Y-9IXx3XCN~GTsB3`0 znq3{#M2-UGp?G_YBd6O(4Ls;J&hCbh=Z#7z^v%QUWON+%Z{a`F06sIE^r$=1(du>1U}mQDsIK?B4=C}2UZsR|`Pj4B6i@MQ zHQnjK&3t{ZyH0UOgQz3>x|h6VHq3})q`UT2NGGtPm%Gy7*JlP&b-D}xWfQwWbbgyY ztE|`g(C7s1AuEnB%!c2)gr8S3z$yLxdZEhCHu)Lo*h`RLl+n$y^ma*==%G{tmpqNs zY=PN`yGZDH{|u4xyM`+JwKFbx9KvrkGXMdm{Wh|zfrvheqWQ;~AL0Tq97;~S^11JS z*qCZ)wRYqJNM>iI_=8*v2gITWryF>LCQvNzlwG`Zd^QHtnAFpnRbISwbC!`+dN8Ee z4m?BLXup3#|){Onu;j)kc2*ur+)#0wb~=Gi7+UteTR7m&W`RAIuQqWW!q( zT+nIccXcvc(8}ue*Ql2YuFBLPc3p0~G#ngcuwmk;G5l;8RwDnxABwQ0jW0@>%pANc z8`{?DW8~KZd^#*;?;zVGr9ZP8mt* zxw+1|HHh_&UUb{yIeNX#=3A7v+B2{=LTVzCovMx)hW$OFO7~Xo?fHPc8eLc0>t35~ zevkO&?RV_J06JD7$$1@I0r|!C^mST|d$cE>7Mlke`Q9YDDXF~OkH(_2tZrp??8xEvbpI;T6Wfcf0#xJzhv1LV$H>gu;d!+G-9`H@? z0B*u*${-1HZ+v~@KyNFcFnrv-5j54_lRAuiX%e^Sjql>sQRipej#}#!J#Wu+4`U_b z4y{g}{N`1%EZu{7vW{BGcq^{Ldk9(Q)i-EYNeJT36lx@!whe2dAxTw!2 zEF3)`N?8)5Hyy);r8T`4M`vQwlPS0v@0*{iV^gEI|66o3zOH!Wv~+A*RF_l5GIU+F z^SffFnDAFBY{RaB@=U3^eBk}I*krVQe}Ac56T>DtwqjlzR4JFP?#T zPmJdANNkj7O8giO_m24E9Oss`tI>82vj);fA!%sO9%8%e0!vK-xw8OQ&!Z*_{DD=^eHVjmRmlBxd4$RiCMRXHM-eDjoy{-D-)+@ z8$Q)l_Vn#v`5Fn8 zrzcUJU$G@*yI)2Ua6XGAu9REU2d4T%JEzu}O8JPIjrO>e!lIbvizHw?OIRuWXvZkO z&M_xzSq~@D3PG(O(^Gu{63-o$T&07$(Dl;N2+S4r=Ov1=)~_XlUyKAxWA`z{6n%*H zelwYSze?pbph#rxQQs}Ec!EULd7bP^?0SzZ3 z{x+#r^_;+*lzidA%E0d)F`)E0L7V5{|?I#X^NHt2+sdB=v3bXM>hLn9UaTFtK8xWn8Q+p^lcv!A@#6x! zuXlkH%WOkQuh#-GnAFw7E9ntb^kF$n&}hWDmWIyCEtQVL*PT04e zu%qg$h;m5#KSP)ERcIWq!99atCeBR7W3bC}rLe~PU|72ECo;y1@eD-)c$oDv+*6-H z^iQFW#lwSBNyIWhldtF72k3-LPm^Aj8edOd`~Z;y_ku1xO6gSuXDqQqEk= zz-xqC#qc%$*6QJ8+v-&o(+=dX--B7bfCRA{6ha7D3y)@Mj>Nj%rS3s(Q*WV?N4nKO z|1u3dW{^47ieQk{Y9xjl*fp@7kn*H`_K#8lMsj-I{t|U|9i>uL>k>=U0pkM&L@8Qb zlt(tt|G*G}1!eFXfg%VX*Jvk^EwZ3_y&6HFf!l$4B))RGb`BTLAoemTstG;~bnt{2Z&vGwMD$mX$M4lr0kzUCl&3BLp zv`0YUE+(S85>QtAig|u)KyT(o4NK+ z&?7jUkXp{Lo{D{bvH};uC3ZZXP#6WV)t3oK|9P$g>0fnsa?PRN%3f+nxstfvw6caT z(f((N1J7|03ZMtK7qljJ}hxye3Uy)7pK5P84 zRu3mj&ar9OL5wqdghO=a-bUQH0y@?@!s$jhChs!h#~^H3y2?9Q|IFfYm)UL!``VMq zZawj!A4j<9mp=Hzh?j`4=0bEg@E=15?rHN3;WRq1K9tiX>cuV;C)>JPCF%f|v6Hy{ zB`T4c3F$JnNlDTMY*~S0h_If5$coq-XCSB@^0m8?_PkrvQ|*?)h+?MQ;*^0^DF%KI z{2gvZtgR;zGN`|haTsxDBt+gKQiyfWU`ctJsCkk=P9qR^K8LtxoOf69{0>XX4x;UT z9`uykgSfK=;vPt(D~onxNl`>YC;^nt+KPAs%NR%9b5|;zh9zYd(Gs-sh*LV^ z4&-L0dK1+47jT2Po4nl;h`0k9@ice4F${6X4w5%$x621(yWMgi?sNdj8}ysxgR$Ra zB_r<40Ljz)trEi6Z?$G1?mPjDjFvZ$61Kd7^(5lX^B{Shs5o77u#|LU%G+2y5O-dN zg+`9fEXPtp&T$+AlRAmGb2t+sPsv;M$5KKL$vg>i${c{WE7&nm&)^QW-u{(%69?yb`AA@l# z$)Jo|Rd0vI`?rV*@UOzozrnbnTx2}MHf|U=8VR5QECX*aZYvj|dEcYcB_5X;9?k---pW$0A+O_m)+d0}JveatNmS5iAXg{QW@S=QhHoj#n)LJ~Juv zHVdU`J<{fFu7+vp--_Ey>{Vd@+BSsr8XbhCfHrS-gbn5uI2s89+&^tknYX-Cipodr zWS+O|5V`UI)`hXC^Tv-zv71I)MFipcGz}=W0tuusbS64&-u|uTSU8c2jN!-zpjfKV zt&g?ufuZP+QpEZSzmMumnX>V#4mA5-?CR+o;AKy}Wn8!N7TD}1B$SIkK%l=p9 z0Q+Wq%{(L&^Ij8!eKYd2SR7Z#vv;{qE0*9gelIrKx8(mbVxUW1^#IjvpW(hG3z<4D zn|zQo)Qw~Lk!7tn-Z!RkuU&JEcW77Ha~Jn&%y-|I)E;$69!lBL+hlgrt9rB;)Q}#xN~Cdx;mCck7x3%6k!*|z!^{NbAK{cBs!GV`&`u5; zf~2E-JbSE`9=)&+kA3s{EaI|n-UJz63a%NS?U#c#J;Azf;S-$pE$rG1$;c62vn}-K zMc{oSPkqa6-$+LSl9IYN`Q>QzkoLbk^?a|9iSGM$CXVd&{4TEhb_RVZ>+A42zr|d! z`3FyFzByWLwx)33H}zY2UwWfzy(ujf`1FX|$iy@qIP{CZe(#psA`ozH zTAe0ZAg-iMfWHlFksJT}gcr^XY>NH^a5cMF+$5pFnK$yc&0m|mAtgM>FI5j)dCf%; gQ!B&tRKK9`lnu#i=Wok@<4l8uCdHSV$ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml new file mode 100644 index 000000000..695880601 --- /dev/null +++ b/app/src/main/res/drawable/divider.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gradient.xml b/app/src/main/res/drawable/gradient.xml new file mode 100644 index 000000000..f90e2ebb4 --- /dev/null +++ b/app/src/main/res/drawable/gradient.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 000000000..2a31b2ef3 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_book_page.xml b/app/src/main/res/drawable/ic_book_page.xml index e53b1da5c..375e7b424 100644 --- a/app/src/main/res/drawable/ic_book_page.xml +++ b/app/src/main/res/drawable/ic_book_page.xml @@ -1,11 +1,10 @@ - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/ic_clear.xml b/app/src/main/res/drawable/ic_clear.xml new file mode 100644 index 000000000..938bb9922 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_eye.xml b/app/src/main/res/drawable/ic_eye.xml index 52e210215..86a1ee448 100644 --- a/app/src/main/res/drawable/ic_eye.xml +++ b/app/src/main/res/drawable/ic_eye.xml @@ -1,12 +1,10 @@ - - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/ic_eye_off.xml b/app/src/main/res/drawable/ic_eye_off.xml index e558a36c4..33f8b6967 100644 --- a/app/src/main/res/drawable/ic_eye_off.xml +++ b/app/src/main/res/drawable/ic_eye_off.xml @@ -1,13 +1,10 @@ - - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml index 220e1b503..0781a7c4a 100644 --- a/app/src/main/res/drawable/ic_lock.xml +++ b/app/src/main/res/drawable/ic_lock.xml @@ -2,7 +2,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" - android:tint="#FFFFFF" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> + + diff --git a/app/src/main/res/drawable/ic_mic_none.xml b/app/src/main/res/drawable/ic_mic_none.xml new file mode 100644 index 000000000..2810bc9a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_mic_none.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml new file mode 100644 index 000000000..1b040f39f --- /dev/null +++ b/app/src/main/res/drawable/ic_person.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml index 753697107..0d7a0a664 100644 --- a/app/src/main/res/drawable/ic_settings.xml +++ b/app/src/main/res/drawable/ic_settings.xml @@ -1,11 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml index 8ed8e7e87..6709b5625 100644 --- a/app/src/main/res/drawable/ic_share.xml +++ b/app/src/main/res/drawable/ic_share.xml @@ -1,5 +1,11 @@ - - + + diff --git a/app/src/main/res/drawable/ic_star.xml b/app/src/main/res/drawable/ic_star.xml new file mode 100644 index 000000000..0850e55fd --- /dev/null +++ b/app/src/main/res/drawable/ic_star.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_manga_info.xml b/app/src/main/res/drawable/ic_star_manga_info.xml new file mode 100644 index 000000000..e4f9e867d --- /dev/null +++ b/app/src/main/res/drawable/ic_star_manga_info.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_web.xml b/app/src/main/res/drawable/ic_web.xml new file mode 100644 index 000000000..3f70646ba --- /dev/null +++ b/app/src/main/res/drawable/ic_web.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/list_selector.xml b/app/src/main/res/drawable/list_selector.xml new file mode 100644 index 000000000..14508c152 --- /dev/null +++ b/app/src/main/res/drawable/list_selector.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/navigation_item_background.xml b/app/src/main/res/drawable/navigation_item_background.xml new file mode 100644 index 000000000..8c635e7a3 --- /dev/null +++ b/app/src/main/res/drawable/navigation_item_background.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_indicator.xml b/app/src/main/res/drawable/tab_indicator.xml index 85211f67c..7f0ba0c38 100644 --- a/app/src/main/res/drawable/tab_indicator.xml +++ b/app/src/main/res/drawable/tab_indicator.xml @@ -4,6 +4,6 @@ android:shape="rectangle"> + android:topRightRadius="3dp"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/tabs_background.xml b/app/src/main/res/drawable/tabs_background.xml index 6c1544f5f..d9df3de10 100644 --- a/app/src/main/res/drawable/tabs_background.xml +++ b/app/src/main/res/drawable/tabs_background.xml @@ -14,7 +14,7 @@ + android:color="@color/list_divider" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/thumb.xml b/app/src/main/res/drawable/thumb.xml index 71e0a8113..8d1b235a1 100644 --- a/app/src/main/res/drawable/thumb.xml +++ b/app/src/main/res/drawable/thumb.xml @@ -9,6 +9,6 @@ android:paddingLeft="22dp" android:paddingRight="22dp" /> - + \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp-land/fragment_details.xml b/app/src/main/res/layout-w600dp-land/fragment_details.xml new file mode 100644 index 000000000..c45f7b1c8 --- /dev/null +++ b/app/src/main/res/layout-w600dp-land/fragment_details.xml @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp-port/fragment_details.xml b/app/src/main/res/layout-w600dp-port/fragment_details.xml new file mode 100644 index 000000000..69630cb0a --- /dev/null +++ b/app/src/main/res/layout-w600dp-port/fragment_details.xml @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp/activity_details.xml b/app/src/main/res/layout-w600dp/activity_details.xml index e5fc1b45a..af351d3aa 100644 --- a/app/src/main/res/layout-w600dp/activity_details.xml +++ b/app/src/main/res/layout-w600dp/activity_details.xml @@ -9,29 +9,27 @@ + app:elevation="0dp"> + app:layout_scrollFlags="scroll|enterAlways"> + app:tabGravity="center" + app:tabMode="fixed" /> diff --git a/app/src/main/res/layout-w600dp/activity_settings.xml b/app/src/main/res/layout-w600dp/activity_settings.xml index 8d031bc79..2a11422ce 100644 --- a/app/src/main/res/layout-w600dp/activity_settings.xml +++ b/app/src/main/res/layout-w600dp/activity_settings.xml @@ -13,15 +13,15 @@ app:layout_constraintTop_toTopOf="parent" android:layout_width="0dp" android:layout_height="wrap_content" - android:background="?colorPrimary" - android:theme="@style/AppToolbarTheme"> + app:elevation="0dp" + style="@style/Widget.Kotatsu.AppBar"> + style="@style/Widget.Kotatsu.Toolbar" /> diff --git a/app/src/main/res/layout-w600dp/fragment_details.xml b/app/src/main/res/layout-w600dp/fragment_details.xml deleted file mode 100644 index b224f5bc1..000000000 --- a/app/src/main/res/layout-w600dp/fragment_details.xml +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_browser.xml b/app/src/main/res/layout/activity_browser.xml index 0c2add382..36b76f907 100644 --- a/app/src/main/res/layout/activity_browser.xml +++ b/app/src/main/res/layout/activity_browser.xml @@ -8,20 +8,20 @@ + app:layout_scrollFlags="scroll|enterAlways" /> diff --git a/app/src/main/res/layout/activity_categories.xml b/app/src/main/res/layout/activity_categories.xml index 57166010a..aa9a032e0 100644 --- a/app/src/main/res/layout/activity_categories.xml +++ b/app/src/main/res/layout/activity_categories.xml @@ -2,22 +2,22 @@ + android:layout_height="match_parent"> + app:elevation="0dp"> + android:layout_height="wrap_content" /> @@ -25,9 +25,9 @@ android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipToPadding="false" android:orientation="vertical" android:scrollbars="vertical" - android:clipToPadding="false" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> @@ -36,13 +36,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:gravity="center" - android:visibility="gone" - tools:visibility="visible" android:layout_margin="20dp" + android:gravity="center" + android:text="@string/text_categories_holder" android:textAppearance="?android:textAppearanceMedium" android:textColor="?android:textColorSecondary" - android:text="@string/text_categories_holder"/> + android:visibility="gone" + tools:visibility="visible" /> - + app:elevation="0dp"> + app:layout_scrollFlags="scroll|enterAlways" /> + app:tabGravity="fill" + app:tabMaxWidth="0dp" + app:tabMode="fixed" /> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 62aee8a5f..e9506d317 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,8 +6,7 @@ android:id="@+id/drawer" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".main.ui.MainActivity" - tools:openDrawer="start"> + tools:context=".main.ui.MainActivity"> + app:elevation="0dp"> @@ -54,7 +54,7 @@ + app:popupTheme="@style/ThemeOverlay.Kotatsu" /> @@ -50,7 +50,7 @@ android:layout_gravity="bottom" android:background="@color/dim" android:elevation="0dp" - android:theme="@style/AppToolbarTheme" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:elevation="0dp" tools:visibility="gone"> @@ -59,7 +59,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="top" - app:popupTheme="@style/AppPopupTheme" /> + app:popupTheme="@style/ThemeOverlay.Kotatsu" /> diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml index a12c6daf6..7a7a4ab76 100644 --- a/app/src/main/res/layout/activity_search.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -7,17 +7,17 @@ + app:elevation="0dp"> + app:layout_scrollFlags="scroll|enterAlways"> + app:elevation="0dp"> + app:layout_scrollFlags="scroll|enterAlways" /> diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 2fa97a72c..5c180d794 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -6,19 +6,19 @@ android:layout_height="match_parent" android:orientation="vertical"> - + app:elevation="0dp"> + app:layout_scrollFlags="scroll|enterAlways" /> diff --git a/app/src/main/res/layout/activity_settings_simple.xml b/app/src/main/res/layout/activity_settings_simple.xml index 4f6c56541..dd60a6ed2 100644 --- a/app/src/main/res/layout/activity_settings_simple.xml +++ b/app/src/main/res/layout/activity_settings_simple.xml @@ -7,17 +7,17 @@ + app:elevation="0dp"> + app:layout_scrollFlags="scroll|enterAlways" /> diff --git a/app/src/main/res/layout/dialog_input.xml b/app/src/main/res/layout/dialog_input.xml index 6c035d585..895bacb61 100644 --- a/app/src/main/res/layout/dialog_input.xml +++ b/app/src/main/res/layout/dialog_input.xml @@ -11,21 +11,16 @@ + android:layout_height="wrap_content"> diff --git a/app/src/main/res/layout/dialog_list_mode.xml b/app/src/main/res/layout/dialog_list_mode.xml index 01d5e426e..8fb04828e 100644 --- a/app/src/main/res/layout/dialog_list_mode.xml +++ b/app/src/main/res/layout/dialog_list_mode.xml @@ -18,7 +18,7 @@ + tools:listitem="@layout/item_branch" + tools:visibility="visible" /> + android:layout_height="match_parent" + android:scrollbars="vertical" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - - - - - - - - - - - - - - + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/guideline"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/buttons_layout" /> + + + tools:text="@tools:sample/lorem/random[25]" /> diff --git a/app/src/main/res/layout/fragment_reader_webtoon.xml b/app/src/main/res/layout/fragment_reader_webtoon.xml index ff906ec6e..bf60f0a5a 100644 --- a/app/src/main/res/layout/fragment_reader_webtoon.xml +++ b/app/src/main/res/layout/fragment_reader_webtoon.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/item_header.xml b/app/src/main/res/layout/item_header.xml index 7031727d6..27b1c2c37 100644 --- a/app/src/main/res/layout/item_header.xml +++ b/app/src/main/res/layout/item_header.xml @@ -5,10 +5,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical|start" - android:paddingStart="?android:listPreferredItemPaddingStart" - android:paddingEnd="?android:listPreferredItemPaddingEnd" - android:minHeight="@dimen/header_height" + android:paddingStart="4dp" + android:paddingEnd="4dp" android:singleLine="true" + android:textAllCaps="true" android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" android:textColor="?android:textColorSecondary" android:textStyle="bold" diff --git a/app/src/main/res/layout/item_manga_grid.xml b/app/src/main/res/layout/item_manga_grid.xml index 9727f3b88..6a168ea68 100644 --- a/app/src/main/res/layout/item_manga_grid.xml +++ b/app/src/main/res/layout/item_manga_grid.xml @@ -1,38 +1,50 @@ - + android:background="@drawable/list_selector"> - + - + android:layout_margin="4dp" + app:cardCornerRadius="4dp" + app:cardElevation="4dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + + + + android:lineSpacingExtra="-2dp" + android:maxLines="2" + android:padding="4dp" + android:textStyle="bold" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/card" + tools:text="Sample name" /> - + - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_manga_list.xml b/app/src/main/res/layout/item_manga_list.xml index f6d964cdc..6a12cd8a3 100644 --- a/app/src/main/res/layout/item_manga_list.xml +++ b/app/src/main/res/layout/item_manga_list.xml @@ -1,24 +1,37 @@ - + android:layout_height="wrap_content" + android:layout_margin="4dp" + app:cardCornerRadius="4dp" + app:cardElevation="4dp"> + + + + + android:maxLines="1" + android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" + tools:text="@tools:sample/lorem/random" /> + android:textColor="?android:textColorSecondary" + tools:text="@tools:sample/lorem/random" /> diff --git a/app/src/main/res/layout/item_manga_list_details.xml b/app/src/main/res/layout/item_manga_list_details.xml index 7257473f2..11ebedae3 100644 --- a/app/src/main/res/layout/item_manga_list_details.xml +++ b/app/src/main/res/layout/item_manga_list_details.xml @@ -1,96 +1,91 @@ - + android:background="@drawable/list_selector" + android:orientation="horizontal"> - + android:layout_height="wrap_content" + android:layout_margin="4dp" + app:cardElevation="4dp"> + + + + + android:textAppearance="@style/TextAppearance.Kotatsu.ToolbarTitle" + android:textSize="18sp" + tools:text="@tools:sample/lorem/random" /> - - - - + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:ellipsize="none" + android:gravity="center_vertical" + android:requiresFadingEdge="horizontal" + android:singleLine="true" + android:textSize="16sp" + tools:text="@tools:sample/lorem/random" /> + android:orientation="horizontal"> + android:singleLine="true" + tools:text="@tools:sample/lorem/random" /> + android:paddingStart="6dp" + app:drawableEndCompat="@drawable/ic_star" + tools:text="9.6" /> @@ -98,4 +93,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_page_webtoon.xml b/app/src/main/res/layout/item_page_webtoon.xml index f032e1819..32afcd4f4 100644 --- a/app/src/main/res/layout/item_page_webtoon.xml +++ b/app/src/main/res/layout/item_page_webtoon.xml @@ -1,18 +1,18 @@ - - + app:zoomEnabled="false" /> + app:drawableTopCompat="@drawable/ic_error_large" + tools:text="@tools:sample/lorem[6]" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_complete.xml b/app/src/main/res/layout/item_search_complete.xml index 6bb124b06..d0ecbdc80 100644 --- a/app/src/main/res/layout/item_search_complete.xml +++ b/app/src/main/res/layout/item_search_complete.xml @@ -12,6 +12,6 @@ android:paddingEnd="?listPreferredItemPaddingEnd" android:textAppearance="?textAppearanceListItemSmall" android:textColor="?android:textColorPrimary" - android:theme="@style/AppPopupTheme" - tools:text="@tools:sample/full_names" - app:drawableStartCompat="@drawable/ic_history" /> \ No newline at end of file + android:theme="@style/ThemeOverlay.Kotatsu" + app:drawableStartCompat="@drawable/ic_history" + tools:text="@tools:sample/full_names" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_tracklog.xml b/app/src/main/res/layout/item_tracklog.xml index d99dc5f14..38766f184 100644 --- a/app/src/main/res/layout/item_tracklog.xml +++ b/app/src/main/res/layout/item_tracklog.xml @@ -1,75 +1,83 @@ - + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:paddingEnd="16dp"> - + - + android:scaleType="centerCrop" + tools:src="@tools:sample/backgrounds/scenic" /> + + + + - - - - + android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" + tools:text="@tools:sample/lorem" /> + android:layout_gravity="center" + android:background="@drawable/badge" + android:paddingHorizontal="6dp" + android:paddingVertical="2dp" + android:textColor="@android:color/white" + android:textSize="12sp" + android:textStyle="bold" + tools:text="54" /> - + - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_search_view.xml b/app/src/main/res/layout/layout_search_view.xml new file mode 100644 index 000000000..7686f1813 --- /dev/null +++ b/app/src/main/res/layout/layout_search_view.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/navigation_header.xml b/app/src/main/res/layout/navigation_header.xml new file mode 100644 index 000000000..9c82c3288 --- /dev/null +++ b/app/src/main/res/layout/navigation_header.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 926f62a66..c846e8b74 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -194,7 +194,7 @@ Прайсці Ачысціць кукi Усе кукi выдалены - Праверка новых частак: %1$d з %2$d + Праверка новых частак: %1$d з %2$d Ачысціць стужку Уся гісторыя абнаўленняў будзе ачышчана і яе нельга будзе вярнуць. Вы ўпэўненыя? Праверка новых частак @@ -208,4 +208,7 @@ Пацвярджаць Пароль павінен змяшчаць не менш за 4 сімвалаў Схаваць загаловак пры прагортцы + Пошук толькі па %s + Вы сапраўды хочаце выдаліць усе апошнія пошукавыя запыты? Гэта дзеянне не можа быць адменена. + Апісанне \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8786b537d..819baa8e0 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -193,7 +193,7 @@ Resolver Borrar cookies Se han eliminado todas las cookies - Buscando nuevos capítulos: %1$d de %2$d + Buscando nuevos capítulos: %1$d de %2$d Limpiar feed Todo el historial de actualizaciones se borrará y esta acción no se puede deshacer. ¿Está seguro? Comprobación de nuevos capítulos diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 7a2753729..957cea79b 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,10 +1,23 @@ - #1976D2 - #0D47A1 - #1A237E - #FF8A65 - #99000000 - #E57373 - #5E636A + #1976D2 + #1565C0 + #E57373 + @android:color/black + + #2EFFFFFF + #2a2b2e + + + #2EFFFFFF + #B3000000 + + + @android:color/transparent + + + #263238 + #1fffffff + + 0.27 \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 65256236f..000000000 --- a/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index d839f3aac..e4fc0c120 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,8 +1,13 @@ - + + + \ No newline at end of file diff --git a/app/src/main/res/values-notnight-v23/bools.xml b/app/src/main/res/values-notnight-v23/bools.xml new file mode 100644 index 000000000..3610539ef --- /dev/null +++ b/app/src/main/res/values-notnight-v23/bools.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/main/res/values-notnight-v23/colors.xml b/app/src/main/res/values-notnight-v23/colors.xml new file mode 100644 index 000000000..ef3e00027 --- /dev/null +++ b/app/src/main/res/values-notnight-v23/colors.xml @@ -0,0 +1,5 @@ + + + + @color/system_ui_scrim_light + \ No newline at end of file diff --git a/app/src/main/res/values-notnight-v27/bools.xml b/app/src/main/res/values-notnight-v27/bools.xml new file mode 100644 index 000000000..2ee1ed1bf --- /dev/null +++ b/app/src/main/res/values-notnight-v27/bools.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/main/res/values-notnight-v27/colors.xml b/app/src/main/res/values-notnight-v27/colors.xml new file mode 100644 index 000000000..8e4e067bf --- /dev/null +++ b/app/src/main/res/values-notnight-v27/colors.xml @@ -0,0 +1,5 @@ + + + + @color/system_ui_scrim_light + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b20c30211..f3887596f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -24,7 +24,7 @@ Читать Добавить закладку Добавьте интересующую Вас мангу в избренное, чтобы не потерять её - Добавить в избранное + В избранное Создать категорию Добавить Введите название @@ -192,7 +192,7 @@ Пройти Очистить куки Все куки удалены - Проверка новых глав: %1$d из %2$d + Проверка новых глав: %1$d из %2$d Очистить ленту Вся история обновлений будет очищена и её нельзя будет вернуть. Вы уверены? Проверка новых глав @@ -207,4 +207,6 @@ Пароль должен содержать не менее 4 символов Прятать заголовок при прокрутке Поиск только по %s + Описание + Вы действительно хотите удалить все последние поисковые запросы? Это действие не может быть отменено. \ No newline at end of file diff --git a/app/src/main/res/values-v23/themes.xml b/app/src/main/res/values-v23/themes.xml new file mode 100644 index 000000000..0955e00db --- /dev/null +++ b/app/src/main/res/values-v23/themes.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v29/themes.xml b/app/src/main/res/values-v29/themes.xml index b8b075c5d..4ae0ef5db 100644 --- a/app/src/main/res/values-v29/themes.xml +++ b/app/src/main/res/values-v29/themes.xml @@ -1,15 +1,10 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml index 442ae059a..044349fc7 100644 --- a/app/src/main/res/values-w600dp/dimens.xml +++ b/app/src/main/res/values-w600dp/dimens.xml @@ -1,6 +1,6 @@ - 4dp + 6dp 2dp 140dp \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 4c8767415..8d2f3f480 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,6 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml index 07aafe1c4..735d84e13 100644 --- a/app/src/main/res/values/bools.xml +++ b/app/src/main/res/values/bools.xml @@ -1,4 +1,6 @@ false + false + false \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5d1c1d8ba..77bb1b4d4 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,5 +1,40 @@ + + #1976D2 + #1565C0 + #EF5350 + @android:color/white + + #39000000 + #F8F9FA + + + #B3FFFFFF + #40000000 + + + @color/system_ui_scrim_black + + @color/system_ui_scrim_black + + + #e8f0fe + @color/event_card_header_background_light + #1f000000> + + + #999 + #e6e6e6 + #33000000 + + #f8f9fa + #252729 + + 0.18 + + + #1976D2 #1565C0 #283593 @@ -10,4 +45,6 @@ #D32F2F #33000000 #C3CFDD + #99000000 + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index ee61388a6..ebeb00c0d 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,8 +1,31 @@ - 2.5dp + + + 16dp + 8dp + 32dp + + + 8dp + 24dp + 36dp + + + @dimen/match_parent + 32dp + 36dp + 24dp + + 0dp + 100dp + 0dp + 8dp> + + 6dp + 6dp 2dp - 84dp + 78dp 120dp 46dp 120dp @@ -10,4 +33,28 @@ 16dp 48dp 16dp + + + + 16dp + 16sp + 48dp + 56dp + 1dp + 1dp + 0dp + 48dp + 56dp + 8dp + 0dp + 3dp + 0dp + 16dp + 8dp + 0dp + + + -1 + -2 + \ No newline at end of file diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100644 index 000000000..5532e13df --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,6 @@ + + + + @android:integer/config_shortAnimTime + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64fb9d0bc..f18633f5d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -194,7 +194,7 @@ Solve Clear cookies All cookies was removed - Checking for new chapters: %1$d of %2$d + Checking for new chapters: %1$d of %2$d Clear feed All updates history will be cleared and this action cannot be undone. Are you sure? New chapters checking @@ -210,4 +210,5 @@ Hide toolbar when scrolling Search only on %s Do you really want to remove all recent search queries? This action cannot be undone. + Description \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 161614112..9e1bc7b0c 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,6 +1,52 @@ - + - + + + + + + + + + + - - - - - - + + + + + - + + + + + + - - - - - - \ No newline at end of file From 4ac406aa2dc5d0840e77fc04f5561b7f4bbf0ff5 Mon Sep 17 00:00:00 2001 From: ztimms73 Date: Thu, 17 Jun 2021 10:03:36 +0300 Subject: [PATCH 020/138] Reformat and some fixes --- .../res/layout-w600dp/activity_settings.xml | 26 +++++++++---------- app/src/main/res/layout/activity_protect.xml | 1 + .../res/layout/activity_setup_protect.xml | 1 + .../res/layout/dialog_favorite_categories.xml | 4 +-- app/src/main/res/layout/item_page.xml | 8 +++--- .../main/res/layout/item_source_config.xml | 3 +-- app/src/main/res/layout/sheet_pages.xml | 1 + 7 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/src/main/res/layout-w600dp/activity_settings.xml b/app/src/main/res/layout-w600dp/activity_settings.xml index 2a11422ce..3b6db9d0b 100644 --- a/app/src/main/res/layout-w600dp/activity_settings.xml +++ b/app/src/main/res/layout-w600dp/activity_settings.xml @@ -8,33 +8,33 @@ + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + app:layout_scrollFlags="scroll|enterAlways" /> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/appbar" + app:layout_constraintWidth_percent="0.6"> diff --git a/app/src/main/res/layout/item_page.xml b/app/src/main/res/layout/item_page.xml index cf4798de0..281625e76 100644 --- a/app/src/main/res/layout/item_page.xml +++ b/app/src/main/res/layout/item_page.xml @@ -3,9 +3,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:background="?android:windowBackground" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:background="?android:windowBackground"> + app:drawableTopCompat="@drawable/ic_error_large" + tools:text="@tools:sample/lorem[6]" /> @@ -32,7 +31,7 @@ + android:layout_height="wrap_content" /> Date: Thu, 17 Jun 2021 10:06:52 +0300 Subject: [PATCH 021/138] Little cleanup --- app/src/main/res/values/dimens.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8e9fc8d18..6b0ba74bd 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -4,9 +4,6 @@ 16dp - - - 36dp 24dp @@ -46,6 +43,4 @@ 8dp 0dp - - \ No newline at end of file From 8c2bc078e586733a0af8bd737639f506f232bf4f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 17 Jun 2021 19:45:36 +0300 Subject: [PATCH 022/138] Manga languages onboarding --- .idea/misc.xml | 7 ++ app/build.gradle | 2 +- .../kotatsu/core/model/MangaSource.kt | 2 +- .../kotatsu/core/prefs/AppSettings.kt | 3 + .../koitharu/kotatsu/main/ui/MainActivity.kt | 3 + .../kotatsu/settings/SettingsModule.kt | 2 + .../settings/onboard/OnboardDialogFragment.kt | 86 +++++++++++++++++ .../settings/onboard/OnboardViewModel.kt | 95 +++++++++++++++++++ .../onboard/adapter/SourceLocaleAD.kt | 23 +++++ .../onboard/adapter/SourceLocalesAdapter.kt | 28 ++++++ .../settings/onboard/model/SourceLocale.kt | 20 ++++ .../sources/SourcesSettingsFragment.kt | 22 ++++- .../koitharu/kotatsu/utils/ext/LocaleExt.kt | 29 ++++++ app/src/main/res/drawable/ic_locale.xml | 11 +++ app/src/main/res/layout/dialog_onboard.xml | 29 ++++++ .../main/res/layout/item_source_locale.xml | 16 ++++ app/src/main/res/menu/opt_sources.xml | 12 +++ app/src/main/res/values-ru/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 19 files changed, 391 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt create mode 100644 app/src/main/res/drawable/ic_locale.xml create mode 100644 app/src/main/res/layout/dialog_onboard.xml create mode 100644 app/src/main/res/layout/item_source_locale.xml create mode 100644 app/src/main/res/menu/opt_sources.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 4fd703491..bb01f41af 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -14,6 +14,8 @@ + + @@ -23,6 +25,7 @@ + @@ -31,6 +34,7 @@ + @@ -41,9 +45,12 @@ + + + diff --git a/app/build.gradle b/app/build.gradle index 9e18f5bc5..2bc93b43f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,7 +74,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-process:2.3.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'androidx.recyclerview:recyclerview:1.2.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.work:work-runtime-ktx:2.5.0' diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index 41476a63b..3f8f07973 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -37,7 +37,7 @@ enum class MangaSource( NINEMANGA_RU("NineManga Русский", "ru", NineMangaRepository.Russian::class.java), NINEMANGA_DE("NineManga Deutsch", "de", NineMangaRepository.Deutsch::class.java), NINEMANGA_IT("NineManga Italiano", "it", NineMangaRepository.Italiano::class.java), - NINEMANGA_BR("NineManga Brasil", "br", NineMangaRepository.Brazil::class.java), + NINEMANGA_BR("NineManga Brasil", "pt", NineMangaRepository.Brazil::class.java), NINEMANGA_FR("NineManga Français", "fr", NineMangaRepository.Francais::class.java), ; diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 7fc34cc4e..4456df455 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -101,6 +101,9 @@ class AppSettings private constructor(private val prefs: SharedPreferences) : var hiddenSources by StringSetPreferenceDelegate(KEY_SOURCES_HIDDEN) + val isSourcesSelected: Boolean + get() = KEY_SOURCES_HIDDEN in prefs + fun getStorageDir(context: Context): File? { val value = prefs.getString(KEY_LOCAL_STORAGE, null)?.let { File(it) diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index cd95e7e8a..bfb806231 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.commit import androidx.swiperefreshlayout.widget.CircularProgressDrawable import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar +import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity @@ -41,6 +42,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel import org.koitharu.kotatsu.search.ui.suggestion.SearchUI import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.getDisplayMessage @@ -86,6 +88,7 @@ class MainActivity : BaseActivity(), if (savedInstanceState == null) { TrackWorker.setup(applicationContext) AppUpdateChecker(this).launchIfNeeded() + OnboardDialogFragment.showWelcome(get(), supportFragmentManager) } viewModel.onOpenReader.observe(this, this::onOpenReader) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt index 222f20258..435847a8e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt @@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.backup.RestoreRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.settings.backup.BackupViewModel import org.koitharu.kotatsu.settings.backup.RestoreViewModel +import org.koitharu.kotatsu.settings.onboard.OnboardViewModel import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel val settingsModule @@ -21,4 +22,5 @@ val settingsModule viewModel { BackupViewModel(get(), androidContext()) } viewModel { (uri: Uri?) -> RestoreViewModel(uri, get(), androidContext()) } viewModel { ProtectSetupViewModel(get()) } + viewModel { OnboardViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt new file mode 100644 index 000000000..b6945a7e4 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt @@ -0,0 +1,86 @@ +package org.koitharu.kotatsu.settings.onboard + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentManager +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.databinding.DialogOnboardBinding +import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocalesAdapter +import org.koitharu.kotatsu.settings.onboard.model.SourceLocale +import org.koitharu.kotatsu.utils.ext.observeNotNull +import org.koitharu.kotatsu.utils.ext.withArgs + +class OnboardDialogFragment : AlertDialogFragment(), + OnListItemClickListener, DialogInterface.OnClickListener { + + private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) + private var isWelcome: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.run { + isWelcome = getBoolean(ARG_WELCOME, false) + } + } + + override fun onInflateView( + inflater: LayoutInflater, + container: ViewGroup?, + ) = DialogOnboardBinding.inflate(inflater, container, false) + + override fun onBuildDialog(builder: AlertDialog.Builder) { + builder + .setPositiveButton(R.string.done, this) + .setCancelable(true) + if (isWelcome) { + builder.setTitle(R.string.welcome) + } else { + builder + .setTitle(R.string.remote_sources) + .setNegativeButton(android.R.string.cancel, this) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val adapter = SourceLocalesAdapter(this) + binding.recyclerView.adapter = adapter + viewModel.list.observeNotNull(viewLifecycleOwner) { + adapter.items = it + } + } + + override fun onItemClick(item: SourceLocale, view: View) { + viewModel.setItemChecked(item.key, !item.isChecked) + } + + override fun onClick(dialog: DialogInterface?, which: Int) { + when (which) { + DialogInterface.BUTTON_POSITIVE -> viewModel.apply() + } + } + + companion object { + + private const val TAG = "OnboardDialog" + private const val ARG_WELCOME = "welcome" + + fun show(fm: FragmentManager) = OnboardDialogFragment().show(fm, TAG) + + fun showWelcome(settings: AppSettings, fm: FragmentManager) { + if (!settings.isSourcesSelected) { + OnboardDialogFragment().withArgs(1) { + putBoolean(ARG_WELCOME, true) + }.show(fm, TAG) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt new file mode 100644 index 000000000..9f8d9e65a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt @@ -0,0 +1,95 @@ +package org.koitharu.kotatsu.settings.onboard + +import androidx.collection.ArraySet +import androidx.core.os.LocaleListCompat +import androidx.lifecycle.MutableLiveData +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.settings.onboard.model.SourceLocale +import org.koitharu.kotatsu.utils.ext.map +import org.koitharu.kotatsu.utils.ext.mapTo +import org.koitharu.kotatsu.utils.ext.mapToSet +import java.util.* + +class OnboardViewModel( + private val settings: AppSettings, +) : BaseViewModel() { + + private val allSources = MangaSource.values().filterNot { x -> x == MangaSource.LOCAL } + + private val locales = allSources.mapTo(ArraySet()) { + it.locale + } + + private val selectedLocales = locales.toMutableSet() + + val list = MutableLiveData?>() + + init { + if (settings.isSourcesSelected) { + selectedLocales.removeAll(settings.hiddenSources.map { x -> MangaSource.valueOf(x).locale }) + } else { + LocaleListCompat.getDefault().mapTo(selectedLocales) { x -> + x.language + } + selectedLocales.retainAll(allSources.map { x -> x.locale }) + if (selectedLocales.isEmpty()) { + selectedLocales += "en" + } + } + rebuildList() + } + + fun setItemChecked(key: String?, isChecked: Boolean) { + val isModified = if (isChecked) { + selectedLocales.add(key) + } else { + selectedLocales.remove(key) + } + if (isModified) { + rebuildList() + } + } + + fun apply() { + settings.hiddenSources = allSources.filterNot { x -> + x.locale in selectedLocales + }.mapToSet { x -> x.name } + } + + private fun rebuildList() { + list.value = locales.map { key -> + val locale = if (key != null) { + Locale(key) + } else null + SourceLocale( + key = key, + title = locale?.getDisplayLanguage(locale)?.capitalize(locale), + isChecked = key in selectedLocales + ) + }.sortedWith(SourceLocaleComparator()) + } + + private class SourceLocaleComparator : Comparator { + + private val deviceLocales = LocaleListCompat.getAdjustedDefault() + .map { it.language } + + override fun compare(a: SourceLocale?, b: SourceLocale?): Int { + return when { + a === b -> 0 + a?.key == null -> 1 + b?.key == null -> -1 + else -> { + val index = deviceLocales.indexOf(a.key) + if (index == -1) { + compareValues(a.title, b.title) + } else { + -2 - index + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt new file mode 100644 index 000000000..d46bafd3d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.settings.onboard.adapter + +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.databinding.ItemSourceLocaleBinding +import org.koitharu.kotatsu.settings.onboard.model.SourceLocale + +fun sourceLocaleAD( + clickListener: OnListItemClickListener +) = adapterDelegateViewBinding( + { inflater, parent -> ItemSourceLocaleBinding.inflate(inflater, parent, false) } +) { + + binding.root.setOnClickListener { + clickListener.onItemClick(item, it) + } + + bind { + binding.root.text = item.title ?: getString(R.string.other) + binding.root.isChecked = item.isChecked + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt new file mode 100644 index 000000000..0d4112f31 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.settings.onboard.adapter + +import androidx.recyclerview.widget.DiffUtil +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.settings.onboard.model.SourceLocale + +class SourceLocalesAdapter( + clickListener: OnListItemClickListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + delegatesManager.addDelegate(sourceLocaleAD(clickListener)) + } + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame( + oldItem: SourceLocale, + newItem: SourceLocale, + ): Boolean = oldItem.key == newItem.key + + override fun areContentsTheSame( + oldItem: SourceLocale, + newItem: SourceLocale, + ): Boolean = oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt new file mode 100644 index 000000000..c2bce3718 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt @@ -0,0 +1,20 @@ +package org.koitharu.kotatsu.settings.onboard.model + +import java.util.* + +data class SourceLocale( + val key: String?, + val title: String?, + val isChecked: Boolean, +) : Comparable { + + override fun compareTo(other: SourceLocale): Int { + return when { + this === other -> 0 + key == Locale.getDefault().language -> -2 + key == null -> 1 + other.key == null -> -1 + else -> compareValues(title, other.title) + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index 9c30c32cb..ac2d96c79 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -1,9 +1,7 @@ package org.koitharu.kotatsu.settings.sources import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.core.graphics.Insets import androidx.core.view.updatePadding import androidx.recyclerview.widget.DividerItemDecoration @@ -16,6 +14,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment class SourcesSettingsFragment : BaseFragment(), OnListItemClickListener { @@ -25,6 +24,7 @@ class SourcesSettingsFragment : BaseFragment(), override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) reorderHelper = ItemTouchHelper(SourcesReorderCallback()) + setHasOptionsMenu(true) } override fun onInflateView( @@ -51,6 +51,22 @@ class SourcesSettingsFragment : BaseFragment(), super.onDestroyView() } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + // TODO handle changes in dialog + // inflater.inflate(R.menu.opt_sources, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when(item.itemId) { + R.id.action_languages -> { + OnboardDialogFragment.show(parentFragmentManager) + true + } + else -> super.onOptionsItemSelected(item) + } + } + override fun onWindowInsetsChanged(insets: Insets) { binding.recyclerView.updatePadding( bottom = insets.bottom, diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt new file mode 100644 index 000000000..1ad4cc003 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt @@ -0,0 +1,29 @@ +package org.koitharu.kotatsu.utils.ext + +import androidx.core.os.LocaleListCompat +import java.util.* +import kotlin.collections.ArrayList + +fun LocaleListCompat.toList(): List { + val list = ArrayList(size()) + for (i in 0 until size()) { + list += get(i) + } + return list +} + +inline fun > LocaleListCompat.mapTo( + destination: C, + block: (Locale) -> R, +): C { + val len = size() + for (i in 0 until len) { + val item = get(i) + destination.add(block(item)) + } + return destination +} + +inline fun LocaleListCompat.map(block: (Locale) -> T): List { + return mapTo(ArrayList(size()), block) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_locale.xml b/app/src/main/res/drawable/ic_locale.xml new file mode 100644 index 000000000..1bc2d39de --- /dev/null +++ b/app/src/main/res/drawable/ic_locale.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/dialog_onboard.xml b/app/src/main/res/layout/dialog_onboard.xml new file mode 100644 index 000000000..2845055a6 --- /dev/null +++ b/app/src/main/res/layout/dialog_onboard.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_source_locale.xml b/app/src/main/res/layout/item_source_locale.xml new file mode 100644 index 000000000..1ccef475e --- /dev/null +++ b/app/src/main/res/layout/item_source_locale.xml @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_sources.xml b/app/src/main/res/menu/opt_sources.xml new file mode 100644 index 000000000..35c7034be --- /dev/null +++ b/app/src/main/res/menu/opt_sources.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b20c30211..4638416bc 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -207,4 +207,7 @@ Пароль должен содержать не менее 4 символов Прятать заголовок при прокрутке Поиск только по %s + Другие + Languages + Welcome \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64fb9d0bc..8112a0029 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -210,4 +210,7 @@ Hide toolbar when scrolling Search only on %s Do you really want to remove all recent search queries? This action cannot be undone. + Other + Languages + Welcome \ No newline at end of file From c71460fcd8dda06f566d2428ebcd756bcecef9fd Mon Sep 17 00:00:00 2001 From: ztimms73 Date: Thu, 17 Jun 2021 23:33:52 +0300 Subject: [PATCH 023/138] Minor fixes --- app/src/main/AndroidManifest.xml | 7 +++++++ .../koitharu/kotatsu/base/ui/widgets/ChipsView.kt | 6 +++--- .../koitharu/kotatsu/details/ui/DetailsFragment.kt | 13 +++++++++---- app/src/main/res/values/styles.xml | 4 ++-- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 78f461918..cc2ca5eba 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,13 @@ + + + + + + + (), View.OnClickList .lifecycle(viewLifecycleOwner) .enqueueWith(coil) textViewTitle.text = manga.title + textViewSubtitle.textAndVisible = manga.altTitle textViewAuthor.textAndVisible = manga.author textViewSource.text = manga.source.title textViewDescription.text = @@ -88,8 +89,12 @@ class DetailsFragment : BaseFragment(), View.OnClickList } val file = manga.url.toUri().toFileOrNull() if (file != null) { - val size = file.length() - textViewSize.text = FileSizeUtils.formatBytes(requireContext(), size) + viewLifecycleScope.launch { + val size = withContext(Dispatchers.IO) { + file.length() + } + textViewSize.text = FileSizeUtils.formatBytes(requireContext(), size) + } sizeContainer.isVisible = true } else { sizeContainer.isVisible = false @@ -117,9 +122,9 @@ class DetailsFragment : BaseFragment(), View.OnClickList private fun onFavouriteChanged(isFavourite: Boolean) { with(binding.buttonFavorite) { if (isFavourite) { - this?.setIconResource(R.drawable.ic_heart) + this.setIconResource(R.drawable.ic_heart) } else { - this?.setIconResource(R.drawable.ic_heart_outline) + this.setIconResource(R.drawable.ic_heart_outline) } } } diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9d7d07c57..b3a63cefa 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,4 +1,4 @@ - + @@ -60,7 +60,7 @@ + + + + + + + + + + \ No newline at end of file From 7bb809f2270490c19a6ec6cbded387d7eba1db00 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Thu, 29 Jul 2021 22:37:08 +0300 Subject: [PATCH 108/138] Improve thumbnail sheet --- .../ui/thumbnails/PagesThumbnailsSheet.kt | 43 ++++++++----------- app/src/main/res/layout/sheet_pages.xml | 25 +++-------- app/src/main/res/values/styles.xml | 4 ++ 3 files changed, 28 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt index 06083d02e..69e5c275d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import org.koin.android.ext.android.get @@ -53,6 +54,15 @@ class PagesThumbnailsSheet : BaseBottomSheet(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + val title = arguments?.getString(ARG_TITLE) + binding.toolbar.title = title + binding.toolbar.setNavigationOnClickListener { dismiss() } + binding.toolbar.subtitle = + resources.getQuantityString(R.plurals.pages, thumbnails.size, thumbnails.size) + + val initialTopPosition = binding.recyclerView.top + with(binding.recyclerView) { addItemDecoration( SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing)) @@ -64,42 +74,27 @@ class PagesThumbnailsSheet : BaseBottomSheet(), get(), this@PagesThumbnailsSheet ) + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) = Unit + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + binding.appbar.isLifted = getChildAt(0).top < initialTopPosition + } + }) addOnLayoutChangeListener(spanResolver) spanResolver.setGridSize(get().gridSize / 100f, this) } - - val title = arguments?.getString(ARG_TITLE) - binding.toolbar.title = title - binding.toolbar.setNavigationOnClickListener { dismiss() } - binding.toolbar.subtitle = - resources.getQuantityString(R.plurals.pages, thumbnails.size, thumbnails.size) - binding.textViewTitle.text = title - if (dialog !is BottomSheetDialog) { - binding.toolbar.isVisible = true - binding.textViewTitle.isVisible = false - binding.appbar.elevation = resources.getDimension(R.dimen.elevation_large) - } } override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).also { val behavior = (it as? BottomSheetDialog)?.behavior ?: return@also behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { - private val elevation = resources.getDimension(R.dimen.elevation_large) override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit - override fun onStateChanged(bottomSheet: View, newState: Int) { - if (newState == BottomSheetBehavior.STATE_EXPANDED) { - binding.toolbar.isVisible = true - binding.textViewTitle.isVisible = false - binding.appbar.elevation = elevation - } else { - binding.toolbar.isVisible = false - binding.textViewTitle.isVisible = true - binding.appbar.elevation = 0f - } - } + override fun onStateChanged(bottomSheet: View, newState: Int) = Unit }) } diff --git a/app/src/main/res/layout/sheet_pages.xml b/app/src/main/res/layout/sheet_pages.xml index 2552723d5..03faf7477 100644 --- a/app/src/main/res/layout/sheet_pages.xml +++ b/app/src/main/res/layout/sheet_pages.xml @@ -9,33 +9,17 @@ + app:liftOnScroll="true"> - - + app:navigationIcon="@drawable/ic_cross" /> @@ -47,6 +31,7 @@ android:padding="@dimen/grid_spacing" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:spanCount="3" tools:listitem="@layout/item_page_thumb" /> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9d246d5e3..b53cfab6e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -9,6 +9,10 @@ 8dp + + - - \ No newline at end of file From 27293f1bf8914e20273887ee0698ebea9dce9749 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 5 Sep 2021 16:00:15 +0300 Subject: [PATCH 124/138] Remove some findViewById --- .../favourites/ui/FavouritesContainerFragment.kt | 4 ++-- .../org/koitharu/kotatsu/list/ui/MangaListFragment.kt | 4 ++-- .../java/org/koitharu/kotatsu/main/ui/AppBarOwner.kt | 8 ++++++++ .../java/org/koitharu/kotatsu/main/ui/MainActivity.kt | 10 +++++++--- .../search/ui/suggestion/SearchSuggestionFragment.kt | 6 +++--- .../org/koitharu/kotatsu/tracker/ui/FeedFragment.kt | 5 +++-- 6 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/main/ui/AppBarOwner.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index a76612600..399314311 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -6,7 +6,6 @@ import androidx.core.graphics.Insets import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.appbar.AppBarLayout import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayoutMediator import org.koin.androidx.viewmodel.ext.android.viewModel @@ -18,6 +17,7 @@ import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel +import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.utils.RecycledViewPoolHolder import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight @@ -68,7 +68,7 @@ class FavouritesContainerFragment : BaseFragment(), } override fun onWindowInsetsChanged(insets: Insets) { - val headerHeight = requireActivity().findViewById(R.id.appbar).measureHeight() + val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top binding.root.updatePadding( top = headerHeight - insets.top ) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index cbb215e00..066da26ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -14,7 +14,6 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import com.google.android.material.appbar.AppBarLayout import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch import org.koin.android.ext.android.get @@ -37,6 +36,7 @@ import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.filter.FilterAdapter import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.main.ui.MainActivity import org.koitharu.kotatsu.utils.RecycledViewPoolHolder import org.koitharu.kotatsu.utils.ext.* @@ -223,7 +223,7 @@ abstract class MangaListFragment : BaseFragment(), } override fun onWindowInsetsChanged(insets: Insets) { - val headerHeight = requireActivity().findViewById(R.id.appbar).measureHeight() + val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top binding.recyclerViewFilter.updatePadding( top = headerHeight, bottom = insets.bottom diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/AppBarOwner.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/AppBarOwner.kt new file mode 100644 index 000000000..d5a2de5bc --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/AppBarOwner.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.main.ui + +import com.google.android.material.appbar.AppBarLayout + +interface AppBarOwner { + + val appBar: AppBarLayout +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index c651f557d..4272a2b5e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.CircularProgressDrawable +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.get @@ -50,7 +51,7 @@ import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.* class MainActivity : BaseActivity(), - NavigationView.OnNavigationItemSelectedListener, + NavigationView.OnNavigationItemSelectedListener, AppBarOwner, View.OnClickListener, View.OnFocusChangeListener, SearchSuggestionListener { private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) @@ -62,6 +63,9 @@ class MainActivity : BaseActivity(), private lateinit var drawerToggle: ActionBarDrawerToggle private var searchViewElevation = 0f + override val appBar: AppBarLayout + get() = binding.appbar + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater)) @@ -334,7 +338,7 @@ class MainActivity : BaseActivity(), binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) drawerToggle.isDrawerIndicatorEnabled = false // Avoiding shadows on the sides if the color is transparent, so we make the AppBarLayout white/dark - binding.appbar.setBackgroundColor(resources.getColor(R.color.color_on_secondary)) + binding.appbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_on_secondary)) binding.toolbarCard.cardElevation = 0f binding.appbar.elevation = searchViewElevation } @@ -343,7 +347,7 @@ class MainActivity : BaseActivity(), binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) drawerToggle.isDrawerIndicatorEnabled = true // Returning transparent color - binding.appbar.setBackgroundColor(resources.getColor(android.R.color.transparent)) + binding.appbar.setBackgroundColor(Color.TRANSPARENT) binding.appbar.elevation = 0f binding.toolbarCard.cardElevation = searchViewElevation } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt index 3777955dd..f48e9b0da 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt @@ -7,12 +7,11 @@ import android.view.ViewGroup import androidx.core.graphics.Insets import androidx.core.view.updatePadding import androidx.recyclerview.widget.ItemTouchHelper -import com.google.android.material.appbar.AppBarLayout import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.sharedViewModel -import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding +import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter import org.koitharu.kotatsu.utils.ext.measureHeight @@ -42,8 +41,9 @@ class SearchSuggestionFragment : BaseFragment() } override fun onWindowInsetsChanged(insets: Insets) { + val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top binding.root.updatePadding( - top = requireActivity().findViewById(R.id.appbar).measureHeight(), + top = headerHeight, left = insets.left, right = insets.right, bottom = insets.bottom, diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt index b24c74b31..66413a5f5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt @@ -5,7 +5,6 @@ import android.view.* import androidx.appcompat.app.AlertDialog import androidx.core.graphics.Insets import androidx.core.view.updatePadding -import com.google.android.material.appbar.AppBarLayout import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel @@ -17,6 +16,7 @@ import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.databinding.FragmentFeedBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.getDisplayMessage @@ -98,8 +98,9 @@ class FeedFragment : BaseFragment(), PaginationScrollListen } override fun onWindowInsetsChanged(insets: Insets) { + val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top binding.recyclerView.updatePadding( - top = requireActivity().findViewById(R.id.appbar).measureHeight(), + top = headerHeight, left = insets.left, right = insets.right, bottom = insets.bottom From 593624fdb996b4c9980e306f80e5caefb860e1cb Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 8 Sep 2021 07:27:25 +0300 Subject: [PATCH 125/138] Manga repository authorization support --- app/src/main/AndroidManifest.xml | 20 ++- .../kotatsu/base/domain/MangaLoaderContext.kt | 17 +++ .../koitharu/kotatsu/base/ui/BaseFragment.kt | 1 + .../koitharu/kotatsu/browser/BrowserClient.kt | 17 --- .../kotatsu/core/model/MangaFilter.kt | 4 +- .../parser/MangaRepositoryAuthProvider.kt | 8 ++ .../kotatsu/core/prefs/SourceSettings.kt | 1 + .../kotatsu/list/ui/filter/FilterAdapter.kt | 3 +- .../kotatsu/local/ui/LocalListViewModel.kt | 2 +- .../reader/ui/SimpleSettingsActivity.kt | 14 +++ .../remotelist/ui/RemoteListFragment.kt | 25 ++++ .../remotelist/ui/RemoteListViewModel.kt | 3 + .../kotatsu/search/ui/SearchViewModel.kt | 2 +- .../settings/HistorySettingsFragment.kt | 28 +++-- .../settings/SourceSettingsFragment.kt | 27 ++++- .../sources/auth/SourceAuthActivity.kt | 114 ++++++++++++++++++ app/src/main/res/menu/opt_list_remote.xml | 11 ++ app/src/main/res/values-ru/strings.xml | 43 ++++--- app/src/main/res/values/strings.xml | 43 ++++--- app/src/main/res/xml/pref_source.xml | 7 ++ 20 files changed, 312 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt create mode 100644 app/src/main/res/menu/opt_list_remote.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7e221bce1..e18e81392 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,9 @@ android:theme="@style/AppTheme" android:usesCleartextTraffic="true" tools:ignore="UnusedAttribute"> - + @@ -32,12 +34,16 @@ android:name="android.app.default_searchable" android:value=".ui.search.SearchActivity" /> - + - + @@ -50,13 +56,19 @@ android:label="@string/settings" /> - + + { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTP) + .host(domain) + .build() + return cookieJar.loadForRequest(url) + } + + fun copyCookies(oldDomain: String, newDomain: String) { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTP) + .host(oldDomain) + val cookies = cookieJar.loadForRequest(url.build()) + url.host(newDomain) + cookieJar.saveFromResponse(url.build(), cookies) + } + private companion object { private const val SCHEME_HTTP = "http" diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt index 27f923937..76f774bc8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt @@ -38,6 +38,7 @@ abstract class BaseFragment : Fragment(), OnApplyWindowInsetsLi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + lastInsets = Insets.NONE ViewCompat.setOnApplyWindowInsetsListener(view, this) } diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserClient.kt b/app/src/main/java/org/koitharu/kotatsu/browser/BrowserClient.kt index 4a666d1c7..f39da5fb5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserClient.kt +++ b/app/src/main/java/org/koitharu/kotatsu/browser/BrowserClient.kt @@ -1,10 +1,8 @@ package org.koitharu.kotatsu.browser import android.graphics.Bitmap -import android.webkit.WebResourceResponse import android.webkit.WebView import okhttp3.OkHttpClient -import okhttp3.Request import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koitharu.kotatsu.core.network.WebViewClientCompat @@ -27,19 +25,4 @@ class BrowserClient(private val callback: BrowserCallback) : WebViewClientCompat super.onPageCommitVisible(view, url) callback.onTitleChanged(view.title.orEmpty(), url) } - - override fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? { - return runCatching { - val request = Request.Builder() - .url(url) - .build() - val response = okHttp.newCall(request).execute() - val ct = response.body?.contentType() - WebResourceResponse( - "${ct?.type}/${ct?.subtype}", - ct?.charset()?.name() ?: "utf-8", - response.body?.byteStream() - ) - }.getOrNull() - } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt index 68a3b5174..814c00571 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt @@ -5,6 +5,6 @@ import kotlinx.parcelize.Parcelize @Parcelize data class MangaFilter( - val sortOrder: SortOrder, - val tag: MangaTag? + val sortOrder: SortOrder?, + val tag: MangaTag?, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt new file mode 100644 index 000000000..16cc5e39f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.core.parser + +interface MangaRepositoryAuthProvider { + + val authUrl: String + + fun isAuthorized(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt index 6ee3d0747..7a6036d06 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt @@ -27,5 +27,6 @@ interface SourceSettings { const val KEY_DOMAIN = "domain" const val KEY_USE_SSL = "ssl" + const val KEY_AUTH = "auth" } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt index b0b32096a..e7fe78058 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt @@ -7,7 +7,6 @@ import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.SortOrder import java.util.* -import kotlin.collections.ArrayList class FilterAdapter( sortOrders: List = emptyList(), @@ -19,7 +18,7 @@ class FilterAdapter( private val sortOrders = ArrayList(sortOrders) private val tags = ArrayList(Collections.singletonList(null) + tags) - private var currentState = state ?: MangaFilter(sortOrders.first(), null) + private var currentState = state ?: MangaFilter(sortOrders.firstOrNull(), null) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { VIEW_TYPE_SORT -> FilterSortHolder(parent).apply { diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 1991e2c0e..844cbcbbc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -61,7 +61,7 @@ class LocalListViewModel( launchLoadingJob(Dispatchers.Default) { try { listError.value = null - mangaList.value = repository.getList(0) + mangaList.value = repository.getList(0, tags = null) } catch (e: Throwable) { listError.value = e } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt index 1c62b2cce..0113bb475 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt @@ -3,16 +3,19 @@ package org.koitharu.kotatsu.reader.ui import android.content.Context import android.content.Intent import android.os.Bundle +import android.os.Parcelable import androidx.core.graphics.Insets import androidx.core.view.updatePadding import androidx.fragment.app.commit import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.databinding.ActivitySettingsSimpleBinding import org.koitharu.kotatsu.settings.MainSettingsFragment import org.koitharu.kotatsu.settings.NetworkSettingsFragment import org.koitharu.kotatsu.settings.ReaderSettingsFragment +import org.koitharu.kotatsu.settings.SourceSettingsFragment class SimpleSettingsActivity : BaseActivity() { @@ -25,6 +28,9 @@ class SimpleSettingsActivity : BaseActivity() { R.id.container, when (intent?.action) { Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment() ACTION_READER -> ReaderSettingsFragment() + ACTION_SOURCE -> SourceSettingsFragment.newInstance( + intent.getParcelableExtra(EXTRA_SOURCE) ?: MangaSource.LOCAL + ) else -> MainSettingsFragment() } ) @@ -43,9 +49,17 @@ class SimpleSettingsActivity : BaseActivity() { private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS" + private const val ACTION_SOURCE = + "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS" + private const val EXTRA_SOURCE = "source" fun newReaderSettingsIntent(context: Context) = Intent(context, SimpleSettingsActivity::class.java) .setAction(ACTION_READER) + + fun newSourceSettingsIntent(context: Context, source: MangaSource) = + Intent(context, SimpleSettingsActivity::class.java) + .setAction(ACTION_SOURCE) + .putExtra(EXTRA_SOURCE, source as Parcelable) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 1c5004eb4..2ecb7f7bf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -1,10 +1,15 @@ package org.koitharu.kotatsu.remotelist.ui +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.list.ui.MangaListFragment +import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity import org.koitharu.kotatsu.utils.ext.parcelableArgument import org.koitharu.kotatsu.utils.ext.withArgs @@ -29,6 +34,26 @@ class RemoteListFragment : MangaListFragment() { super.onFilterChanged(filter) } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.opt_list_remote, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_source_settings -> { + startActivity( + SimpleSettingsActivity.newSourceSettingsIntent( + context ?: return false, + source, + ) + ) + true + } + else -> super.onOptionsItemSelected(item) + } + } + companion object { private const val ARG_SOURCE = "provider" diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index b344b2bda..aaf6ccaa3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -90,6 +90,9 @@ class RemoteListViewModel( } hasNextPage.value = list.isNotEmpty() } catch (e: Throwable) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } listError.value = e } } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt index 193817c33..35df96c82 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt @@ -12,7 +12,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import java.util.* class SearchViewModel( private val repository: MangaRepository, @@ -74,6 +73,7 @@ class SearchViewModel( listError.value = null val list = repository.getList( offset = if (append) mangaList.value?.size ?: 0 else 0, + tags = null, query = query ) if (!append) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index 8380e414f..99ec51e9c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -76,15 +76,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach true } AppSettings.KEY_COOKIES_CLEAR -> { - viewLifecycleScope.launch { - val cookieJar = get() - cookieJar.clear() - Snackbar.make( - listView ?: return@launch, - R.string.cookies_cleared, - Snackbar.LENGTH_SHORT - ).show() - } + clearCookies() true } AppSettings.KEY_SEARCH_HISTORY_CLEAR -> { @@ -144,4 +136,22 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach } }.show() } + + private fun clearCookies() { + AlertDialog.Builder(context ?: return) + .setTitle(R.string.clear_cookies) + .setMessage(R.string.text_clear_cookies_prompt) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.clear) { _, _ -> + viewLifecycleScope.launch { + val cookieJar = get() + cookieJar.clear() + Snackbar.make( + listView ?: return@launch, + R.string.cookies_cleared, + Snackbar.LENGTH_SHORT + ).show() + } + }.show() + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt index 4bccaf8e2..da9115e80 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt @@ -9,8 +9,10 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.TwoStatePreference import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.SourceSettings +import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity import org.koitharu.kotatsu.settings.utils.EditTextBindListener import org.koitharu.kotatsu.settings.utils.EditTextDefaultSummaryProvider import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf @@ -20,6 +22,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs class SourceSettingsFragment : PreferenceFragmentCompat() { private val source by parcelableArgument(EXTRA_SOURCE) + private var repository: RemoteMangaRepository? = null override fun onResume() { super.onResume() @@ -29,6 +32,7 @@ class SourceSettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.sharedPreferencesName = source.name val repo = mangaRepositoryOf(source) as? RemoteMangaRepository ?: return + repository = repo addPreferencesFromResource(R.xml.pref_source) val screen = preferenceScreen val prefsMap = ArrayMap(screen.preferenceCount) @@ -41,13 +45,32 @@ class SourceSettingsFragment : PreferenceFragmentCompat() { initPreferenceWithDefaultValue(pref, defValue) } } + findPreference(SourceSettings.KEY_AUTH)?.run { + isVisible = repo is MangaRepositoryAuthProvider + isEnabled = (repo as? MangaRepositoryAuthProvider)?.isAuthorized() == false + } + } + + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + return when (preference?.key) { + SourceSettings.KEY_AUTH -> { + startActivity( + SourceAuthActivity.newIntent( + context ?: return false, + source, + ) + ) + true + } + else -> super.onPreferenceTreeClick(preference) + } } private fun initPreferenceWithDefaultValue(preference: Preference, defaultValue: Any) { - when(preference) { + when (preference) { is EditTextPreference -> { preference.summaryProvider = EditTextDefaultSummaryProvider(defaultValue.toString()) - when(preference.key) { + when (preference.key) { SourceSettings.KEY_DOMAIN -> preference.setOnBindEditTextListener( EditTextBindListener( EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI, diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt new file mode 100644 index 000000000..896c92780 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt @@ -0,0 +1,114 @@ +package org.koitharu.kotatsu.settings.sources.auth + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.view.MenuItem +import android.widget.Toast +import androidx.core.graphics.Insets +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.browser.BrowserCallback +import org.koitharu.kotatsu.browser.BrowserClient +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider +import org.koitharu.kotatsu.databinding.ActivityBrowserBinding +import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf + +class SourceAuthActivity : BaseActivity(), BrowserCallback { + + private lateinit var repository: MangaRepositoryAuthProvider + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityBrowserBinding.inflate(layoutInflater)) + val source = intent?.getParcelableExtra(EXTRA_SOURCE) + if (source == null) { + finish() + return + } + repository = mangaRepositoryOf(source) as? MangaRepositoryAuthProvider ?: run { + Toast.makeText( + this, + getString(R.string.auth_not_supported_by, source.title), + Toast.LENGTH_SHORT + ).show() + finishAfterTransition() + return + } + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setHomeAsUpIndicator(R.drawable.ic_cross) + } + with(binding.webView.settings) { + javaScriptEnabled = true + } + binding.webView.webViewClient = BrowserClient(this) + val url = repository.authUrl + onTitleChanged( + source.title, + getString(R.string.loading_) + ) + binding.webView.loadUrl(url) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + android.R.id.home -> { + binding.webView.stopLoading() + finishAfterTransition() + true + } + else -> super.onOptionsItemSelected(item) + } + + override fun onBackPressed() { + if (binding.webView.canGoBack()) { + binding.webView.goBack() + } else { + super.onBackPressed() + } + } + + override fun onPause() { + binding.webView.onPause() + super.onPause() + } + + override fun onResume() { + super.onResume() + binding.webView.onResume() + } + + override fun onLoadingStateChanged(isLoading: Boolean) { + binding.progressBar.isVisible = isLoading + if (!isLoading && repository.isAuthorized()) { + Toast.makeText(this, R.string.auth_complete, Toast.LENGTH_SHORT).show() + finishAfterTransition() + } + } + + override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) { + this.title = title + supportActionBar?.subtitle = subtitle + } + + override fun onWindowInsetsChanged(insets: Insets) { + binding.appbar.updatePadding(top = insets.top) + binding.webView.updatePadding(bottom = insets.bottom) + } + + companion object { + + private const val EXTRA_SOURCE = "source" + + fun newIntent(context: Context, source: MangaSource): Intent { + return Intent(context, SourceAuthActivity::class.java) + .putExtra(EXTRA_SOURCE, source as Parcelable) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/menu/opt_list_remote.xml b/app/src/main/res/menu/opt_list_remote.xml new file mode 100644 index 000000000..deb531840 --- /dev/null +++ b/app/src/main/res/menu/opt_list_remote.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 29428323b..35be37286 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -213,24 +213,27 @@ Другие Описание Языки - Добро пожаловать - Вы действительно хотите удалить все недавние поисковые запросы? Это действие не может быть отменено. - Резервная копия успешно сохранена - Некоторые производители могут изменять поведение системы, нарушая работу фоновых задач. - Подробнее - В очереди - На данный момент нет активных загрузок - Глава отсутствует - Эта глава отсутствует на вашем устройстве. Скачайте или прочитайте её онлайн. - Помочь с переводом приложения - Перевод - Автор - Тема на 4PDA - Обратная связь - Поддержать разработчика - Если вам нравится это приложение, вы можете помочь финансово с помощью ЮMoney (бывш. Яндекс.Деньги) - Благодарности - Эти люди помогают Kotatsu стать лучше! - Авторские права и лицензии - Лицензия + Добро пожаловать + Вы действительно хотите удалить все недавние поисковые запросы? Это действие не может быть отменено. + Резервная копия успешно сохранена + Некоторые производители могут изменять поведение системы, нарушая работу фоновых задач. + Подробнее + В очереди + На данный момент нет активных загрузок + Глава отсутствует + Эта глава отсутствует на вашем устройстве. Скачайте или прочитайте её онлайн. + Помочь с переводом приложения + Перевод + Автор + Тема на 4PDA + Обратная связь + Поддержать разработчика + Если вам нравится это приложение, вы можете помочь финансово с помощью ЮMoney (бывш. Яндекс.Деньги) + Благодарности + Эти люди помогают Kotatsu стать лучше! + Авторские права и лицензии + Лицензия + Авторизация выполнена + Авторизация в %s не поддерживается + Вы выйдете из всех источников, в которых Вы авторизованы \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6566b5749..f82f4b5d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -216,24 +216,27 @@ Do you really want to remove all recent search queries? This action cannot be undone. Other Languages - Welcome - Description - Backup saved successfully - Some manufacturers can change the system behavior, which may breaks background tasks. - Read more - Queued - There are currently no active downloads - This chapter is missing on your device. Download or read it online. - Chapter is missing - Translate this app - Translation - Author - Feedback - Topic on 4PDA - Support the developer - If you like this app, you can help financially through Yoomoney (ex. Yandex.Money) - Gratitudes - These people make Kotatsu become better! - Copyright & Licenses - License + Welcome + Description + Backup saved successfully + Some manufacturers can change the system behavior, which may breaks background tasks. + Read more + Queued + There are currently no active downloads + This chapter is missing on your device. Download or read it online. + Chapter is missing + Translate this app + Translation + Author + Feedback + Topic on 4PDA + Support the developer + If you like this app, you can help financially through Yoomoney (ex. Yandex.Money) + Gratitudes + These people make Kotatsu become better! + Copyright & Licenses + License + Authorization complete + Authorization on %s is not supported + You will be logged out from all sources that you are authorized in \ No newline at end of file diff --git a/app/src/main/res/xml/pref_source.xml b/app/src/main/res/xml/pref_source.xml index fa59bb8f9..6e773ce26 100644 --- a/app/src/main/res/xml/pref_source.xml +++ b/app/src/main/res/xml/pref_source.xml @@ -14,4 +14,11 @@ android:title="@string/use_ssl" app:iconSpaceReserved="false" /> + + \ No newline at end of file From 4977464e690750fba607a7f2b365ac959e260460 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 8 Sep 2021 07:27:44 +0300 Subject: [PATCH 126/138] ExHentai manga source --- .../kotatsu/base/domain/MangaLoaderContext.kt | 29 +- .../kotatsu/core/model/MangaSource.kt | 1 + .../kotatsu/core/parser/ParserModule.kt | 1 + .../core/parser/site/ExHentaiRepository.kt | 265 ++++++++++++++++++ .../core/parser/site/NineMangaRepository.kt | 2 +- .../kotatsu/utils/ext/CookieJarExt.kt | 37 +++ .../koitharu/kotatsu/utils/ext/ParseExt.kt | 8 +- .../koitharu/kotatsu/utils/ext/StringExt.kt | 8 +- 8 files changed, 320 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ext/CookieJarExt.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt index 821d89e7b..2c8ec865d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt @@ -9,7 +9,7 @@ import org.koitharu.kotatsu.utils.ext.await open class MangaLoaderContext( private val okHttp: OkHttpClient, - private val cookieJar: CookieJar + val cookieJar: CookieJar ) : KoinComponent { suspend fun httpGet(url: String, headers: Headers? = null): Response { @@ -57,33 +57,6 @@ open class MangaLoaderContext( open fun getSettings(source: MangaSource) = SourceSettings(get(), source) - fun insertCookies(domain: String, vararg cookies: String) { - val url = HttpUrl.Builder() - .scheme(SCHEME_HTTP) - .host(domain) - .build() - cookieJar.saveFromResponse(url, cookies.mapNotNull { - Cookie.parse(url, it) - }) - } - - fun getCookies(domain: String): List { - val url = HttpUrl.Builder() - .scheme(SCHEME_HTTP) - .host(domain) - .build() - return cookieJar.loadForRequest(url) - } - - fun copyCookies(oldDomain: String, newDomain: String) { - val url = HttpUrl.Builder() - .scheme(SCHEME_HTTP) - .host(oldDomain) - val cookies = cookieJar.loadForRequest(url.build()) - url.host(newDomain) - cookieJar.saveFromResponse(url.build(), cookies) - } - private companion object { private const val SCHEME_HTTP = "http" diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index 3f8f07973..f881a59f0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -39,6 +39,7 @@ enum class MangaSource( NINEMANGA_IT("NineManga Italiano", "it", NineMangaRepository.Italiano::class.java), NINEMANGA_BR("NineManga Brasil", "pt", NineMangaRepository.Brazil::class.java), NINEMANGA_FR("NineManga Français", "fr", NineMangaRepository.Francais::class.java), + EXHENTAI("ExHentai", null, ExHentaiRepository::class.java) ; @get:Throws(NoBeanDefFoundException::class) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt index baf3156e3..361bdfa61 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt @@ -32,4 +32,5 @@ val parserModule factory(named(MangaSource.NINEMANGA_RU)) { NineMangaRepository.Russian(get()) } factory(named(MangaSource.NINEMANGA_IT)) { NineMangaRepository.Italiano(get()) } factory(named(MangaSource.NINEMANGA_FR)) { NineMangaRepository.Francais(get()) } + factory(named(MangaSource.EXHENTAI)) { ExHentaiRepository(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt new file mode 100644 index 000000000..a699357e8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt @@ -0,0 +1,265 @@ +package org.koitharu.kotatsu.core.parser.site + +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.base.domain.MangaLoaderContext +import org.koitharu.kotatsu.core.model.* +import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.utils.ext.* +import kotlin.math.pow + +private const val DOMAIN_UNAUTHORIZED = "e-hentai.org" +private const val DOMAIN_AUTHORIZED = "exhentai.org" + +class ExHentaiRepository( + loaderContext: MangaLoaderContext, +) : RemoteMangaRepository(loaderContext), MangaRepositoryAuthProvider { + + override val source = MangaSource.EXHENTAI + + override val defaultDomain: String + get() = if (isAuthorized()) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED + + override val authUrl: String + get() = "https://${getDomain()}/bounce_login.php" + + private val ratingPattern = Regex("-?[0-9]+px") + private val authCookies = arrayOf("ipb_member_id", "ipb_pass_hash") + private var updateDm = false + + init { + loaderContext.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "nw=1", "sl=dm_2") + loaderContext.cookieJar.insertCookies(DOMAIN_UNAUTHORIZED, "nw=1", "sl=dm_2") + } + + override suspend fun getList( + offset: Int, + query: String?, + sortOrder: SortOrder?, + tag: MangaTag?, + ): List = getList(offset, query, setOfNotNull(tag), sortOrder) + + override suspend fun getList( + offset: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder?, + ): List { + val page = (offset / 25f).toIntUp() + var search = query?.urlEncoded().orEmpty() + val url = buildString { + append("https://") + append(getDomain()) + append("/?page=") + append(page) + if (!tags.isNullOrEmpty()) { + var fCats = 0 + for (tag in tags) { + tag.key.toIntOrNull()?.let { fCats = fCats or it } ?: run { + search += tag.key + " " + } + } + if (fCats != 0) { + append("&f_cats=") + append(1023 - fCats) + } + } + if (search.isNotEmpty()) { + append("&f_search=") + append(search.trim().replace(' ', '+')) + } + // by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again + if (updateDm) { + append("&inline_set=dm_e") + } + } + val body = loaderContext.httpGet(url).parseHtml().body() + val root = body.selectFirst("table.itg") + ?.selectFirst("tbody") + ?: if (updateDm) { + parseFailed("Cannot find root") + } else { + updateDm = true + return getList(offset, query, tags, sortOrder) + } + updateDm = false + return root.children().mapNotNull { tr -> + if (tr.childrenSize() != 2) return@mapNotNull null + val (td1, td2) = tr.children() + val glink = td2.selectFirst("div.glink") ?: parseFailed("glink not found") + val a = glink.parents().select("a").first() ?: parseFailed("link not found") + val href = a.relUrl("href") + val tagsDiv = glink.nextElementSibling() ?: parseFailed("tags div not found") + val mainTag = td2.selectFirst("div.cn")?.let { div -> + MangaTag( + title = div.text(), + key = tagIdByClass(div.classNames()) ?: return@let null, + source = source, + ) + } + Manga( + id = generateUid(href), + title = glink.text().cleanupTitle(), + altTitle = null, + url = href, + publicUrl = a.absUrl("href"), + rating = td2.selectFirst("div.ir")?.parseRating() ?: Manga.NO_RATING, + isNsfw = true, + coverUrl = td1.selectFirst("img")?.absUrl("src").orEmpty(), + tags = setOfNotNull(mainTag), + state = null, + author = tagsDiv.getElementsContainingOwnText("artist:").first() + ?.nextElementSibling()?.text(), + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = loaderContext.httpGet(manga.url.withDomain()).parseHtml() + val root = doc.body().selectFirst("div.gm") ?: parseFailed("Cannot find root") + val cover = root.getElementById("gd1")?.children()?.first() + val title = root.getElementById("gd2") + val taglist = root.getElementById("taglist") + val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr") + return manga.copy( + title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title, + altTitle = title?.getElementById("gj")?.text()?.cleanupTitle() ?: manga.altTitle, + publicUrl = doc.baseUri().ifEmpty { manga.publicUrl }, + rating = root.getElementById("rating_label")?.text() + ?.substringAfterLast(' ') + ?.toFloatOrNull() + ?.div(5f) ?: manga.rating, + largeCoverUrl = cover?.css("background")?.cssUrl(), + description = taglist?.select("tr")?.joinToString("
") { tr -> + val (tc, td) = tr.children() + val subtags = td.select("a").joinToString { it.html() } + "${tc.html()} $subtags" + }, + chapters = tabs?.select("a")?.findLast { a -> + a.text().toIntOrNull() != null + }?.let { a -> + val count = a.text().toInt() + val chapters = ArrayList(count) + for (i in 1..count) { + val url = "${manga.url}?p=$i" + chapters += MangaChapter( + id = generateUid(url), + name = "${manga.title} #$i", + number = i, + url = url, + branch = null, + source = source, + ) + } + chapters + }, + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = loaderContext.httpGet(chapter.url.withDomain()).parseHtml() + val root = doc.body().getElementById("gdt") ?: parseFailed("Root not found") + return root.select("a").mapNotNull { a -> + val url = a.relUrl("href") + MangaPage( + id = generateUid(url), + url = url, + referer = a.absUrl("href"), + preview = null, + source = source, + ) + } + } + + override suspend fun getPageUrl(page: MangaPage): String { + val doc = loaderContext.httpGet(page.url.withDomain()).parseHtml() + return doc.body().getElementById("img")?.absUrl("src") + ?: parseFailed("Image not found") + } + + override suspend fun getTags(): Set { + val doc = loaderContext.httpGet("https://${getDomain()}").parseHtml() + val root = doc.body().getElementById("searchbox")?.selectFirst("table") + ?: parseFailed("Root not found") + return root.select("div.cs").mapNotNullToSet { div -> + val id = div.id().substringAfterLast('_').toIntOrNull() + ?: return@mapNotNullToSet null + MangaTag( + title = div.text(), + key = id.toString(), + source = source + ) + } + } + + override fun isAuthorized(): Boolean { + val authorized = isAuthorized(DOMAIN_UNAUTHORIZED) + if (authorized) { + if (!isAuthorized(DOMAIN_AUTHORIZED)) { + loaderContext.cookieJar.copyCookies( + DOMAIN_UNAUTHORIZED, + DOMAIN_AUTHORIZED, + authCookies, + ) + loaderContext.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "yay=louder") + } + return true + } + return false + } + + private fun isAuthorized(domain: String): Boolean { + val cookies = loaderContext.cookieJar.getCookies(domain).mapToSet { x -> x.name } + return authCookies.all { it in cookies } + } + + private fun Element.parseRating(): Float { + return runCatching { + val style = requireNotNull(attr("style")) + val (v1, v2) = ratingPattern.find(style)!!.destructured + var p1 = v1.dropLast(2).toInt() + val p2 = v2.dropLast(2).toInt() + if (p2 != -1) { + p1 += 8 + } + (80 - p1) / 80f + }.getOrDefault(Manga.NO_RATING) + } + + private fun String.cleanupTitle(): String { + val result = StringBuilder(length) + var skip = false + for (c in this) { + when { + c == '[' -> skip = true + c == ']' -> skip = false + c.isWhitespace() && result.isEmpty() -> continue + !skip -> result.append(c) + } + } + while (result.lastOrNull()?.isWhitespace() == true) { + result.deleteCharAt(result.lastIndex) + } + return result.toString() + } + + private fun String.cssUrl(): String? { + val fromIndex = indexOf("url(") + if (fromIndex == -1) { + return null + } + val toIndex = indexOf(')', startIndex = fromIndex) + return if (toIndex == -1) { + null + } else { + substring(fromIndex + 4, toIndex).trim() + } + } + + private fun tagIdByClass(classNames: Collection): String? { + val className = classNames.find { x -> x.startsWith("ct") } ?: return null + val num = className.drop(2).toIntOrNull(16) ?: return null + return 2.0.pow(num).toInt().toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt index 7fea87b3c..bbb236521 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt @@ -16,7 +16,7 @@ abstract class NineMangaRepository( ) : RemoteMangaRepository(loaderContext) { init { - loaderContext.insertCookies(getDomain(), "ninemanga_template_desk=yes") + loaderContext.cookieJar.insertCookies(getDomain(), "ninemanga_template_desk=yes") } override val sortOrders: Set = EnumSet.of( diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CookieJarExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CookieJarExt.kt new file mode 100644 index 000000000..d188cadbe --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CookieJarExt.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.utils.ext + +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl + +private const val SCHEME_HTTPS = "https" + +fun CookieJar.insertCookies(domain: String, vararg cookies: String) { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(domain) + .build() + saveFromResponse(url, cookies.mapNotNull { + Cookie.parse(url, it) + }) +} + +fun CookieJar.getCookies(domain: String): List { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(domain) + .build() + return loadForRequest(url) +} + +fun CookieJar.copyCookies(oldDomain: String, newDomain: String, names: Array? = null) { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(oldDomain) + var cookies = loadForRequest(url.build()) + if (names != null) { + cookies = cookies.filter { c -> c.name in names } + } + url.host(newDomain) + saveFromResponse(url.build(), cookies) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt index c6e46b05e..7968b5ba2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt @@ -91,4 +91,10 @@ fun Element.relUrl(attributeKey: String): String { return attr.removePrefix(baseUrl.dropLast(1)) } -private val REGEX_URL_BASE = Regex("^[^/]{2,6}://[^/]+/", RegexOption.IGNORE_CASE) \ No newline at end of file +private val REGEX_URL_BASE = Regex("^[^/]{2,6}://[^/]+/", RegexOption.IGNORE_CASE) + +fun Element.css(property: String): String? { + val regex = Regex("${Regex.escape(property)}\\s*:\\s*[^;]+") + val css = attr("style").find(regex) ?: return null + return css.substringAfter(':').removeSuffix(';').trim() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index fbf8326ae..52f774e6f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -6,7 +6,6 @@ import java.math.BigInteger import java.net.URLEncoder import java.security.MessageDigest import java.util.* -import kotlin.contracts.contract import kotlin.math.min fun String.longHashCode(): Long { @@ -158,6 +157,13 @@ fun String.substringBetweenLast(from: String, to: String, fallbackValue: String fun String.find(regex: Regex) = regex.find(this)?.value +fun String.removeSuffix(suffix: Char): String { + if (lastOrNull() == suffix) { + return substring(0, length - 1) + } + return this +} + fun String.levenshteinDistance(other: String): Int { if (this == other) { return 0 From c1b6cef3621972889564f1bab627f07a37b5a63a Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 11 Sep 2021 13:50:47 +0300 Subject: [PATCH 127/138] Multiple tags support in (almost) all sources #19 --- .../kotatsu/core/model/MangaFilter.kt | 2 +- .../kotatsu/core/parser/MangaRepository.kt | 16 +----- .../core/parser/site/AnibelRepository.kt | 2 +- .../core/parser/site/ChanRepository.kt | 12 ++-- .../core/parser/site/DesuMeRepository.kt | 10 ++-- .../core/parser/site/ExHentaiRepository.kt | 11 +--- .../core/parser/site/GroupleRepository.kt | 56 ++++++++++++++++--- .../core/parser/site/HenChanRepository.kt | 8 +-- .../core/parser/site/MangaLibRepository.kt | 10 ++-- .../core/parser/site/MangaTownRepository.kt | 15 +++-- .../core/parser/site/MangareadRepository.kt | 11 +++- .../core/parser/site/NineMangaRepository.kt | 4 +- .../core/parser/site/RemangaRepository.kt | 12 ++-- .../kotatsu/list/ui/MangaListFragment.kt | 5 +- .../kotatsu/list/ui/filter/FilterAdapter.kt | 40 ++++++------- .../list/ui/filter/FilterSortHolder.kt | 4 +- .../kotatsu/list/ui/filter/FilterTagHolder.kt | 16 +++--- .../local/domain/LocalMangaRepository.kt | 6 +- .../kotatsu/local/ui/LocalListViewModel.kt | 2 +- .../remotelist/ui/RemoteListFragment.kt | 1 - .../remotelist/ui/RemoteListViewModel.kt | 4 +- .../search/domain/MangaSearchRepository.kt | 6 +- .../kotatsu/search/ui/SearchViewModel.kt | 5 +- .../koitharu/kotatsu/utils/ext/StringExt.kt | 20 +++++++ .../res/layout/item_checkable_multiple.xml | 13 +++++ .../main/res/layout/item_checkable_single.xml | 1 - .../core/parser/RemoteMangaRepositoryTest.kt | 12 ++-- 27 files changed, 187 insertions(+), 117 deletions(-) create mode 100644 app/src/main/res/layout/item_checkable_multiple.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt index 814c00571..498492f24 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt @@ -6,5 +6,5 @@ import kotlinx.parcelize.Parcelize @Parcelize data class MangaFilter( val sortOrder: SortOrder?, - val tag: MangaTag?, + val tags: Set, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt index fc960f069..c8904b2a8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -9,24 +9,12 @@ interface MangaRepository { val sortOrders: Set - suspend fun getList( + suspend fun getList2( offset: Int, query: String? = null, tags: Set? = null, sortOrder: SortOrder? = null, - ): List = if (tags == null || tags.size <= 1) { - getList(offset, query, sortOrder, tags?.singleOrNull()) - } else { - throw NotImplementedError("Multiple filter are not supported by this source yet") - } - - @Deprecated("Use multiple tag variant") - suspend fun getList( - offset: Int, - query: String? = null, - sortOrder: SortOrder? = null, - tag: MangaTag? = null, - ): List = throw NotImplementedError("This is fine") + ): List suspend fun getDetails(manga: Manga): Manga diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt index 6142f71d3..bb3622afc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt @@ -16,7 +16,7 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor SortOrder.NEWEST ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, tags: Set?, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt index 242686815..629082010 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt @@ -17,11 +17,11 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe SortOrder.ALPHABETICAL ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { val domain = getDomain() val url = when { @@ -31,7 +31,11 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe } "https://$domain/?do=search&subaction=search&story=${query.urlEncoded()}" } - tag != null -> "https://$domain/tags/${tag.key}&n=${getSortKey2(sortOrder)}?offset=$offset" + !tags.isNullOrEmpty() -> tags.joinToString( + prefix = "https://$domain/tags/", + postfix = "&n=${getSortKey2(sortOrder)}?offset=$offset", + separator = "+", + ) { tag -> tag.key } else -> "https://$domain/${getSortKey(sortOrder)}?offset=$offset" } val doc = loaderContext.httpGet(url).parseHtml() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt index 4d62b7af5..73b223b82 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt @@ -20,11 +20,11 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor SortOrder.ALPHABETICAL ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { if (query != null && offset != 0) { return emptyList() @@ -37,9 +37,9 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor append(getSortKey(sortOrder)) append("&page=") append((offset / 20) + 1) - if (tag != null) { + if (!tags.isNullOrEmpty()) { append("&genres=") - append(tag.key) + appendAll(tags, ",") { it.key } } if (query != null) { append("&search=") diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt index a699357e8..bef2af960 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt @@ -32,14 +32,7 @@ class ExHentaiRepository( loaderContext.cookieJar.insertCookies(DOMAIN_UNAUTHORIZED, "nw=1", "sl=dm_2") } - override suspend fun getList( - offset: Int, - query: String?, - sortOrder: SortOrder?, - tag: MangaTag?, - ): List = getList(offset, query, setOfNotNull(tag), sortOrder) - - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, tags: Set?, @@ -80,7 +73,7 @@ class ExHentaiRepository( parseFailed("Cannot find root") } else { updateDm = true - return getList(offset, query, tags, sortOrder) + return getList2(offset, query, tags, sortOrder) } updateDm = false return root.children().mapNotNull { tr -> diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt index 268ad03b0..2d35111e8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.core.parser.site import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Response import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.model.* @@ -18,11 +19,11 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : SortOrder.RATING ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { val domain = getDomain() val doc = when { @@ -33,22 +34,24 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : "offset" to (offset upBy PAGE_SIZE_SEARCH).toString() ) ) - tag == null -> loaderContext.httpGet( + tags.isNullOrEmpty() -> loaderContext.httpGet( "https://$domain/list?sortType=${ getSortKey( sortOrder ) }&offset=${offset upBy PAGE_SIZE}" ) - else -> loaderContext.httpGet( - "https://$domain/list/genre/${tag.key}?sortType=${ + tags.size == 1 -> loaderContext.httpGet( + "https://$domain/list/genre/${tags.first().key}?sortType=${ getSortKey( sortOrder ) }&offset=${offset upBy PAGE_SIZE}" ) - }.parseHtml() - val root = doc.body().getElementById("mangaBox") + offset > 0 -> return emptyList() + else -> advancedSearch(domain, tags) + }.parseHtml().body() + val root = (doc.getElementById("mangaBox") ?: doc.getElementById("mangaResults")) ?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root") val baseHost = root.baseUri().toHttpUrl().host return root.select("div.tile").mapNotNull { node -> @@ -182,6 +185,43 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : null -> "updated" } + private suspend fun advancedSearch(domain: String, tags: Set): Response { + val url = "https://$domain/search/advanced" + // Step 1: map catalog genres names to advanced-search genres ids + val tagsIndex = loaderContext.httpGet(url).parseHtml() + .body().selectFirst("form.search-form") + ?.select("div.form-group") + ?.get(1) ?: parseFailed("Genres filter element not found") + val tagNames = tags.map { it.title.lowercase() } + val payload = HashMap() + var foundGenres = 0 + tagsIndex.select("li.property").forEach { li -> + val name = li.text().trim().lowercase() + val id = li.selectFirst("input")?.id() + ?: parseFailed("Id for tag $name not found") + payload[id] = if (name in tagNames) { + foundGenres++ + "in" + } else "" + } + if (foundGenres != tags.size) { + parseFailed("Some genres are not found") + } + // Step 2: advanced search + payload["q"] = "" + payload["s_high_rate"] = "" + payload["s_single"] = "" + payload["s_mature"] = "" + payload["s_completed"] = "" + payload["s_translated"] = "" + payload["s_many_chapters"] = "" + payload["s_wait_upload"] = "" + payload["s_sale"] = "" + payload["years"] = "1900,2099" + payload["+"] = "Искать".urlEncoded() + return loaderContext.httpPost(url, payload) + } + private companion object { private const val PAGE_SIZE = 70 diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt index aaad60566..a358f50df 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt @@ -11,13 +11,13 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load override val defaultDomain = "hentaichan.live" override val source = MangaSource.HENCHAN - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { - return super.getList(offset, query, sortOrder, tag).map { + return super.getList2(offset, query, tags, sortOrder).map { val cover = it.coverUrl if (cover.contains("_blur")) { it.copy(coverUrl = cover.replace("_blur", "")) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt index e2d7fbc5f..ba0ea771d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt @@ -26,11 +26,11 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : SortOrder.NEWEST ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { if (!query.isNullOrEmpty()) { return if (offset == 0) search(query) else emptyList() @@ -43,8 +43,8 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : append(getSortKey(sortOrder)) append("&page=") append(page) - if (tag != null) { - append("&includeGenres[]=") + tags?.forEach { tag -> + append("&genres[include][]=") append(tag.key) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt index 44fd5937e..bf5ce1b8f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt @@ -23,11 +23,11 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : SortOrder.UPDATED ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { val sortKey = when (sortOrder) { SortOrder.ALPHABETICAL -> "?name.az" @@ -43,8 +43,13 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : } "/search?name=${query.urlEncoded()}".withDomain() } - tag != null -> "/directory/${tag.key}/$page.htm$sortKey".withDomain() - else -> "/directory/$page.htm$sortKey".withDomain() + tags.isNullOrEmpty() -> "/directory/$page.htm$sortKey".withDomain() + tags.size == 1 -> "/directory/${tags.first().key}/$page.htm$sortKey".withDomain() + else -> tags.joinToString( + prefix = "/search?page=$page".withDomain() + ) { tag -> + "&genres[${tag.key}]=1" + } } val doc = loaderContext.httpGet(url).parseHtml() val root = doc.body().selectFirst("ul.manga_pic_list") diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt index ee69048bb..0a94c3d04 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt @@ -20,12 +20,17 @@ class MangareadRepository( SortOrder.POPULARITY ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { + val tag = when { + tags.isNullOrEmpty() -> null + tags.size == 1 -> tags.first() + else -> throw NotImplementedError("Multiple genres are not supported by this source") + } val payload = createRequestTemplate() payload["page"] = (offset / PAGE_SIZE.toFloat()).toIntUp().toString() payload["vars[meta_key]"] = when (sortOrder) { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt index bbb236521..9c67b2146 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt @@ -23,7 +23,7 @@ abstract class NineMangaRepository( SortOrder.POPULARITY, ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, tags: Set?, @@ -146,7 +146,7 @@ abstract class NineMangaRepository( val cateId = li.attr("cate_id") ?: return@mapNotNullToSet null val a = li.selectFirst("a") ?: return@mapNotNullToSet null MangaTag( - title = a.text(), + title = a.text().toTitleCase(), key = cateId, source = source ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt index f368db41e..17a3dd5d7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt @@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.utils.ext.* import java.util.* -import kotlin.collections.ArrayList class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) { @@ -24,11 +23,11 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito SortOrder.NEWEST ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { val domain = getDomain() val urlBuilder = StringBuilder() @@ -40,8 +39,9 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito } else { urlBuilder.append("/api/search/catalog/?ordering=") .append(getSortKey(sortOrder)) - if (tag != null) { - urlBuilder.append("&genres=" + tag.key) + tags?.forEach { tag -> + urlBuilder.append("&genres=") + urlBuilder.append(tag.key) } } urlBuilder diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 066da26ec..7c06f6943 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -217,10 +217,7 @@ abstract class MangaListFragment : BaseFragment(), activity?.invalidateOptionsMenu() } - @CallSuper - override fun onFilterChanged(filter: MangaFilter) { - drawer?.closeDrawers() - } + override fun onFilterChanged(filter: MangaFilter) = Unit override fun onWindowInsetsChanged(insets: Insets) { val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt index e7fe78058..983fdf0f1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt @@ -6,19 +6,15 @@ import org.koitharu.kotatsu.base.ui.list.BaseViewHolder import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.SortOrder -import java.util.* class FilterAdapter( - sortOrders: List = emptyList(), - tags: List = emptyList(), + private val sortOrders: List = emptyList(), + private val tags: List = emptyList(), state: MangaFilter?, private val listener: OnFilterChangedListener ) : RecyclerView.Adapter>() { - private val sortOrders = ArrayList(sortOrders) - private val tags = ArrayList(Collections.singletonList(null) + tags) - - private var currentState = state ?: MangaFilter(sortOrders.firstOrNull(), null) + private var currentState = state ?: MangaFilter(sortOrders.firstOrNull(), emptySet()) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { VIEW_TYPE_SORT -> FilterSortHolder(parent).apply { @@ -28,7 +24,7 @@ class FilterAdapter( } VIEW_TYPE_TAG -> FilterTagHolder(parent).apply { itemView.setOnClickListener { - setCheckedTag(boundData) + setCheckedTag(boundData ?: return@setOnClickListener, !isChecked) } } else -> throw IllegalArgumentException("Unknown viewType $viewType") @@ -44,7 +40,7 @@ class FilterAdapter( } is FilterTagHolder -> { val item = tags[position - sortOrders.size] - holder.bind(item, item == currentState.tag) + holder.bind(item, item in currentState.tags) } } } @@ -54,19 +50,25 @@ class FilterAdapter( else -> VIEW_TYPE_TAG } - fun setCheckedTag(tag: MangaTag?) { - if (tag != currentState.tag) { - val oldItemPos = tags.indexOf(currentState.tag) - val newItemPos = tags.indexOf(tag) - currentState = currentState.copy(tag = tag) - if (oldItemPos in tags.indices) { - notifyItemChanged(sortOrders.size + oldItemPos) + fun setCheckedTag(tag: MangaTag, isChecked: Boolean) { + currentState = if (tag in currentState.tags) { + if (!isChecked) { + currentState.copy(tags = currentState.tags - tag) + } else { + return } - if (newItemPos in tags.indices) { - notifyItemChanged(sortOrders.size + newItemPos) + } else { + if (isChecked) { + currentState.copy(tags = currentState.tags + tag) + } else { + return } - listener.onFilterChanged(currentState) } + val index = tags.indexOf(tag) + if (index in tags.indices) { + notifyItemChanged(sortOrders.size + index) + } + listener.onFilterChanged(currentState) } fun setCheckedSort(sort: SortOrder) { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt index a182131b6..9275ae831 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt @@ -12,7 +12,7 @@ class FilterSortHolder(parent: ViewGroup) : ) { override fun onBind(data: SortOrder, extra: Boolean) { - binding.radio.setText(data.titleRes) - binding.radio.isChecked = extra + binding.root.setText(data.titleRes) + binding.root.isChecked = extra } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt index f3bf89635..2054d4cb9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt @@ -2,18 +2,20 @@ package org.koitharu.kotatsu.list.ui.filter import android.view.LayoutInflater import android.view.ViewGroup -import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.list.BaseViewHolder import org.koitharu.kotatsu.core.model.MangaTag -import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding +import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding class FilterTagHolder(parent: ViewGroup) : - BaseViewHolder( - ItemCheckableSingleBinding.inflate(LayoutInflater.from(parent.context), parent, false) + BaseViewHolder( + ItemCheckableMultipleBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) { - override fun onBind(data: MangaTag?, extra: Boolean) { - binding.radio.text = data?.title ?: context.getString(R.string.all) - binding.radio.isChecked = extra + val isChecked: Boolean + get() = binding.root.isChecked + + override fun onBind(data: MangaTag, extra: Boolean) { + binding.root.text = data.title + binding.root.isChecked = extra } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index 383c295bc..3d4c51571 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -25,11 +25,11 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { private val filenameFilter = CbzFilter() - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { require(offset == 0) { "LocalMangaRepository does not support pagination" diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 844cbcbbc..5c12ad115 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -61,7 +61,7 @@ class LocalListViewModel( launchLoadingJob(Dispatchers.Default) { try { listError.value = null - mangaList.value = repository.getList(0, tags = null) + mangaList.value = repository.getList2(0) } catch (e: Throwable) { listError.value = e } diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 2ecb7f7bf..1f9b543a6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -31,7 +31,6 @@ class RemoteListFragment : MangaListFragment() { override fun onFilterChanged(filter: MangaFilter) { viewModel.applyFilter(filter) - super.onFilterChanged(filter) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index aaf6ccaa3..870480e35 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -78,10 +78,10 @@ class RemoteListViewModel( loadingJob = launchLoadingJob(Dispatchers.Default) { try { listError.value = null - val list = repository.getList( + val list = repository.getList2( offset = if (append) mangaList.value?.size ?: 0 else 0, sortOrder = appliedFilter?.sortOrder, - tag = appliedFilter?.tag + tags = appliedFilter?.tags, ) if (!append) { mangaList.value = list diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index a58e5098c..d099f54c5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -29,7 +29,11 @@ class MangaSearchRepository( MangaProviderFactory.getSources(settings, includeHidden = false).asFlow() .flatMapMerge(concurrency) { source -> runCatching { - source.repository.getList(0, query, SortOrder.POPULARITY) + source.repository.getList2( + offset = 0, + query = query, + sortOrder = SortOrder.POPULARITY + ) }.getOrElse { emptyList() }.asFlow() diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt index 35df96c82..659e12082 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt @@ -71,10 +71,9 @@ class SearchViewModel( loadingJob = launchLoadingJob(Dispatchers.Default) { try { listError.value = null - val list = repository.getList( + val list = repository.getList2( offset = if (append) mangaList.value?.size ?: 0 else 0, - tags = null, - query = query + query = query, ) if (!append) { mangaList.value = list diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index 52f774e6f..05d310506 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -48,6 +48,10 @@ fun String.toCamelCase(): String { return result.toString() } +fun String.toTitleCase(): String { + return replaceFirstChar { x -> x.uppercase() } +} + fun String.transliterate(skipMissing: Boolean): String { val cyr = charArrayOf( 'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', @@ -200,4 +204,20 @@ fun String.levenshteinDistance(other: String): Int { } return cost[lhsLength - 1] +} + +inline fun StringBuilder.appendAll( + items: Iterable, + separator: CharSequence, + transform: (T) -> CharSequence = { it.toString() }, +) { + var isFirst = true + for (item in items) { + if (isFirst) { + isFirst = false + } else { + append(separator) + } + append(transform(item)) + } } \ No newline at end of file diff --git a/app/src/main/res/layout/item_checkable_multiple.xml b/app/src/main/res/layout/item_checkable_multiple.xml new file mode 100644 index 000000000..14b97b714 --- /dev/null +++ b/app/src/main/res/layout/item_checkable_multiple.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_checkable_single.xml b/app/src/main/res/layout/item_checkable_single.xml index af15894cd..12eb91d8d 100644 --- a/app/src/main/res/layout/item_checkable_single.xml +++ b/app/src/main/res/layout/item_checkable_single.xml @@ -2,7 +2,6 @@ Date: Sat, 11 Sep 2021 16:01:15 +0300 Subject: [PATCH 128/138] Show current filter in list header --- .../kotatsu/base/ui/widgets/ChipsView.kt | 17 +++++++++++++- .../kotatsu/list/ui/MangaListFragment.kt | 10 ++++++-- .../kotatsu/list/ui/MangaListViewModel.kt | 3 +++ .../list/ui/adapter/CurrentFilterAD.kt | 23 +++++++++++++++++++ .../list/ui/adapter/MangaListAdapter.kt | 6 ++++- .../list/ui/model/CurrentFilterModel.kt | 7 ++++++ .../remotelist/ui/RemoteListViewModel.kt | 19 ++++++++++++++- .../main/res/layout/item_current_filter.xml | 15 ++++++++++++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 10 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/model/CurrentFilterModel.kt create mode 100644 app/src/main/res/layout/item_current_filter.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt index 84a34261e..eb867ec0f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt @@ -22,12 +22,21 @@ class ChipsView @JvmOverloads constructor( private var chipOnClickListener = OnClickListener { onChipClickListener?.onChipClick(it as Chip, it.tag) } + private var chipOnCloseListener = OnClickListener { + onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag) + } var onChipClickListener: OnChipClickListener? = null set(value) { field = value val isChipClickable = value != null children.forEach { it.isClickable = isChipClickable } } + var onChipCloseClickListener: OnChipCloseClickListener? = null + set(value) { + field = value + val isCloseIconVisible = value != null + children.forEach { (it as? Chip)?.isCloseIconVisible = isCloseIconVisible } + } override fun requestLayout() { if (isLayoutSuppressedCompat) { @@ -69,7 +78,8 @@ class ChipsView @JvmOverloads constructor( val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip) chip.setChipDrawable(drawable) chip.setTextColor(ContextCompat.getColor(context, R.color.color_primary)) - chip.isCloseIconVisible = false + chip.isCloseIconVisible = onChipCloseClickListener != null + chip.setOnCloseIconClickListener(chipOnCloseListener) chip.setEnsureMinTouchTargetSize(false) chip.setOnClickListener(chipOnClickListener) addView(chip) @@ -96,4 +106,9 @@ class ChipsView @JvmOverloads constructor( fun onChipClick(chip: Chip, data: Any?) } + + fun interface OnChipCloseClickListener { + + fun onChipCloseClick(chip: Chip, data: Any?) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 7c06f6943..1fa44344c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -71,7 +71,13 @@ abstract class MangaListFragment : BaseFragment(), super.onViewCreated(view, savedInstanceState) drawer = binding.root as? DrawerLayout drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this, ::resolveException) + listAdapter = MangaListAdapter( + coil = get(), + lifecycleOwner = viewLifecycleOwner, + clickListener = this, + onRetryClick = ::resolveException, + onTagRemoveClick = viewModel::onRemoveFilterTag + ) paginationListener = PaginationScrollListener(4, this) with(binding.recyclerView) { setHasFixedSize(true) @@ -287,7 +293,7 @@ abstract class MangaListFragment : BaseFragment(), final override fun getSectionTitle(position: Int): CharSequence? { return when (binding.recyclerViewFilter.adapter?.getItemViewType(position)) { FilterAdapter.VIEW_TYPE_SORT -> getString(R.string.sort_order) - FilterAdapter.VIEW_TYPE_TAG -> getString(R.string.genre) + FilterAdapter.VIEW_TYPE_TAG -> getString(R.string.genres) else -> null } } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 0fa365629..3d94d1b65 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.list.ui.model.ListModel @@ -36,6 +37,8 @@ abstract class MangaListViewModel( } } + open fun onRemoveFilterTag(tag: MangaTag) = Unit + abstract fun onRefresh() abstract fun onRetry() diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt new file mode 100644 index 000000000..4848a27b8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.list.ui.adapter + +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.base.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.model.MangaTag +import org.koitharu.kotatsu.databinding.ItemCurrentFilterBinding +import org.koitharu.kotatsu.list.ui.model.CurrentFilterModel +import org.koitharu.kotatsu.list.ui.model.ListModel + +fun currentFilterAD( + onTagRemoveClick: (MangaTag) -> Unit, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemCurrentFilterBinding.inflate(inflater, parent, false) } +) { + + binding.chipsTags.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { chip, data -> + onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener) + } + + bind { + binding.chipsTags.setChips(item.chips) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 71b4c2064..cc4c80967 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -6,6 +6,7 @@ import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel @@ -17,7 +18,8 @@ class MangaListAdapter( coil: ImageLoader, lifecycleOwner: LifecycleOwner, clickListener: OnListItemClickListener, - onRetryClick: (Throwable) -> Unit + onRetryClick: (Throwable) -> Unit, + onTagRemoveClick: (MangaTag) -> Unit, ) : AsyncListDifferDelegationAdapter(DiffCallback()) { init { @@ -38,6 +40,7 @@ class MangaListAdapter( .addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(onRetryClick)) .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD()) .addDelegate(ITEM_TYPE_HEADER, listHeaderAD()) + .addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick)) } fun setItems(list: List, commitCallback: Runnable) { @@ -79,5 +82,6 @@ class MangaListAdapter( const val ITEM_TYPE_ERROR_FOOTER = 7 const val ITEM_TYPE_EMPTY = 8 const val ITEM_TYPE_HEADER = 9 + const val ITEM_TYPE_FILTER = 10 } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/CurrentFilterModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/CurrentFilterModel.kt new file mode 100644 index 000000000..32cebb25c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/CurrentFilterModel.kt @@ -0,0 +1,7 @@ +package org.koitharu.kotatsu.list.ui.model + +import org.koitharu.kotatsu.base.ui.widgets.ChipsView + +data class CurrentFilterModel( + val chips: Collection, +) : ListModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 870480e35..d8df693a3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -7,8 +7,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaFilter +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings @@ -40,8 +42,9 @@ class RemoteListViewModel( list == null -> listOf(LoadingState) list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_cross, R.string.nothing_found, R.string._empty)) else -> { - val result = ArrayList(list.size + 2) + val result = ArrayList(list.size + 3) result += headerModel + createFilterModel()?.let { result.add(it) } list.toUi(result, mode) when { error != null -> result += error.toErrorFooter() @@ -65,6 +68,16 @@ class RemoteListViewModel( loadList(append = !mangaList.value.isNullOrEmpty()) } + override fun onRemoveFilterTag(tag: MangaTag) { + val filter = appliedFilter ?: return + if (tag !in filter.tags) { + return + } + applyFilter( + filter.copy(tags = filter.tags - tag) + ) + } + fun loadNextPage() { if (hasNextPage.value && listError.value == null) { loadList(append = true) @@ -108,6 +121,10 @@ class RemoteListViewModel( } } + private fun createFilterModel() = appliedFilter?.run { + CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) }) + } + private fun loadFilter() { launchJob(Dispatchers.Default) { try { diff --git a/app/src/main/res/layout/item_current_filter.xml b/app/src/main/res/layout/item_current_filter.xml new file mode 100644 index 000000000..1014b0fdd --- /dev/null +++ b/app/src/main/res/layout/item_current_filter.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 35be37286..862eb79f5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -236,4 +236,5 @@ Авторизация выполнена Авторизация в %s не поддерживается Вы выйдете из всех источников, в которых Вы авторизованы + Жанры \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f82f4b5d9..e09f1ba22 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -239,4 +239,5 @@ Authorization complete Authorization on %s is not supported You will be logged out from all sources that you are authorized in + Genres \ No newline at end of file From f9cee7a8f58231b8a1970fc98c31b3bf0e3054c7 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 13 Sep 2021 08:36:05 +0300 Subject: [PATCH 129/138] Update gradle and dependencies --- app/build.gradle | 12 ++++++------ build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 7 ++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d69a4cef4..cb82e64e2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 30 - versionCode 367 - versionName '1.1.2' + versionCode 368 + versionName '2.0-a1' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -66,8 +66,8 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.activity:activity-ktx:1.3.1' @@ -82,7 +82,7 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.preference:preference-ktx:1.1.1' - implementation 'androidx.work:work-runtime-ktx:2.6.0-rc01' + implementation 'androidx.work:work-runtime-ktx:2.6.0' implementation 'com.google.android.material:material:1.4.0' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.3.1' @@ -108,7 +108,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.3' testImplementation 'org.json:json:20210307' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' testImplementation 'io.insert-koin:koin-test-junit4:3.1.2' androidTestImplementation 'androidx.test:runner:1.4.0' diff --git a/build.gradle b/build.gradle index cd35015e3..9fb759f76 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:7.0.2' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30' // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 821394777..24415a607 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,8 @@ -#Sat Jul 03 12:50:59 EEST 2021 +#Mon Sep 13 08:29:18 EEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME -distributionSha256Sum=765442b8069c6bee2ea70713861c027587591c6b1df2c857a23361512560894e +# https://gradle.org/release-checksums/ +distributionSha256Sum=0e46229820205440b48a5501122002842b82886e76af35f0f3a069243dca4b3c From 71f205ca8b409637e24115767b8b7fd47e88cd0d Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sun, 12 Sep 2021 15:37:41 +0300 Subject: [PATCH 130/138] Fix AMOLED theme --- .../koitharu/kotatsu/base/ui/BaseActivity.kt | 4 ++ .../koitharu/kotatsu/main/ui/MainActivity.kt | 47 +++++++++++++++---- app/src/main/res/drawable/tabs_background.xml | 2 +- app/src/main/res/values-night/colors.xml | 8 ++-- app/src/main/res/values-night/themes.xml | 4 +- .../main/res/values-notnight-v23/colors.xml | 5 ++ app/src/main/res/values/colors.xml | 15 ++++-- app/src/main/res/values/themes.xml | 6 +-- 8 files changed, 67 insertions(+), 24 deletions(-) create mode 100644 app/src/main/res/values-notnight-v23/colors.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt index 8fbaa7ad4..ff3d20ea9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.base.ui +import android.graphics.Color import android.os.Build import android.os.Bundle import android.view.KeyEvent @@ -57,6 +58,9 @@ abstract class BaseActivity : AppCompatActivity(), OnApplyWindo this.binding = binding super.setContentView(binding.root) val toolbar = (binding.root.findViewById(R.id.toolbar) as? Toolbar) + if (get().isAmoledTheme) { + toolbar?.setBackgroundColor(Color.BLACK) + } toolbar?.let(this::setSupportActionBar) ViewCompat.setOnApplyWindowInsetsListener(binding.root, this) diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 4272a2b5e..096481624 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -8,7 +8,7 @@ import android.os.Build import android.os.Bundle import android.view.MenuItem import android.view.View -import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat @@ -30,6 +30,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.prefs.AppSection +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.databinding.ActivityMainBinding import org.koitharu.kotatsu.databinding.NavigationHeaderBinding import org.koitharu.kotatsu.details.ui.DetailsActivity @@ -86,6 +87,13 @@ class MainActivity : BaseActivity(), binding.drawer.addDrawerListener(drawerToggle) supportActionBar?.setDisplayHomeAsUpEnabled(true) + if (get().isAmoledTheme) { + binding.appbar.setBackgroundColor(Color.BLACK) + binding.toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_background)) + } else { + binding.toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_surface)) + } + with(binding.searchView) { onFocusChangeListener = this@MainActivity searchSuggestionListener = this@MainActivity @@ -205,17 +213,15 @@ class MainActivity : BaseActivity(), } override fun onWindowInsetsChanged(insets: Insets) { - binding.toolbarCard.updateLayoutParams { + binding.toolbarCard.updateLayoutParams { topMargin = insets.top + resources.resolveDp(8) - leftMargin = insets.left + resources.resolveDp(16) - rightMargin = insets.right + resources.resolveDp(16) } - binding.fab.updateLayoutParams { + binding.fab.updateLayoutParams { bottomMargin = insets.bottom + topMargin leftMargin = insets.left + topMargin rightMargin = insets.right + topMargin } - binding.container.updateLayoutParams { + binding.container.updateLayoutParams { topMargin = -(binding.appbar.measureHeight()) } } @@ -337,19 +343,40 @@ class MainActivity : BaseActivity(), private fun onSearchOpened() { binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) drawerToggle.isDrawerIndicatorEnabled = false - // Avoiding shadows on the sides if the color is transparent, so we make the AppBarLayout white/dark - binding.appbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_on_secondary)) - binding.toolbarCard.cardElevation = 0f + // Avoiding shadows on the sides if the color is transparent, so we make the AppBarLayout white/grey/black + if (get().isAmoledTheme) { + binding.toolbar.setBackgroundColor(Color.BLACK) + } else { + binding.appbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_surface)) + } + binding.toolbarCard.apply { + cardElevation = 0f + // Remove margin + updateLayoutParams { + leftMargin = 0 + rightMargin = 0 + } + + } binding.appbar.elevation = searchViewElevation } private fun onSearchClosed() { binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) drawerToggle.isDrawerIndicatorEnabled = true + if (get().isAmoledTheme) { + binding.toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_background)) + } // Returning transparent color binding.appbar.setBackgroundColor(Color.TRANSPARENT) binding.appbar.elevation = 0f - binding.toolbarCard.cardElevation = searchViewElevation + binding.toolbarCard.apply { + cardElevation = searchViewElevation + updateLayoutParams { + leftMargin = resources.resolveDp(16) + rightMargin = resources.resolveDp(16) + } + } } private fun onFirstStart() { diff --git a/app/src/main/res/drawable/tabs_background.xml b/app/src/main/res/drawable/tabs_background.xml index d9df3de10..9743b040f 100644 --- a/app/src/main/res/drawable/tabs_background.xml +++ b/app/src/main/res/drawable/tabs_background.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android"> - + @android:color/black #2EFFFFFF - #2a2b2e + #272727 + #121212 #2EFFFFFF - #B3000000 - - + #B3121212 + @color/system_ui_scrim_dark #1fffffff diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index e4fc0c120..1c0c88d29 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -2,7 +2,9 @@