Compare commits

..
Author SHA1 Message Date
tarsin f695c4598a Bump Version
Releases / Build (push) Has been cancelled
2023-01-15 09:57:00 +08:00
tarsin eada1bc023 Avoid unnecessary overhead 2023-01-14 19:56:55 +08:00
FooIbar 0ebb55551c Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783) (#672)
* Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783)

Resolve #641

* fixup! Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783)
2023-01-14 17:19:48 +08:00
tarsin 20bce419c9 Fix GalleryDetailScene bindViewFirst 2023-01-14 12:19:58 +08:00
tarsin ca3fc370c3 Minor refactor 2023-01-14 12:11:27 +08:00
tarsin 94a8b78d1a Use typesafe request 2023-01-14 12:03:55 +08:00
tarsin 06e9ff6fcf Add comments for some dependencies 2023-01-14 11:49:29 +08:00
FooIbar 699e74656d Disable drag and swipe on "All" or "Default" label 2023-01-14 11:41:51 +08:00
tarsin b3ad6ef8b8 Move out DirectionalViewPager
We rewrite it with ViewPager2 later
2023-01-14 11:33:26 +08:00
tarsin 673c8e25f5 Cleanup dependencies 2023-01-14 11:27:05 +08:00
tarsin 333c8e812d Remove lifecycle annotation
We turned to viewbinding
2023-01-14 10:34:44 +08:00
tarsin f0f7a88cfb Kotlinize EhFilter 2023-01-14 10:28:47 +08:00
tarsin 0661d78bce Rename .java to .kt 2023-01-14 10:28:47 +08:00
tarsin dc4aece272 Inline EhCookieStore supertype 2023-01-14 10:21:59 +08:00
tarsin 57a312b0b4 Singletonize EhCookieStore 2023-01-14 10:11:49 +08:00
tarsin b9897ed6ac Rename .java to .kt 2023-01-14 10:11:49 +08:00
tarsin 733230f037 Convert EhCallback to kotlin 2023-01-14 10:04:55 +08:00
tarsin fc6ba3e6ad Rename .java to .kt 2023-01-14 10:04:55 +08:00
tarsin 2d7c539c5f Convert FavouriteScene to kotlin 2023-01-14 09:55:11 +08:00
tarsin 7f34965026 Rename .java to .kt 2023-01-14 09:55:11 +08:00
tarsin de8b915cff Convert GalleryAdapter to kotlin 2023-01-14 09:32:15 +08:00
tarsin d56040581d Rename .java to .kt 2023-01-14 09:32:15 +08:00
tarsin 9869b7bbb5 Convert GalleryCommentsScene to kotlin 2023-01-14 09:26:56 +08:00
tarsin 7df97c28c0 Rename .java to .kt 2023-01-14 09:26:56 +08:00
tarsin 018a559f78 Fix crash in grid mode 2023-01-13 16:49:52 +08:00
tarsin 3b32ad5fed Don't refresh fragment when reselected 2023-01-13 10:26:41 +08:00
tarsin f796d2a842 Tweak NavigationUI to disable multiple backstack and use custom anim 2023-01-13 10:13:10 +08:00
FooIbar 2f7e84e126 Refresh the item as well if the confirmation dialog is canceled 2023-01-12 17:31:28 +08:00
FooIbar 996ddbf3db fixup! Support drag reordering for downloads 2023-01-12 16:48:30 +08:00
FooIbar 7779ba2ac5 Add "All" in download labels
Resolve #412
2023-01-12 15:23:12 +08:00
tarsin 4dadeab762 Avoid querying database on Main Thread, GalleryDetailScene, #629 2023-01-12 12:06:10 +08:00
tarsin f4d3131ef3 Put history in IO coroutines, #629 2023-01-12 11:53:41 +08:00
renovate[bot] efa2ac803e Update dependency androidx.compose:compose-bom to v2023 2023-01-12 11:50:46 +08:00
renovate[bot] e0be9d46bd Update room_version to v2.5.0 2023-01-12 11:50:32 +08:00
tarsin 106b27dbec Finalize ViewBindingize GalleryDetailScene 2023-01-12 11:46:56 +08:00
tarsin f764567523 ViewBindingize GalleryDetailScene, step 6 2023-01-12 11:40:56 +08:00
tarsin 245f8669bc ViewBindingize GalleryDetailScene, step 5 2023-01-12 11:34:51 +08:00
tarsin b265d9bba6 ViewBindingize GalleryDetailScene, step 4 2023-01-12 11:28:12 +08:00
tarsin 72228574f9 ViewBindingize GalleryDetailScene, step 3 2023-01-12 11:16:32 +08:00
tarsin fba1ac3d90 ViewBindingize GalleryDetailScene, step 2 2023-01-12 11:11:32 +08:00
tarsin 4a73fb565e ViewBindingize GalleryDetailScene, step 1 2023-01-12 11:05:41 +08:00
tarsin 13011ed4af Kotlinize GalleryDetailScene 2023-01-12 10:32:49 +08:00
tarsin b2eccdefd3 Rename .java to .kt 2023-01-12 10:32:49 +08:00
tarsin dd2d6a977b Kotlinize GalleryHolder 2023-01-12 10:01:07 +08:00
tarsin 38cd1b6040 Rename .java to .kt 2023-01-12 10:01:07 +08:00
tarsin 1a28bbcde8 ViewBindinglize GalleryPreviewsScene 2023-01-12 09:57:54 +08:00
tarsin 91013daadc Kotlinize GalleryPreviewsScene 2023-01-12 09:50:09 +08:00
tarsin 1ad435e79d Rename .java to .kt 2023-01-12 09:50:09 +08:00
tarsin fd5c300c53 Remove unused navigateToTop 2023-01-12 09:39:57 +08:00
tarsin aa7d9cf29c Remove unused TransitionNameFactory 2023-01-12 09:38:14 +08:00
tarsin 8db1c3400e Kotlinize ProgressScene 2023-01-12 09:34:48 +08:00
tarsin e723336706 Rename .java to .kt 2023-01-12 09:34:48 +08:00
tarsin b5cd4c36ab Singletonize ItemTouchHelper 2023-01-12 09:22:35 +08:00
tarsin d1928f2647 Fixup! Support drag reordering for downloads, fix drag ripple 2023-01-12 09:12:37 +08:00
tarsin 61621ae00b Use compatible compose compiler 2023-01-12 08:56:46 +08:00
52 changed files with 5459 additions and 9459 deletions

No files matched your search

+21 -61
View File
@@ -33,7 +33,7 @@ android {
minSdkVersion 28
targetSdkVersion 33
versionCode 180030
versionName "1.8.5.3-alpha04"
versionName "1.8.5.3-alpha05"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
}
@@ -52,9 +52,7 @@ android {
jvmTarget = '19'
freeCompilerArgs += [
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive",
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
"-progressive"
]
}
@@ -95,105 +93,67 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.0-alpha02"
kotlinCompilerExtensionVersion = "1.4.0-dev-k1.8.0-33c0ad36f83"
}
namespace 'com.hippo.ehviewer'
}
dependencies {
def activity_version = "1.6.1"
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.activity:activity-compose:$activity_version"
implementation "androidx.annotation:annotation:1.5.0"
implementation "androidx.annotation:annotation-experimental:1.3.0"
implementation "androidx.appcompat:appcompat:1.7.0-alpha01"
def arch_core_version = "2.1.0"
implementation "androidx.arch.core:core-common:$arch_core_version"
implementation "androidx.arch.core:core-runtime:$arch_core_version"
// https://developer.android.com/jetpack/androidx/releases/activity
implementation "androidx.activity:activity-compose:1.6.1"
implementation "androidx.appcompat:appcompat:1.6.0"
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
implementation "androidx.browser:browser:1.4.0"
def collection_version = "1.3.0-alpha02"
implementation "androidx.collection:collection:$collection_version"
implementation "androidx.collection:collection-ktx:$collection_version"
implementation platform("androidx.compose:compose-bom:2022.12.00")
// https://developer.android.com/jetpack/androidx/releases/compose-material3
implementation platform("androidx.compose:compose-bom:2023.01.00")
implementation "androidx.compose.material:material-icons-extended"
implementation "androidx.compose.material3:material3"
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha05"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation "androidx.lifecycle:lifecycle-process:2.5.1"
def core_version = "1.9.0"
implementation "androidx.core:core-ktx:$core_version"
implementation "androidx.customview:customview:1.2.0-alpha02"
implementation "androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03"
def fragment_version = "1.6.0-alpha04"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
def lifecycle_version = "2.6.0-alpha04"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
implementation "androidx.loader:loader:1.1.0"
// https://developer.android.com/jetpack/androidx/releases/navigation
def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-compose:$nav_version"
// https://developer.android.com/jetpack/androidx/releases/paging
implementation "androidx.paging:paging-runtime-ktx:3.2.0-alpha03"
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
def room_version = "2.5.0-rc01"
implementation "androidx.room:room-runtime:$room_version"
// https://developer.android.com/jetpack/androidx/releases/room
def room_version = "2.5.0"
ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation 'androidx.startup:startup-runtime:1.2.0-alpha02'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
implementation "androidx.transition:transition-ktx:1.4.1"
implementation "androidx.vectordrawable:vectordrawable:1.2.0-beta01"
implementation "androidx.vectordrawable:vectordrawable-animated:1.1.0"
implementation 'androidx.viewpager:viewpager:1.1.0-alpha01'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'com.drakeet.drawer:drawer:1.0.3'
def accompanist_version = "0.28.0"
implementation "com.google.accompanist:accompanist-themeadapter-material3:$accompanist_version"
implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Dead Dependency
implementation 'com.github.tachiyomiorg:DirectionalViewPager:1.0.0' // Dead Dependency
// https://github.com/google/accompanist/releases
implementation "com.google.accompanist:accompanist-themeadapter-material3:0.28.0"
implementation 'com.google.android.material:material:1.8.0-rc01'
// https://square.github.io/okhttp/changelogs/changelog/
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp'
implementation 'com.squareup.okhttp3:okhttp-coroutines'
implementation 'dev.chrisbanes.insetter:insetter:0.6.1' // Dead Dependency
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
// https://coil-kt.github.io/coil/changelog/
implementation 'io.coil-kt:coil:2.2.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jsoup:jsoup:1.15.3'
// Tachiyomi Reader
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'dev.chrisbanes.insetter:insetter:0.6.1'
}
configurations.all {
+1 -1
View File
@@ -45,6 +45,6 @@ SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
add_subdirectory(libarchive)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c)
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifUtils.c)
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
+11
View File
@@ -0,0 +1,11 @@
#include <string.h>
#include <jni.h>
#define GIF_HEADER_87A "GIF87a"
#define GIF_HEADER_89A "GIF89a"
JNIEXPORT jboolean JNICALL
Java_com_hippo_Native_isGif(JNIEnv *env, jobject thiz, jobject buffer) {
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
}
@@ -0,0 +1,49 @@
package androidx.navigation.ui
import android.view.Menu
import android.view.MenuItem
import androidx.annotation.IdRes
import androidx.navigation.ActivityNavigator
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavOptions
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
if (navController.currentDestination?.matchDestination(item.itemId) == true) return false
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
is ActivityNavigator.Destination
) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(com.hippo.ehviewer.R.anim.scene_open_enter)
.setExitAnim(com.hippo.ehviewer.R.anim.scene_open_exit)
.setPopEnterAnim(com.hippo.ehviewer.R.anim.scene_close_enter)
.setPopExitAnim(com.hippo.ehviewer.R.anim.scene_close_exit)
}
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
builder.setPopUpTo(
navController.graph.findStartDestination().id,
inclusive = false,
saveState = false
)
}
val options = builder.build()
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.itemId, null, options)
// Return true only if the destination we've navigated to matches the MenuItem
navController.currentDestination?.matchDestination(item.itemId) == true
} catch (e: IllegalArgumentException) {
false
}
}
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,88 @@
@file:Suppress("SameParameterValue")
package coil.decode
import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.decodeHex
import okio.ForwardingSource
import okio.Source
/**
* A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's
* below a threshold.
*/
internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(delegate) {
// An intermediary buffer so we can read and alter the data before it's written to the destination.
private val buffer = Buffer()
override fun read(sink: Buffer, byteCount: Long): Long {
// Ensure our buffer has enough bytes to satisfy this read.
request(byteCount)
// Short circuit if there are no bytes in the buffer.
if (buffer.size == 0L) {
return if (byteCount == 0L) 0L else -1L
}
// Search through the buffer and rewrite any frame delays below the threshold.
var bytesWritten = 0L
while (true) {
val index = indexOf(FRAME_DELAY_START_MARKER)
if (index == -1L) break
// Write up until the end of the frame delay start marker.
bytesWritten += write(sink, index + FRAME_DELAY_START_MARKER_SIZE_BYTES)
// Check for the end of the graphics control extension block.
if (!request(5) || buffer[4] != 0.toByte()) continue
// Rewrite the frame delay if it is below the threshold.
// The frame delay is stored as two unsigned bytes in reverse order
// (i.e. the most significant bits are in the byte that's read second).
val frameDelay = (buffer[2].toUByte().toInt() shl 8) or buffer[1].toUByte().toInt()
if (frameDelay < MINIMUM_FRAME_DELAY) {
sink.writeByte(buffer[0].toInt())
sink.writeByte(DEFAULT_FRAME_DELAY)
sink.writeByte(0)
buffer.skip(3)
}
}
// Write anything left in the source.
if (bytesWritten < byteCount) {
bytesWritten += write(sink, byteCount - bytesWritten)
}
return if (bytesWritten == 0L) -1 else bytesWritten
}
private fun indexOf(bytes: ByteString): Long {
var index = -1L
while (true) {
index = buffer.indexOf(bytes[0], index + 1)
if (index == -1L) break
if (request(bytes.size.toLong()) && buffer.rangeEquals(index, bytes)) break
}
return index
}
private fun write(sink: Buffer, byteCount: Long): Long {
return buffer.read(sink, byteCount).coerceAtLeast(0)
}
private fun request(byteCount: Long): Boolean {
if (buffer.size >= byteCount) return true
val toRead = byteCount - buffer.size
return super.read(buffer, toRead) == toRead
}
private companion object {
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
// See: "Graphics Control Extension"
private val FRAME_DELAY_START_MARKER = "0021F904".decodeHex()
private const val FRAME_DELAY_START_MARKER_SIZE_BYTES = 4
private const val MINIMUM_FRAME_DELAY = 2
private const val DEFAULT_FRAME_DELAY = 10
}
}
+3
View File
@@ -18,6 +18,7 @@
package com.hippo
import java.io.FileDescriptor
import java.nio.ByteBuffer
object Native {
fun initialize() {
@@ -26,4 +27,6 @@ object Native {
@JvmStatic
external fun getFd(fd: FileDescriptor?): Int
external fun isGif(buffer: ByteBuffer): Boolean
}
@@ -281,7 +281,7 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
private set
@JvmStatic
val ehCookieStore by lazy { EhCookieStore(application) }
val ehCookieStore by lazy { EhCookieStore }
@JvmStatic
val ehProxySelector by lazy { EhProxySelector() }
@@ -1,116 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client;
import android.content.Context;
import androidx.annotation.NonNull;
import com.hippo.network.CookieRepository;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
public class EhCookieStore extends CookieRepository {
public static final String KEY_IPD_MEMBER_ID = "ipb_member_id";
public static final String KEY_IPD_PASS_HASH = "ipb_pass_hash";
public static final String KEY_IGNEOUS = "igneous";
public static final Cookie sTipsCookie =
new Cookie.Builder()
.name(EhConfig.KEY_CONTENT_WARNING)
.value(EhConfig.CONTENT_WARNING_NOT_SHOW)
.domain(EhUrl.DOMAIN_E)
.path("/")
.expiresAt(Long.MAX_VALUE)
.build();
public EhCookieStore(Context context) {
super(context, "okhttp3-cookie.db");
}
public static Cookie newCookie(Cookie cookie, String newDomain, boolean forcePersistent,
boolean forceLongLive, boolean forceNotHostOnly) {
Cookie.Builder builder = new Cookie.Builder();
builder.name(cookie.name());
builder.value(cookie.value());
if (forceLongLive) {
builder.expiresAt(Long.MAX_VALUE);
} else if (cookie.persistent()) {
builder.expiresAt(cookie.expiresAt());
} else if (forcePersistent) {
builder.expiresAt(Long.MAX_VALUE);
}
if (cookie.hostOnly() && !forceNotHostOnly) {
builder.hostOnlyDomain(newDomain);
} else {
builder.domain(newDomain);
}
builder.path(cookie.path());
if (cookie.secure()) {
builder.secure();
}
if (cookie.httpOnly()) {
builder.httpOnly();
}
return builder.build();
}
public void signOut() {
clear();
}
public boolean hasSignedIn() {
HttpUrl url = HttpUrl.parse(EhUrl.HOST_E);
return contains(url, KEY_IPD_MEMBER_ID) &&
contains(url, KEY_IPD_PASS_HASH);
}
@NonNull
@Override
public List<Cookie> loadForRequest(@NonNull HttpUrl url) {
List<Cookie> cookies = super.loadForRequest(url);
boolean checkTips = domainMatch(url, EhUrl.DOMAIN_E);
if (checkTips) {
List<Cookie> result = new ArrayList<>(cookies.size() + 1);
// Add all but skip some
for (Cookie cookie : cookies) {
String name = cookie.name();
if (EhConfig.KEY_CONTENT_WARNING.equals(name)) {
continue;
}
if (EhConfig.KEY_UCONFIG.equals(name)) {
continue;
}
result.add(cookie);
}
// Add some
result.add(sTipsCookie);
return Collections.unmodifiableList(result);
} else {
return cookies;
}
}
}
@@ -0,0 +1,247 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.EhApplication
import com.hippo.network.CookieDatabase
import com.hippo.network.CookieSet
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.Collections
import java.util.regex.Pattern
object EhCookieStore : CookieJar {
private val db: CookieDatabase = CookieDatabase(EhApplication.application, "okhttp3-cookie.db")
private val map: MutableMap<String, CookieSet> = db.allCookies
fun signOut() {
clear()
}
fun hasSignedIn(): Boolean {
val url = EhUrl.HOST_E.toHttpUrl()
return contains(url, KEY_IPD_MEMBER_ID) &&
contains(url, KEY_IPD_PASS_HASH)
}
const val KEY_IPD_MEMBER_ID = "ipb_member_id"
const val KEY_IPD_PASS_HASH = "ipb_pass_hash"
const val KEY_IGNEOUS = "igneous"
private val sTipsCookie: Cookie = Cookie.Builder()
.name(EhConfig.KEY_CONTENT_WARNING)
.value(EhConfig.CONTENT_WARNING_NOT_SHOW)
.domain(EhUrl.DOMAIN_E)
.path("/")
.expiresAt(Long.MAX_VALUE)
.build()
fun newCookie(
cookie: Cookie, newDomain: String, forcePersistent: Boolean,
forceLongLive: Boolean, forceNotHostOnly: Boolean
): Cookie {
val builder = Cookie.Builder()
builder.name(cookie.name)
builder.value(cookie.value)
if (forceLongLive) {
builder.expiresAt(Long.MAX_VALUE)
} else if (cookie.persistent) {
builder.expiresAt(cookie.expiresAt)
} else if (forcePersistent) {
builder.expiresAt(Long.MAX_VALUE)
}
if (cookie.hostOnly && !forceNotHostOnly) {
builder.hostOnlyDomain(newDomain)
} else {
builder.domain(newDomain)
}
builder.path(cookie.path)
if (cookie.secure) {
builder.secure()
}
if (cookie.httpOnly) {
builder.httpOnly()
}
return builder.build()
}
@Synchronized
fun addCookie(cookie: Cookie) {
// For cookie database
var toAdd: Cookie? = null
var toUpdate: Cookie? = null
var toRemove: Cookie? = null
var set = map[cookie.domain]
if (set == null) {
set = CookieSet()
map[cookie.domain] = set
}
if (cookie.expiresAt <= System.currentTimeMillis()) {
toRemove = set.remove(cookie)
// If the cookie is not persistent, it's not in database
if (toRemove != null && !toRemove.persistent) {
toRemove = null
}
} else {
toAdd = cookie
toUpdate = set.add(cookie)
// If the cookie is not persistent, it's not in database
if (!toAdd.persistent) toAdd = null
if (toUpdate != null && !toUpdate.persistent) toUpdate = null
// Remove the cookie if it updates to null
if (toAdd == null && toUpdate != null) {
toRemove = toUpdate
toUpdate = null
}
}
if (toRemove != null) {
db.remove(toRemove)
}
if (toAdd != null) {
if (toUpdate != null) {
db.update(toUpdate, toAdd)
} else {
db.add(toAdd)
}
}
}
fun getCookieHeader(url: HttpUrl): String {
val cookies = getCookies(url)
val cookieHeader = StringBuilder()
var i = 0
val size = cookies.size
while (i < size) {
if (i > 0) {
cookieHeader.append("; ")
}
val cookie = cookies[i]
cookieHeader.append(cookie.name).append('=').append(cookie.value)
i++
}
return cookieHeader.toString()
}
@Synchronized
fun getCookies(url: HttpUrl): List<Cookie> {
val accepted: MutableList<Cookie> = ArrayList()
val expired: MutableList<Cookie> = ArrayList()
for ((domain, cookieSet) in map) {
if (domainMatch(url, domain)) {
cookieSet[url, accepted, expired]
}
}
for (cookie in expired) {
if (cookie.persistent) {
db.remove(cookie)
}
}
// RFC 6265 Section-5.4 step 2, sort the cookie-list
// Cookies with longer paths are listed before cookies with shorter paths.
// Ignore creation-time, we don't store them.
accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }
return accepted
}
fun contains(url: HttpUrl, name: String?): Boolean {
for (cookie in getCookies(url)) {
if (cookie.name == name) {
return true
}
}
return false
}
/**
* Remove all cookies in this `CookieRepository`.
*/
@Synchronized
fun clear() {
map.clear()
db.clear()
}
@Synchronized
fun close() {
db.close()
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
addCookie(cookie)
}
}
/**
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
* of Android's private InetAddress#isNumeric API.
*
*
* This matches IPv6 addresses as a hex string containing at least one colon, and possibly
* including dots after the first colon. It matches IPv4 addresses as strings containing only
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
* verification).
*/
private val VERIFY_AS_IP_ADDRESS =
Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
/**
* Returns true if `host` is not a host name and might be an IP address.
*/
private fun verifyAsIpAddress(host: String): Boolean {
return VERIFY_AS_IP_ADDRESS.matcher(host).matches()
}
// okhttp3.Cookie.domainMatch(HttpUrl, String)
@JvmStatic
fun domainMatch(url: HttpUrl, domain: String?): Boolean {
val urlHost = url.host
return if (urlHost == domain) {
true // As in 'example.com' matching 'example.com'.
} else urlHost.endsWith(domain!!)
&& urlHost[urlHost.length - domain.length - 1] == '.' && !verifyAsIpAddress(
urlHost
)
// As in 'example.com' matching 'www.example.com'.
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookies = getCookies(url)
val checkTips = domainMatch(url, EhUrl.DOMAIN_E)
return if (checkTips) {
val result: MutableList<Cookie> = ArrayList(cookies.size + 1)
// Add all but skip some
for (cookie in cookies) {
val name = cookie.name
if (EhConfig.KEY_CONTENT_WARNING == name) {
continue
}
if (EhConfig.KEY_UCONFIG == name) {
continue
}
result.add(cookie)
}
// Add some
result.add(sTipsCookie)
Collections.unmodifiableList(result)
} else {
cookies
}
}
}
@@ -75,7 +75,7 @@ object EhEngine {
private const val SAD_PANDA_LENGTH = "9615"
private const val KOKOMADE_URL = "https://exhentai.org/img/kokomade.jpg"
private val MEDIA_TYPE_JPEG: MediaType = "image/jpeg".toMediaType()
private var sEhFilter = EhFilter.getInstance()
private var sEhFilter = EhFilter
private val okHttpClient = EhApplication.okHttpClient
private fun transformException(code: Int, headers: Headers?, body: String?, e: Throwable?) {
@@ -1,315 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client;
import android.util.Log;
import androidx.annotation.NonNull;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.Filter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class EhFilter {
public static final int MODE_TITLE = 0;
public static final int MODE_UPLOADER = 1;
public static final int MODE_TAG = 2;
public static final int MODE_TAG_NAMESPACE = 3;
public static final int MODE_COMMENTER = 4;
public static final int MODE_COMMENT = 5;
private static final String TAG = EhFilter.class.getSimpleName();
private static EhFilter sInstance;
private final List<Filter> mTitleFilterList = new ArrayList<>();
private final List<Filter> mUploaderFilterList = new ArrayList<>();
private final List<Filter> mTagFilterList = new ArrayList<>();
private final List<Filter> mTagNamespaceFilterList = new ArrayList<>();
private final List<Filter> mCommenterFilterList = new ArrayList<>();
private final List<Filter> mCommentFilterList = new ArrayList<>();
private EhFilter() {
List<Filter> list = EhDB.getAllFilter();
for (int i = 0, n = list.size(); i < n; i++) {
Filter filter = list.get(i);
switch (filter.mode) {
case MODE_TITLE -> {
filter.text = filter.text.toLowerCase();
mTitleFilterList.add(filter);
}
case MODE_TAG -> {
filter.text = filter.text.toLowerCase();
mTagFilterList.add(filter);
}
case MODE_TAG_NAMESPACE -> {
filter.text = filter.text.toLowerCase();
mTagNamespaceFilterList.add(filter);
}
case MODE_UPLOADER -> mUploaderFilterList.add(filter);
case MODE_COMMENTER -> mCommenterFilterList.add(filter);
case MODE_COMMENT -> mCommentFilterList.add(filter);
default -> Log.d(TAG, "Unknown mode: " + filter.mode);
}
}
}
public static @NonNull EhFilter getInstance() {
if (sInstance == null) {
sInstance = new EhFilter();
}
return sInstance;
}
public List<Filter> getTitleFilterList() {
return mTitleFilterList;
}
public List<Filter> getUploaderFilterList() {
return mUploaderFilterList;
}
public List<Filter> getTagFilterList() {
return mTagFilterList;
}
public List<Filter> getTagNamespaceFilterList() {
return mTagNamespaceFilterList;
}
public List<Filter> getCommenterFilterList() {
return mCommenterFilterList;
}
public List<Filter> getCommentFilterList() {
return mCommentFilterList;
}
public synchronized boolean addFilter(Filter filter) {
// enable filter by default before it is added to database
filter.enable = true;
if (!EhDB.addFilter(filter)) return false;
switch (filter.mode) {
case MODE_TITLE -> {
filter.text = filter.text.toLowerCase();
mTitleFilterList.add(filter);
}
case MODE_TAG -> {
filter.text = filter.text.toLowerCase();
mTagFilterList.add(filter);
}
case MODE_TAG_NAMESPACE -> {
filter.text = filter.text.toLowerCase();
mTagNamespaceFilterList.add(filter);
}
case MODE_UPLOADER -> mUploaderFilterList.add(filter);
case MODE_COMMENTER -> mCommenterFilterList.add(filter);
case MODE_COMMENT -> mCommentFilterList.add(filter);
default -> Log.d(TAG, "Unknown mode: " + filter.mode);
}
return true;
}
public synchronized void triggerFilter(Filter filter) {
EhDB.triggerFilter(filter);
}
public synchronized void deleteFilter(Filter filter) {
EhDB.deleteFilter(filter);
switch (filter.mode) {
case MODE_TITLE -> mTitleFilterList.remove(filter);
case MODE_TAG -> mTagFilterList.remove(filter);
case MODE_TAG_NAMESPACE -> mTagNamespaceFilterList.remove(filter);
case MODE_UPLOADER -> mUploaderFilterList.remove(filter);
case MODE_COMMENTER -> mCommenterFilterList.remove(filter);
case MODE_COMMENT -> mCommentFilterList.remove(filter);
default -> Log.d(TAG, "Unknown mode: " + filter.mode);
}
}
public synchronized boolean needTags() {
return 0 != mTagFilterList.size() || 0 != mTagNamespaceFilterList.size();
}
public synchronized boolean filterTitle(GalleryInfo info) {
if (null == info) {
return false;
}
// Title
String title = info.getTitle();
List<Filter> filters = mTitleFilterList;
if (null != title && filters.size() > 0) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && title.toLowerCase().contains(filters.get(i).text)) {
return false;
}
}
}
return true;
}
public synchronized boolean filterUploader(GalleryInfo info) {
if (null == info) {
return false;
}
// Uploader
String uploader = info.getUploader();
List<Filter> filters = mUploaderFilterList;
if (null != uploader && filters.size() > 0) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && uploader.equals(filters.get(i).text)) {
return false;
}
}
}
return true;
}
private boolean matchTag(String tag, String filter) {
if (null == tag || null == filter) {
return false;
}
String tagNamespace;
String tagName;
String filterNamespace;
String filterName;
int index = tag.indexOf(':');
if (index < 0) {
tagNamespace = null;
tagName = tag;
} else {
tagNamespace = tag.substring(0, index);
tagName = tag.substring(index + 1);
}
index = filter.indexOf(':');
if (index < 0) {
filterNamespace = null;
filterName = filter;
} else {
filterNamespace = filter.substring(0, index);
filterName = filter.substring(index + 1);
}
if (null != tagNamespace && null != filterNamespace &&
!tagNamespace.equals(filterNamespace)) {
return false;
}
return tagName.equals(filterName);
}
public synchronized boolean filterTag(GalleryInfo info) {
if (null == info) {
return false;
}
// Tag
String[] tags = info.getSimpleTags();
List<Filter> filters = mTagFilterList;
if (null != tags && filters.size() > 0) {
for (String tag : tags) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && matchTag(tag, filters.get(i).text)) {
return false;
}
}
}
}
return true;
}
private boolean matchTagNamespace(String tag, String filter) {
if (null == tag || null == filter) {
return false;
}
String tagNamespace;
int index = tag.indexOf(':');
if (index >= 0) {
tagNamespace = tag.substring(0, index);
return tagNamespace.equals(filter);
} else {
return false;
}
}
public synchronized boolean filterTagNamespace(GalleryInfo info) {
if (null == info) {
return false;
}
String[] tags = info.getSimpleTags();
List<Filter> filters = mTagNamespaceFilterList;
if (null != tags && filters.size() > 0) {
for (String tag : tags) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && matchTagNamespace(tag, filters.get(i).text)) {
return false;
}
}
}
}
return true;
}
public synchronized boolean filterCommenter(String commenter) {
if (null == commenter) {
return false;
}
List<Filter> filters = mCommenterFilterList;
if (filters.size() > 0) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && commenter.equals(filters.get(i).text)) {
return false;
}
}
}
return true;
}
public synchronized boolean filterComment(String comment) {
if (null == comment) {
return false;
}
List<Filter> filters = mCommentFilterList;
if (filters.size() > 0) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable) {
Pattern p = Pattern.compile(filters.get(i).text);
Matcher m = p.matcher(comment);
if (m.find()) return false;
}
}
}
return true;
}
}
@@ -0,0 +1,315 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import android.util.Log
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.Filter
import java.util.Locale
import java.util.regex.Pattern
object EhFilter {
private val mTitleFilterList: MutableList<Filter> = ArrayList()
private val mUploaderFilterList: MutableList<Filter> = ArrayList()
private val mTagFilterList: MutableList<Filter> = ArrayList()
private val mTagNamespaceFilterList: MutableList<Filter> = ArrayList()
private val mCommenterFilterList: MutableList<Filter> = ArrayList()
private val mCommentFilterList: MutableList<Filter> = ArrayList()
const val MODE_TITLE = 0
const val MODE_UPLOADER = 1
const val MODE_TAG = 2
const val MODE_TAG_NAMESPACE = 3
const val MODE_COMMENTER = 4
const val MODE_COMMENT = 5
private val TAG = EhFilter::class.java.simpleName
init {
val list = EhDB.getAllFilter()
var i = 0
val n = list.size
while (i < n) {
val filter = list[i]
when (filter.mode) {
MODE_TITLE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTitleFilterList.add(filter)
}
MODE_TAG -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagFilterList.add(filter)
}
MODE_TAG_NAMESPACE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagNamespaceFilterList.add(filter)
}
MODE_UPLOADER -> mUploaderFilterList.add(filter)
MODE_COMMENTER -> mCommenterFilterList.add(filter)
MODE_COMMENT -> mCommentFilterList.add(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
i++
}
}
val titleFilterList: List<Filter>
get() = mTitleFilterList
val uploaderFilterList: List<Filter>
get() = mUploaderFilterList
val tagFilterList: List<Filter>
get() = mTagFilterList
val tagNamespaceFilterList: List<Filter>
get() = mTagNamespaceFilterList
val commenterFilterList: List<Filter>
get() = mCommenterFilterList
val commentFilterList: List<Filter>
get() = mCommentFilterList
@Synchronized
fun addFilter(filter: Filter): Boolean {
// enable filter by default before it is added to database
filter.enable = true
if (!EhDB.addFilter(filter)) return false
when (filter.mode) {
MODE_TITLE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTitleFilterList.add(filter)
}
MODE_TAG -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagFilterList.add(filter)
}
MODE_TAG_NAMESPACE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagNamespaceFilterList.add(filter)
}
MODE_UPLOADER -> mUploaderFilterList.add(filter)
MODE_COMMENTER -> mCommenterFilterList.add(filter)
MODE_COMMENT -> mCommentFilterList.add(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
return true
}
@Synchronized
fun triggerFilter(filter: Filter?) {
EhDB.triggerFilter(filter)
}
@Synchronized
fun deleteFilter(filter: Filter) {
EhDB.deleteFilter(filter)
when (filter.mode) {
MODE_TITLE -> mTitleFilterList.remove(filter)
MODE_TAG -> mTagFilterList.remove(filter)
MODE_TAG_NAMESPACE -> mTagNamespaceFilterList.remove(filter)
MODE_UPLOADER -> mUploaderFilterList.remove(filter)
MODE_COMMENTER -> mCommenterFilterList.remove(filter)
MODE_COMMENT -> mCommentFilterList.remove(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
}
@Synchronized
fun needTags(): Boolean {
return 0 != mTagFilterList.size || 0 != mTagNamespaceFilterList.size
}
@Synchronized
fun filterTitle(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Title
val title = info.title
val filters: List<Filter> = mTitleFilterList
if (null != title && filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && title.lowercase(Locale.getDefault()).contains(
filters[i].text!!
)
) {
return false
}
i++
}
}
return true
}
@Synchronized
fun filterUploader(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Uploader
val uploader = info.uploader
val filters: List<Filter> = mUploaderFilterList
if (null != uploader && filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && uploader == filters[i].text) {
return false
}
i++
}
}
return true
}
private fun matchTag(tag: String?, filter: String?): Boolean {
if (null == tag || null == filter) {
return false
}
val tagNamespace: String?
val tagName: String
val filterNamespace: String?
val filterName: String
var index = tag.indexOf(':')
if (index < 0) {
tagNamespace = null
tagName = tag
} else {
tagNamespace = tag.substring(0, index)
tagName = tag.substring(index + 1)
}
index = filter.indexOf(':')
if (index < 0) {
filterNamespace = null
filterName = filter
} else {
filterNamespace = filter.substring(0, index)
filterName = filter.substring(index + 1)
}
return if (null != tagNamespace && null != filterNamespace &&
tagNamespace != filterNamespace
) {
false
} else tagName == filterName
}
@Synchronized
fun filterTag(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Tag
val tags = info.simpleTags
val filters: List<Filter> = mTagFilterList
if (null != tags && filters.isNotEmpty()) {
for (tag in tags) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && matchTag(tag, filters[i].text)) {
return false
}
i++
}
}
}
return true
}
private fun matchTagNamespace(tag: String?, filter: String?): Boolean {
if (null == tag || null == filter) {
return false
}
val tagNamespace: String
val index = tag.indexOf(':')
return if (index >= 0) {
tagNamespace = tag.substring(0, index)
tagNamespace == filter
} else {
false
}
}
@Synchronized
fun filterTagNamespace(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
val tags = info.simpleTags
val filters: List<Filter> = mTagNamespaceFilterList
if (null != tags && filters.isNotEmpty()) {
for (tag in tags) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && matchTagNamespace(tag, filters[i].text)) {
return false
}
i++
}
}
}
return true
}
@Synchronized
fun filterCommenter(commenter: String?): Boolean {
if (null == commenter) {
return false
}
val filters: List<Filter> = mCommenterFilterList
if (filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && commenter == filters[i].text) {
return false
}
i++
}
}
return true
}
@Synchronized
fun filterComment(comment: String?): Boolean {
if (null == comment) {
return false
}
val filters: List<Filter> = mCommentFilterList
if (filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!!) {
val p = Pattern.compile(filters[i].text!!)
val m = p.matcher(comment)
if (m.find()) return false
}
i++
}
}
return true
}
}
@@ -31,7 +31,7 @@ class EhRequest {
get() = job?.isActive ?: false
var method = 0
private set
var args: Array<out Any>? = null
var args: Array<out Any?>? = null
private set
var callback: EhClient.Callback<Any?>? = null
private set
@@ -41,7 +41,7 @@ class EhRequest {
return this
}
fun setArgs(vararg args: Any): EhRequest {
fun setArgs(vararg args: Any?): EhRequest {
this.args = args
return this
}
@@ -477,7 +477,7 @@ public class GalleryDetailParser {
comment.comment = JsoupUtils.getElementByClass(element, "c6").html();
// filter comment
if (!comment.uploader) {
EhFilter sEhFilter = EhFilter.getInstance();
EhFilter sEhFilter = EhFilter.INSTANCE;
if (!sEhFilter.filterCommenter(comment.user) || !sEhFilter.filterComment(comment.comment)) {
return null;
}
@@ -158,6 +158,11 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
return mLabelList;
}
@NonNull
public List<DownloadInfo> getAllDownloadInfoList() {
return mAllInfoList;
}
@NonNull
public List<DownloadInfo> getDefaultDownloadInfoList() {
return mDefaultInfoList;
@@ -34,13 +34,17 @@ import androidx.activity.OnBackPressedCallback
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.view.GravityCompat
import androidx.customview.widget.Openable
import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.onNavDestinationSelected2
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import com.hippo.app.BaseDialogBuilder
import com.hippo.app.EditTextDialogBuilder
@@ -57,6 +61,7 @@ import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.ehviewer.ui.scene.GalleryDetailScene
import com.hippo.ehviewer.ui.scene.GalleryListScene
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.ehviewer.ui.scene.ProgressScene
import com.hippo.io.UniFileInputStreamPipe
import com.hippo.unifile.UniFile
@@ -181,7 +186,7 @@ class MainActivity : EhActivity() {
builder.keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
navController.navigate(
R.id.galleryListScene,
GalleryListScene.getStartArgs(builder)
builder.toStartArgs()
)
return true
} else if (type != null && type.startsWith("image/")) {
@@ -196,7 +201,7 @@ class MainActivity : EhActivity() {
builder.isUseSimilarityScan = true
navController.navigate(
R.id.galleryListScene,
GalleryListScene.getStartArgs(builder)
builder.toStartArgs()
)
return true
}
@@ -231,6 +236,26 @@ class MainActivity : EhActivity() {
}
binding.drawView.addDrawerListener(mDrawerOnBackPressedCallback)
binding.navView.setupWithNavController(navController)
// Trick: Tweak NavigationUI to disable multiple backstack
binding.navView.setNavigationItemSelectedListener {
val navigationView = binding.navView
val handled = onNavDestinationSelected2(it, navController)
if (handled) {
val parent = navigationView.parent
if (parent is Openable) {
parent.close()
} else {
val bottomSheetBehavior = NavigationUI.findBottomSheetBehavior(navigationView)
if (bottomSheetBehavior != null) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}
}
}
handled
}
// Trick End
if (savedInstanceState == null) {
if (intent.action != Intent.ACTION_MAIN) {
onNewIntent(intent)
@@ -1,37 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.annotation;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created in {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)},
* destroyed in {@link Fragment#onDestroyView()}.
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
public @interface DrawerLifeCircle {
}
@@ -1,37 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.annotation;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created in {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)},
* destroyed in {@link Fragment#onDestroyView()}.
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
public @interface ViewLifeCircle {
}
@@ -1,36 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.annotation;
import android.app.Activity;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created in {@link Fragment#onCreate(Bundle)} or {@link Activity#onCreate(Bundle)},
* destroyed in {@link Fragment#onDestroy()} or {@link Activity#onDestroy()}.
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
public @interface WholeLifeCircle {
}
@@ -49,16 +49,14 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhClient.METHOD_GET_PROFILE
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
import com.hippo.util.ExceptionUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.Cookie
@@ -105,7 +103,7 @@ fun CookieSignInScene(navController: NavController) {
}
fun login() {
signInJob?.cancel()
if (signInJob?.isActive == true) return
if (ipbMemberId.isBlank()) {
ipbMemberIdErrorState = true
return
@@ -123,18 +121,16 @@ fun CookieSignInScene(navController: NavController) {
isProgressIndicatorVisible = true
signInJob = coroutineScope.launchIO {
runCatching {
EhClient.execute(METHOD_GET_PROFILE)
EhEngine.getProfile()
}.onSuccess {
withUIContext {
navController.navigate(SELECT_SITE_ROUTE_NAME)
}
}.onFailure {
EhApplication.ehCookieStore.signOut()
if (it !is CancellationException) {
loginErrorException = it
showErrorDialog = true
isProgressIndicatorVisible = false
}
loginErrorException = it
showErrorDialog = true
isProgressIndicatorVisible = false
}
}
}
@@ -34,9 +34,8 @@ import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.parser.ProfileParser
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.GlobalScope
@@ -135,7 +134,7 @@ fun SelectSiteScreen(finish: () -> Unit) {
suspend fun getProfile() {
runCatching {
(EhClient.execute(EhClient.METHOD_GET_PROFILE) as ProfileParser.Result).run {
EhEngine.getProfile().run {
Settings.putDisplayName(displayName)
Settings.putAvatar(avatar)
}
@@ -55,7 +55,7 @@ import androidx.navigation.NavController
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.UrlOpener
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.LoginActivity.Companion.COOKIE_SIGN_IN_ROUTE_NAME
@@ -104,9 +104,7 @@ fun SignInScreen(navController: NavController) {
EhUtils.signOut()
signInJob = coroutineScope.launchIO {
runCatching {
(EhClient.execute(EhClient.METHOD_SIGN_IN, username, password) as String).let {
Settings.putDisplayName(it)
}
EhEngine.signIn(username, password)
}.onFailure {
it.printStackTrace()
withUIContext {
@@ -259,7 +259,7 @@ class FilterFragment : BaseFragment() {
}
private inner class FilterList {
private val mEhFilter: EhFilter = EhFilter.getInstance()
private val mEhFilter: EhFilter = EhFilter
private val mTitleFilterList: List<Filter> = mEhFilter.titleFilterList
private val mUploaderFilterList: List<Filter> = mEhFilter.uploaderFilterList
private val mTagFilterList: List<Filter> = mEhFilter.tagFilterList
@@ -407,11 +407,11 @@ class FilterFragment : BaseFragment() {
throw IndexOutOfBoundsException()
}
fun add(filter: Filter?): Boolean {
fun add(filter: Filter): Boolean {
return mEhFilter.addFilter(filter)
}
fun delete(filter: Filter?) {
fun delete(filter: Filter) {
mEhFilter.deleteFilter(filter)
}
@@ -236,17 +236,6 @@ abstract class BaseScene : Fragment() {
NavHostFragment.findNavController(this).navigate(id, args, options)
}
fun navigateToTop() {
val navController = NavHostFragment.findNavController(this)
val id = navController.graph.startDestinationId
val options: NavOptions = NavOptions.Builder()
.setEnterAnim(R.anim.scene_open_enter).setExitAnim(R.anim.scene_open_exit)
.setPopEnterAnim(R.anim.scene_close_enter).setPopExitAnim(R.anim.scene_close_exit)
.setPopUpTo(id, true)
.build()
navController.navigate(id, null, options)
}
companion object {
const val LENGTH_SHORT = 0
const val LENGTH_LONG = 1
@@ -34,7 +34,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
@@ -112,8 +111,9 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
private fun initLabels() {
context ?: return
val listLabel = downloadManager.labelList
mLabels = ArrayList(listLabel.size + 1)
// Add default label name
mLabels = ArrayList(listLabel.size + LABEL_OFFSET)
// Add "All" and "Default" label names
mLabels.add(getString(R.string.download_all))
mLabels.add(getString(R.string.default_download_label_name))
for ((_, label) in listLabel) {
mLabels.add(label!!)
@@ -185,12 +185,14 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
}
var list: MutableList<DownloadInfo>?
if (mLabel == null) {
list = mDownloadManager!!.allDownloadInfoList
} else if (mLabel == getString(R.string.default_download_label_name)) {
list = mDownloadManager!!.defaultDownloadInfoList
} else {
list = mDownloadManager!!.getLabelDownloadInfoList(mLabel)
if (list == null) {
mLabel = null
list = mDownloadManager!!.defaultDownloadInfoList
list = mDownloadManager!!.allDownloadInfoList
}
}
if (mType != -1) {
@@ -213,7 +215,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
setTitle(
getString(
R.string.scene_download_title,
if (mLabel != null) mLabel else getString(R.string.default_download_label_name)
if (mLabel != null) mLabel else getString(R.string.download_all)
)
)
}
@@ -286,7 +288,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
mRecyclerView!!.scrollToPosition(mInitPosition)
mInitPosition = -1
}
val itemTouchHelper = ItemTouchHelper(DownloadItemTouchHelperCallback())
itemTouchHelper.attachToRecyclerView(mRecyclerView)
mFastScroller!!.attachToRecyclerView(mRecyclerView)
val handlerDrawable = HandlerDrawable()
@@ -634,10 +635,10 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
if (mList !== list) {
if (mLabel != null && mList !== list) {
return
}
val index = list.indexOf(info)
val index = mList!!.indexOf(info)
if (index >= 0 && mAdapter != null) {
mAdapter!!.notifyItemChanged(index)
}
@@ -785,7 +786,8 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
private inner class DownloadLabelAdapter(private val mInflater: LayoutInflater) :
RecyclerView.Adapter<DownloadLabelHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadLabelHolder {
val holder = DownloadLabelHolder(mInflater.inflate(R.layout.item_drawer_list, parent, false))
val holder =
DownloadLabelHolder(mInflater.inflate(R.layout.item_drawer_list, parent, false))
holder.itemView.setOnClickListener {
val position = holder.bindingAdapterPosition
val label1: String? = if (position == 0) {
@@ -827,10 +829,18 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
}
var list: List<DownloadInfo?>? = null
if (mDownloadManager != null) {
list = if (position == 0) {
mDownloadManager!!.defaultDownloadInfoList
} else {
mDownloadManager!!.getLabelDownloadInfoList(label)
list = when (position) {
0 -> {
mDownloadManager!!.allDownloadInfoList
}
1 -> {
mDownloadManager!!.defaultDownloadInfoList
}
else -> {
mDownloadManager!!.getLabelDownloadInfoList(label)
}
}
}
if (list != null) {
@@ -838,17 +848,17 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
} else {
holder.label.text = label
}
if (position > 0) {
holder.edit.visibility = View.VISIBLE
holder.option.visibility = View.VISIBLE
} else {
if (position < LABEL_OFFSET) {
holder.edit.visibility = View.GONE
holder.option.visibility = View.GONE
} else {
holder.edit.visibility = View.VISIBLE
holder.option.visibility = View.VISIBLE
}
}
override fun getItemId(position: Int): Long {
return mLabels[position].hashCode().toLong()
return (if (position < LABEL_OFFSET) position else mLabels[position].hashCode()).toLong()
}
override fun getItemCount(): Int {
@@ -1032,12 +1042,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
category.setBackgroundColor(EhUtils.getCategoryColor(info.category))
}
bindForState(holder, info)
// Update transition name
ViewCompat.setTransitionName(
holder.thumb,
TransitionNameFactory.getThumbTransitionName(info.gid)
)
}
override fun getItemCount(): Int {
@@ -1149,10 +1153,15 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.LEFT
)
val position = viewHolder.bindingAdapterPosition
return if (position < LABEL_OFFSET) {
makeMovementFlags(0, 0)
} else {
makeMovementFlags(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.LEFT
)
}
}
override fun onMove(
@@ -1163,10 +1172,10 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
val context = context
if (null == context || fromPosition == toPosition || toPosition == 0 || fromPosition == 0) {
if (null == context || fromPosition == toPosition || toPosition < LABEL_OFFSET) {
return false
}
downloadManager.moveLabel(fromPosition - 1, toPosition - 1)
downloadManager.moveLabel(fromPosition - LABEL_OFFSET, toPosition - LABEL_OFFSET)
val item = mLabels.removeAt(fromPosition)
mLabels.add(toPosition, item)
mLabelAdapter!!.notifyItemMoved(fromPosition, toPosition)
@@ -1176,10 +1185,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
if (mDownloadManager == null) return
if (position == 0) {
mLabelAdapter!!.notifyItemChanged(position)
return
}
val label = mLabels[position]
BaseDialogBuilder(context!!)
.setMessage(getString(R.string.delete_label, label))
@@ -1191,13 +1196,15 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
mLabelAdapter!!.notifyItemRemoved(position)
}
}
}.setNegativeButton(android.R.string.cancel) { _, _ ->
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.cancel()
}.setOnCancelListener {
mLabelAdapter!!.notifyItemChanged(position)
}.show()
}
}
private inner class DownloadItemTouchHelperCallback : ItemTouchHelper.Callback() {
private val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
@@ -1218,10 +1225,17 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
if (fromPosition == toPosition) {
return false
}
val allInfoList = mDownloadManager!!.allDownloadInfoList
val dbFromPosition = allInfoList.indexOf(mList!![fromPosition])
val dbToPosition = allInfoList.indexOf(mList!![toPosition])
lifecycleScope.launchIO {
EhDB.moveDownloadInfo(fromPosition, toPosition)
val item = mList!!.removeAt(fromPosition)
mList!!.add(toPosition, item)
EhDB.moveDownloadInfo(dbFromPosition, dbToPosition)
val item = allInfoList.removeAt(dbFromPosition)
allInfoList.add(dbToPosition, item)
if (mLabel != null) {
val item1 = mList!!.removeAt(fromPosition)
mList!!.add(toPosition, item1)
}
withUIContext {
mAdapter!!.notifyItemMoved(fromPosition, toPosition)
}
@@ -1231,7 +1245,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}
})
companion object {
const val KEY_GID = "gid"
@@ -1239,5 +1253,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
private val TAG = DownloadsScene::class.java.simpleName
private const val KEY_LABEL = "label"
private const val LABEL_OFFSET = 2
}
}
@@ -1,69 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.ui.EhActivity;
import com.hippo.ehviewer.ui.MainActivity;
public abstract class EhCallback<E extends Fragment, T> implements EhClient.Callback<T> {
private final EhApplication mApplication;
public EhCallback(Context context) {
mApplication = (EhApplication) context.getApplicationContext();
}
public Context getContent() {
Context context = mApplication.getTopActivity();
if (context == null) {
context = getApplication();
}
return context;
}
public EhApplication getApplication() {
return mApplication;
}
public void showTip(@StringRes int id, int length) {
EhActivity activity = mApplication.getTopActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).showTip(id, length);
} else {
Toast.makeText(getApplication(), id,
length == BaseScene.LENGTH_LONG ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
}
}
public void showTip(String tip, int length) {
EhActivity activity = mApplication.getTopActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).showTip(tip, length);
} else {
Toast.makeText(getApplication(), tip,
length == BaseScene.LENGTH_LONG ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.content.Context
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.ui.MainActivity
abstract class EhCallback<E : Fragment?, T>(context: Context) : EhClient.Callback<T> {
val application: EhApplication
init {
application = context.applicationContext as EhApplication
}
val content: Context
get() {
var context: Context? = application.topActivity
if (context == null) {
context = application
}
return context
}
fun showTip(@StringRes id: Int, length: Int) {
val activity = application.topActivity
if (activity is MainActivity) {
activity.showTip(id, length)
} else {
Toast.makeText(
application, id,
if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
).show()
}
}
fun showTip(tip: String, length: Int) {
val activity = application.topActivity
if (activity is MainActivity) {
activity.showTip(tip, length)
} else {
Toast.makeText(
application, tip,
if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
).show()
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,245 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.annotation.SuppressLint;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import com.hippo.drawable.TriangleDrawable;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhCacheKeyFactory;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.ehviewer.widget.TileThumb;
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager;
import com.hippo.yorozuya.ViewUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
public static final int TYPE_INVALID = -1;
public static final int TYPE_LIST = 0;
public static final int TYPE_GRID = 1;
private final LayoutInflater mInflater;
private final Resources mResources;
private final RecyclerView mRecyclerView;
private final AutoStaggeredGridLayoutManager mLayoutManager;
private final int mPaddingTopSB;
private final int mListThumbWidth;
private final int mListThumbHeight;
private final boolean mShowFavourited;
private final DownloadManager mDownloadManager;
private MarginItemDecoration mListDecoration;
private MarginItemDecoration mGirdDecoration;
private int mType = TYPE_INVALID;
public GalleryAdapter(@NonNull LayoutInflater inflater, @NonNull Resources resources,
@NonNull RecyclerView recyclerView, int type, boolean showFavourited) {
mInflater = inflater;
mResources = resources;
mRecyclerView = recyclerView;
mLayoutManager = new AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL);
mPaddingTopSB = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar);
mShowFavourited = showFavourited;
mRecyclerView.setAdapter(this);
mRecyclerView.setLayoutManager(mLayoutManager);
@SuppressLint("InflateParams") View calculator = inflater.inflate(R.layout.item_gallery_list_thumb_height, null);
ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT);
mListThumbHeight = calculator.getMeasuredHeight();
mListThumbWidth = mListThumbHeight * 2 / 3;
setType(type);
mDownloadManager = EhApplication.getDownloadManager();
}
private void adjustPaddings() {
RecyclerView recyclerView = mRecyclerView;
recyclerView.setPadding(recyclerView.getPaddingLeft(), recyclerView.getPaddingTop() + mPaddingTopSB,
recyclerView.getPaddingRight(), recyclerView.getPaddingBottom());
}
public int getType() {
return mType;
}
public void setType(int type) {
if (type == mType) {
return;
}
mType = type;
RecyclerView recyclerView = mRecyclerView;
switch (type) {
case GalleryAdapter.TYPE_LIST -> {
int columnWidth = mResources.getDimensionPixelOffset(Settings.getDetailSizeResId());
mLayoutManager.setColumnSize(columnWidth);
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE);
if (null != mGirdDecoration) {
recyclerView.removeItemDecoration(mGirdDecoration);
}
if (null == mListDecoration) {
int interval = mResources.getDimensionPixelOffset(R.dimen.gallery_list_interval);
int paddingH = mResources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h);
int paddingV = mResources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v);
mListDecoration = new MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV);
}
recyclerView.addItemDecoration(mListDecoration);
adjustPaddings();
notifyDataSetChanged();
}
case GalleryAdapter.TYPE_GRID -> {
int columnWidth = Settings.getThumbSize();
mLayoutManager.setColumnSize(columnWidth);
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_SUITABLE_SIZE);
if (null != mListDecoration) {
recyclerView.removeItemDecoration(mListDecoration);
}
if (null == mGirdDecoration) {
int interval = mResources.getDimensionPixelOffset(R.dimen.gallery_grid_interval);
int paddingH = mResources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_h);
int paddingV = mResources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_v);
mGirdDecoration = new MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV);
}
recyclerView.addItemDecoration(mGirdDecoration);
adjustPaddings();
notifyDataSetChanged();
}
}
}
@NonNull
@Override
public GalleryHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
int layoutId = switch (viewType) {
case TYPE_LIST -> R.layout.item_gallery_list;
case TYPE_GRID -> R.layout.item_gallery_grid;
default -> throw new IllegalStateException("Unexpected value: " + viewType);
};
GalleryHolder holder = new GalleryHolder(mInflater.inflate(layoutId, parent, false));
if (viewType == TYPE_LIST) {
ViewGroup.LayoutParams lp = holder.thumb.getLayoutParams();
lp.width = mListThumbWidth;
lp.height = mListThumbHeight;
holder.thumb.setLayoutParams(lp);
}
holder.card.setOnClickListener(v -> onItemClick(holder.itemView, holder.getBindingAdapterPosition()));
holder.card.setOnLongClickListener(v -> onItemLongClick(holder.itemView, holder.getBindingAdapterPosition()));
return holder;
}
abstract void onItemClick(View view, int position);
abstract boolean onItemLongClick(View view, int position);
@Override
public int getItemViewType(int position) {
return mType;
}
@Nullable
public abstract GalleryInfo getDataAt(int position);
@SuppressLint("SetTextI18n")
@Override
public void onBindViewHolder(@NonNull GalleryHolder holder, int position) {
GalleryInfo gi = getDataAt(position);
if (null == gi) {
return;
}
switch (mType) {
case TYPE_LIST -> {
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.getGid()), gi.getThumb());
holder.title.setText(EhUtils.getSuitableTitle(gi));
holder.uploader.setText(gi.getUploader());
holder.uploader.setAlpha(gi.getDisowned() ? .5f : 1f);
holder.rating.setRating(gi.getRating());
TextView category = holder.category;
String newCategoryText = EhUtils.getCategory(gi.getCategory());
if (!newCategoryText.equals(category.getText().toString())) {
category.setText(newCategoryText);
category.setBackgroundColor(EhUtils.getCategoryColor(gi.getCategory()));
}
holder.posted.setText(gi.getPosted());
if (gi.getPages() == 0 || !Settings.getShowGalleryPages()) {
holder.pages.setText(null);
holder.pages.setVisibility(View.GONE);
} else {
holder.pages.setText(gi.getPages() + "P");
holder.pages.setVisibility(View.VISIBLE);
}
if (TextUtils.isEmpty(gi.getSimpleLanguage())) {
holder.simpleLanguage.setText(null);
holder.simpleLanguage.setVisibility(View.GONE);
} else {
holder.simpleLanguage.setText(gi.getSimpleLanguage());
holder.simpleLanguage.setVisibility(View.VISIBLE);
}
holder.favourited.setVisibility((mShowFavourited && gi.getFavoriteSlot() >= -1 && gi.getFavoriteSlot() <= 10) ? View.VISIBLE : View.GONE);
holder.downloaded.setVisibility(mDownloadManager.containDownloadInfo(gi.getGid()) ? View.VISIBLE : View.GONE);
}
case TYPE_GRID -> {
((TileThumb) holder.thumb).setThumbSize(gi.getThumbWidth(), gi.getThumbHeight());
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.getGid()), gi.getThumb());
View category = holder.category;
Drawable drawable = category.getBackground();
int color = EhUtils.getCategoryColor(gi.getCategory());
if (!(drawable instanceof TriangleDrawable)) {
drawable = new TriangleDrawable(color);
category.setBackground(drawable);
} else {
((TriangleDrawable) drawable).setColor(color);
}
holder.simpleLanguage.setText(gi.getSimpleLanguage());
}
}
// Update transition name
ViewCompat.setTransitionName(holder.thumb, TransitionNameFactory.getThumbTransitionName(gi.getGid()));
}
@IntDef({TYPE_LIST, TYPE_GRID})
@Retention(RetentionPolicy.SOURCE)
public @interface Type {
}
}
@@ -0,0 +1,231 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
import android.content.res.Resources
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IntDef
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.hippo.drawable.TriangleDrawable
import com.hippo.easyrecyclerview.MarginItemDecoration
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCacheKeyFactory
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.widget.TileThumb
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager
import com.hippo.yorozuya.ViewUtils
internal abstract class GalleryAdapter(
private val mInflater: LayoutInflater, private val mResources: Resources,
private val mRecyclerView: RecyclerView, type: Int, showFavourited: Boolean
) : RecyclerView.Adapter<GalleryHolder>() {
private val mLayoutManager: AutoStaggeredGridLayoutManager =
AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)
private val mPaddingTopSB: Int =
mResources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar)
private val mListThumbWidth: Int
private val mListThumbHeight: Int
private val mShowFavourited: Boolean
private val mDownloadManager: DownloadManager
private var mListDecoration: MarginItemDecoration? = null
private var mGirdDecoration: MarginItemDecoration? = null
private var mType = TYPE_INVALID
var type: Int
get() = mType
set(type) {
if (type == mType) {
return
}
mType = type
val recyclerView = mRecyclerView
when (type) {
TYPE_LIST -> {
val columnWidth =
mResources.getDimensionPixelOffset(Settings.getDetailSizeResId())
mLayoutManager.setColumnSize(columnWidth)
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE)
if (null != mGirdDecoration) {
recyclerView.removeItemDecoration(mGirdDecoration!!)
}
if (null == mListDecoration) {
val interval =
mResources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
val paddingH =
mResources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h)
val paddingV =
mResources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v)
mListDecoration =
MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV)
}
recyclerView.addItemDecoration(mListDecoration!!)
adjustPaddings()
notifyDataSetChanged()
}
TYPE_GRID -> {
val columnWidth = Settings.getThumbSize()
mLayoutManager.setColumnSize(columnWidth)
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_SUITABLE_SIZE)
if (null != mListDecoration) {
recyclerView.removeItemDecoration(mListDecoration!!)
}
if (null == mGirdDecoration) {
val interval =
mResources.getDimensionPixelOffset(R.dimen.gallery_grid_interval)
val paddingH =
mResources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_h)
val paddingV =
mResources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_v)
mGirdDecoration =
MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV)
}
recyclerView.addItemDecoration(mGirdDecoration!!)
adjustPaddings()
notifyDataSetChanged()
}
}
}
init {
mShowFavourited = showFavourited
mRecyclerView.adapter = this
mRecyclerView.layoutManager = mLayoutManager
@SuppressLint("InflateParams") val calculator =
mInflater.inflate(R.layout.item_gallery_list_thumb_height, null)
ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT)
mListThumbHeight = calculator.measuredHeight
mListThumbWidth = mListThumbHeight * 2 / 3
this.type = type
mDownloadManager = downloadManager
}
private fun adjustPaddings() {
val recyclerView = mRecyclerView
recyclerView.setPadding(
recyclerView.paddingLeft, recyclerView.paddingTop + mPaddingTopSB,
recyclerView.paddingRight, recyclerView.paddingBottom
)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryHolder {
val layoutId = when (viewType) {
TYPE_LIST -> R.layout.item_gallery_list
TYPE_GRID -> R.layout.item_gallery_grid
else -> throw IllegalStateException("Unexpected value: $viewType")
}
val holder = GalleryHolder(mInflater.inflate(layoutId, parent, false))
if (viewType == TYPE_LIST) {
val lp = holder.thumb.layoutParams
lp.width = mListThumbWidth
lp.height = mListThumbHeight
holder.thumb.layoutParams = lp
}
holder.card.setOnClickListener {
onItemClick(
holder.itemView,
holder.bindingAdapterPosition
)
}
holder.card.setOnLongClickListener {
onItemLongClick(
holder.itemView,
holder.bindingAdapterPosition
)
}
return holder
}
abstract fun onItemClick(view: View, position: Int)
abstract fun onItemLongClick(view: View, position: Int): Boolean
override fun getItemViewType(position: Int): Int {
return mType
}
abstract fun getDataAt(position: Int): GalleryInfo?
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: GalleryHolder, position: Int) {
val gi = getDataAt(position) ?: return
when (mType) {
TYPE_LIST -> {
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb!!)
holder.title!!.text = EhUtils.getSuitableTitle(gi)
holder.uploader!!.text = gi.uploader
holder.uploader.alpha = if (gi.disowned) .5f else 1f
holder.rating!!.rating = gi.rating
val category = holder.category
val newCategoryText = EhUtils.getCategory(gi.category)
if (newCategoryText != category.text.toString()) {
category.text = newCategoryText
category.setBackgroundColor(EhUtils.getCategoryColor(gi.category))
}
holder.posted!!.text = gi.posted
if (gi.pages == 0 || !Settings.getShowGalleryPages()) {
holder.pages?.text = null
holder.pages!!.visibility = View.GONE
} else {
holder.pages!!.text = gi.pages.toString() + "P"
holder.pages.visibility = View.VISIBLE
}
if (TextUtils.isEmpty(gi.simpleLanguage)) {
holder.simpleLanguage.text = null
holder.simpleLanguage.visibility = View.GONE
} else {
holder.simpleLanguage.text = gi.simpleLanguage
holder.simpleLanguage.visibility = View.VISIBLE
}
holder.favourited!!.visibility =
if (mShowFavourited && gi.favoriteSlot >= -1 && gi.favoriteSlot <= 10) View.VISIBLE else View.GONE
holder.downloaded!!.visibility =
if (mDownloadManager.containDownloadInfo(gi.gid)) View.VISIBLE else View.GONE
}
TYPE_GRID -> {
(holder.thumb as TileThumb).setThumbSize(gi.thumbWidth, gi.thumbHeight)
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb!!)
val category: View = holder.category
var drawable = category.background
val color = EhUtils.getCategoryColor(gi.category)
if (drawable !is TriangleDrawable) {
drawable = TriangleDrawable(color)
category.background = drawable
} else {
drawable.setColor(color)
}
holder.simpleLanguage.text = gi.simpleLanguage
}
}
}
@IntDef(TYPE_LIST, TYPE_GRID)
@Retention(AnnotationRetention.SOURCE)
annotation class Type
companion object {
const val TYPE_INVALID = -1
const val TYPE_LIST = 0
const val TYPE_GRID = 1
}
}
@@ -1,972 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Looper;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.UrlOpener;
import com.hippo.ehviewer.WindowInsetsAnimationHelper;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhFilter;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.data.GalleryComment;
import com.hippo.ehviewer.client.data.GalleryCommentList;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.data.ListUrlBuilder;
import com.hippo.ehviewer.client.parser.VoteCommentParser;
import com.hippo.ehviewer.dao.Filter;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.text.URLImageGetter;
import com.hippo.util.ClipboardUtilKt;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.ReadableTime;
import com.hippo.util.TextUrl;
import com.hippo.view.ViewTransition;
import com.hippo.widget.FabLayout;
import com.hippo.widget.LinkifyTextView;
import com.hippo.widget.ObservedTextView;
import com.hippo.yorozuya.AnimationUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.SimpleAnimatorListener;
import com.hippo.yorozuya.StringUtils;
import com.hippo.yorozuya.ViewUtils;
import com.hippo.yorozuya.collect.IntList;
import java.util.ArrayList;
import java.util.List;
import rikka.core.res.ResourcesKt;
public final class GalleryCommentsScene extends BaseToolbarScene
implements View.OnClickListener, SwipeRefreshLayout.OnRefreshListener {
public static final String TAG = GalleryCommentsScene.class.getSimpleName();
public static final String KEY_API_UID = "api_uid";
public static final String KEY_API_KEY = "api_key";
public static final String KEY_GID = "gid";
public static final String KEY_TOKEN = "token";
public static final String KEY_COMMENT_LIST = "comment_list";
public static final String KEY_GALLERY_DETAIL = "gallery_detail";
private static final int TYPE_COMMENT = 0;
private static final int TYPE_MORE = 1;
private static final int TYPE_PROGRESS = 2;
private final EditPanelOnBackPressedCallback mCallback = new EditPanelOnBackPressedCallback();
private GalleryDetail mGalleryDetail;
@Nullable
private EasyRecyclerView mRecyclerView;
@Nullable
private FabLayout mFabLayout;
@Nullable
private FloatingActionButton mFab;
@Nullable
private View mEditPanel;
@Nullable
private ImageView mSendImage;
@Nullable
private EditText mEditText;
@Nullable
private CommentAdapter mAdapter;
@Nullable
private ViewTransition mViewTransition;
private SwipeRefreshLayout mRefreshLayout;
private Drawable mSendDrawable;
private Drawable mPencilDrawable;
private long mCommentId;
private boolean mInAnimation = false;
private boolean mShowAllComments = false;
private boolean mRefreshingComments = false;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requireActivity().getOnBackPressedDispatcher().addCallback(mCallback);
if (savedInstanceState == null) {
onInit();
} else {
onRestore(savedInstanceState);
}
}
private void handleArgs(Bundle args) {
if (args == null) {
return;
}
mGalleryDetail = args.getParcelable(KEY_GALLERY_DETAIL);
mShowAllComments = mGalleryDetail != null && mGalleryDetail.comments != null && !mGalleryDetail.comments.hasMore;
}
private void onInit() {
handleArgs(getArguments());
}
private void onRestore(@NonNull Bundle savedInstanceState) {
mGalleryDetail = savedInstanceState.getParcelable(KEY_GALLERY_DETAIL);
mShowAllComments = mGalleryDetail != null && mGalleryDetail.comments != null && !mGalleryDetail.comments.hasMore;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_GALLERY_DETAIL, mGalleryDetail);
}
@NonNull
@Override
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
ViewGroup view = (ViewGroup) inflater.inflate(R.layout.scene_gallery_comments, container, false);
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view);
setLiftOnScrollTargetView(mRecyclerView);
TextView tip = (TextView) ViewUtils.$$(view, R.id.tip);
mEditPanel = ViewUtils.$$(view, R.id.edit_panel);
mSendImage = (ImageView) ViewUtils.$$(mEditPanel, R.id.send);
mEditText = (EditText) ViewUtils.$$(mEditPanel, R.id.edit_text);
mFabLayout = (FabLayout) ViewUtils.$$(view, R.id.fab_layout);
mFab = (FloatingActionButton) ViewUtils.$$(view, R.id.fab);
mRefreshLayout = (SwipeRefreshLayout) ViewUtils.$$(view, R.id.refresh_layout);
// Workaround for fab and edittext render out of screen
view.removeView(mFabLayout);
view.removeView((View) mEditPanel.getParent());
assert container != null;
container.addView(mFabLayout);
container.addView((View) mEditPanel.getParent());
ViewCompat.setWindowInsetsAnimationCallback(view, new WindowInsetsAnimationHelper(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
mEditPanel,
mFabLayout
));
mRefreshLayout.setOnRefreshListener(this);
Context context = requireContext();
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
tip.setCompoundDrawables(null, drawable, null, null);
mSendDrawable = ContextCompat.getDrawable(context, R.drawable.v_send_dark_x24);
mPencilDrawable = ContextCompat.getDrawable(context, R.drawable.v_pencil_dark_x24);
mAdapter = new CommentAdapter();
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(context,
RecyclerView.VERTICAL, false));
mRecyclerView.setHasFixedSize(true);
// Cancel change animator
RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
if (itemAnimator instanceof DefaultItemAnimator) {
((DefaultItemAnimator) itemAnimator).setSupportsChangeAnimations(false);
}
mSendImage.setOnClickListener(this);
mFab.setOnClickListener(this);
addAboveSnackView(mEditPanel);
addAboveSnackView(mFabLayout);
mViewTransition = new ViewTransition(mRecyclerView, tip);
updateView(false);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mCallback.remove();
if (null != mRecyclerView) {
mRecyclerView.stopScroll();
mRecyclerView = null;
}
if (null != mEditPanel) {
removeAboveSnackView(mEditPanel);
mEditPanel = null;
}
if (null != mFabLayout) {
removeAboveSnackView(mFabLayout);
mFabLayout = null;
}
mFab = null;
mSendImage = null;
mEditText = null;
mAdapter = null;
mViewTransition = null;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setTitle(R.string.gallery_comments);
setNavigationIcon(R.drawable.v_arrow_left_dark_x24);
}
private void showFilterCommenterDialog(String commenter, int position) {
Context context = getContext();
if (context == null || commenter == null) {
return;
}
new BaseDialogBuilder(context)
.setMessage(getString(R.string.filter_the_commenter, commenter))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Filter filter = new Filter();
filter.mode = EhFilter.MODE_COMMENTER;
filter.text = commenter;
EhFilter.getInstance().addFilter(filter);
hideComment(position);
showTip(R.string.filter_added, LENGTH_SHORT);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void hideComment(int position) {
if (mGalleryDetail == null || mGalleryDetail.comments == null || mGalleryDetail.comments.comments == null) {
return;
}
GalleryComment[] oldCommentsList = mGalleryDetail.comments.comments;
GalleryComment[] newCommentsList = new GalleryComment[oldCommentsList.length - 1];
for (int i = 0, j = 0; i < oldCommentsList.length; i++) {
if (i != position) {
newCommentsList[j] = oldCommentsList[i];
j++;
}
}
mGalleryDetail.comments.comments = newCommentsList;
mAdapter.notifyDataSetChanged();
updateView(true);
}
private void voteComment(long id, int vote) {
Context context = getContext();
MainActivity activity = getMainActivity();
if (null == context || null == activity) {
return;
}
EhRequest request = new EhRequest()
.setMethod(EhClient.METHOD_VOTE_COMMENT)
.setArgs(mGalleryDetail.apiUid, mGalleryDetail.apiKey, mGalleryDetail.getGid(), mGalleryDetail.getToken(), id, vote)
.setCallback(new VoteCommentListener(context));
request.enqueue(this);
}
@SuppressLint("InflateParams")
public void showVoteStatusDialog(Context context, String voteStatus) {
String[] temp = StringUtils.split(voteStatus, ',');
final int length = temp.length;
final String[] userArray = new String[length];
final String[] voteArray = new String[length];
for (int i = 0; i < length; i++) {
String str = StringUtils.trim(temp[i]);
int index = str.lastIndexOf(' ');
if (index < 0) {
Log.d(TAG, "Something wrong happened about vote state");
userArray[i] = str;
voteArray[i] = "";
} else {
userArray[i] = StringUtils.trim(str.substring(0, index));
voteArray[i] = StringUtils.trim(str.substring(index + 1));
}
}
BaseDialogBuilder builder = new BaseDialogBuilder(context);
context = builder.getContext();
final LayoutInflater inflater = LayoutInflater.from(context);
EasyRecyclerView rv = (EasyRecyclerView) inflater.inflate(R.layout.dialog_recycler_view, null);
rv.setAdapter(new RecyclerView.Adapter<InfoHolder>() {
@NonNull
@Override
public InfoHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new InfoHolder(inflater.inflate(R.layout.item_drawer_favorites, parent, false));
}
@Override
public void onBindViewHolder(@NonNull InfoHolder holder, int position) {
holder.key.setText(userArray[position]);
holder.value.setText(voteArray[position]);
}
@Override
public int getItemCount() {
return length;
}
});
rv.setLayoutManager(new LinearLayoutManager(context));
rv.setClipToPadding(false);
builder.setView(rv).show();
}
private void showCommentDialog(int position) {
final Context context = getContext();
if (context == null || mGalleryDetail == null || mGalleryDetail.comments == null || mGalleryDetail.comments.comments == null || position >= mGalleryDetail.comments.comments.length || position < 0) {
return;
}
final GalleryComment comment = mGalleryDetail.comments.comments[position];
List<String> menu = new ArrayList<>();
final IntList menuId = new IntList();
Resources resources = context.getResources();
menu.add(resources.getString(R.string.copy_comment_text));
menuId.add(R.id.copy);
if (!comment.uploader && !comment.editable) {
menu.add(resources.getString(R.string.block_commenter));
menuId.add(R.id.block_commenter);
}
if (comment.editable) {
menu.add(resources.getString(R.string.edit_comment));
menuId.add(R.id.edit_comment);
}
if (comment.voteUpAble) {
menu.add(resources.getString(comment.voteUpEd ? R.string.cancel_vote_up : R.string.vote_up));
menuId.add(R.id.vote_up);
}
if (comment.voteDownAble) {
menu.add(resources.getString(comment.voteDownEd ? R.string.cancel_vote_down : R.string.vote_down));
menuId.add(R.id.vote_down);
}
if (!TextUtils.isEmpty(comment.voteState)) {
menu.add(resources.getString(R.string.check_vote_status));
menuId.add(R.id.check_vote_status);
}
new BaseDialogBuilder(context)
.setItems(menu.toArray(new String[0]), (dialog, which) -> {
if (which < 0 || which >= menuId.size()) {
return;
}
int id = menuId.get(which);
if (id == R.id.copy) {
ClipboardUtilKt.addTextToClipboard(requireActivity(), comment.comment, false);
} else if (id == R.id.block_commenter) {
showFilterCommenterDialog(comment.user, position);
} else if (id == R.id.vote_up) {
voteComment(comment.id, 1);
} else if (id == R.id.vote_down) {
voteComment(comment.id, -1);
} else if (id == R.id.check_vote_status) {
showVoteStatusDialog(context, comment.voteState);
} else if (id == R.id.edit_comment) {
prepareEditComment(comment.id);
if (!mInAnimation && mEditPanel != null && mEditPanel.getVisibility() != View.VISIBLE) {
showEditPanel(true);
}
}
}).show();
}
public boolean onItemClick(EasyRecyclerView parent, View view, int position) {
MainActivity activity = getMainActivity();
if (null == activity) {
return false;
}
RecyclerView.ViewHolder holder = parent.getChildViewHolder(view);
if (holder instanceof ActualCommentHolder commentHolder) {
ClickableSpan span = commentHolder.comment.getCurrentSpan();
commentHolder.comment.clearCurrentSpan();
if (span instanceof URLSpan) {
UrlOpener.openUrl(activity, ((URLSpan) span).getURL(), true, mGalleryDetail);
} else {
showCommentDialog(position);
}
} else if (holder instanceof MoreCommentHolder && !mRefreshingComments && mAdapter != null) {
mRefreshingComments = true;
mShowAllComments = true;
mAdapter.notifyItemChanged(position);
String url = getGalleryDetailUrl();
if (url != null) {
// Request
EhRequest request = new EhRequest()
.setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)
.setArgs(url)
.setCallback(new RefreshCommentListener(activity));
request.enqueue(this);
}
}
return true;
}
private void updateView(boolean animation) {
if (null == mViewTransition) {
return;
}
if (mGalleryDetail == null || mGalleryDetail.comments == null || mGalleryDetail.comments.comments == null || mGalleryDetail.comments.comments.length <= 0) {
mViewTransition.showView(1, animation);
} else {
mViewTransition.showView(0, animation);
}
}
private void prepareNewComment() {
mCommentId = 0;
if (mSendImage != null) {
mSendImage.setImageDrawable(mSendDrawable);
}
}
private void prepareEditComment(long commentId) {
mCommentId = commentId;
if (mSendImage != null) {
mSendImage.setImageDrawable(mPencilDrawable);
}
}
private void showEditPanelWithAnimation() {
if (null == mFab || null == mEditPanel) {
return;
}
mInAnimation = true;
mFab.setTranslationX(0.0f);
mFab.setTranslationY(0.0f);
mFab.setScaleX(1.0f);
mFab.setScaleY(1.0f);
int fabEndX = mEditPanel.getLeft() + (mEditPanel.getWidth() / 2) - (mFab.getWidth() / 2);
int fabEndY = mEditPanel.getTop() + (mEditPanel.getHeight() / 2) - (mFab.getHeight() / 2);
mFab.animate().x(fabEndX).y(fabEndY).scaleX(0.0f).scaleY(0.0f)
.setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)
.setDuration(300L).setListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
if (null == mFab || null == mEditPanel) {
return;
}
((View) mFab).setVisibility(View.INVISIBLE);
mEditPanel.setVisibility(View.VISIBLE);
int halfW = mEditPanel.getWidth() / 2;
int halfH = mEditPanel.getHeight() / 2;
Animator animator = ViewAnimationUtils.createCircularReveal(mEditPanel, halfW, halfH, 0,
(float) Math.hypot(halfW, halfH)).setDuration(300L);
animator.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator a) {
mInAnimation = false;
}
});
animator.start();
}
}).start();
}
private void showEditPanel(boolean animation) {
mCallback.setEnabled(true);
if (animation) {
showEditPanelWithAnimation();
} else {
if (null == mFab || null == mEditPanel) {
return;
}
((View) mFab).setVisibility(View.INVISIBLE);
mEditPanel.setVisibility(View.VISIBLE);
}
}
private void hideEditPanelWithAnimation() {
if (null == mFab || null == mEditPanel) {
return;
}
mInAnimation = true;
int halfW = mEditPanel.getWidth() / 2;
int halfH = mEditPanel.getHeight() / 2;
Animator animator = ViewAnimationUtils.createCircularReveal(mEditPanel, halfW, halfH,
(float) Math.hypot(halfW, halfH), 0.0f).setDuration(300L);
animator.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator a) {
if (null == mFab || null == mEditPanel) {
return;
}
if (Looper.myLooper() != Looper.getMainLooper()) {
// Some devices may run this block in non-UI thread.
// It might be a bug of Android OS.
// Check it here to avoid crash.
return;
}
mEditPanel.setVisibility(View.GONE);
((View) mFab).setVisibility(View.VISIBLE);
int fabStartX = mEditPanel.getLeft() + (mEditPanel.getWidth() / 2) - (mFab.getWidth() / 2);
int fabStartY = mEditPanel.getTop() + (mEditPanel.getHeight() / 2) - (mFab.getHeight() / 2);
mFab.setX(fabStartX);
mFab.setY(fabStartY);
mFab.setScaleX(0.0f);
mFab.setScaleY(0.0f);
mFab.setRotation(-45.0f);
mFab.animate().translationX(0.0f).translationY(0.0f).scaleX(1.0f).scaleY(1.0f).rotation(0.0f)
.setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)
.setDuration(300L).setListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
mInAnimation = false;
}
}).start();
}
});
animator.start();
}
private void hideEditPanel(boolean animation) {
mCallback.setEnabled(false);
hideSoftInput();
if (animation) {
hideEditPanelWithAnimation();
} else {
if (null == mFab || null == mEditPanel) {
return;
}
((View) mFab).setVisibility(View.VISIBLE);
mEditPanel.setVisibility(View.INVISIBLE);
}
}
@Nullable
private String getGalleryDetailUrl() {
if (mGalleryDetail != null && mGalleryDetail.getGid() != -1 && mGalleryDetail.getToken() != null) {
return EhUrl.getGalleryDetailUrl(mGalleryDetail.getGid(), mGalleryDetail.getToken(), 0, mShowAllComments);
} else {
return null;
}
}
@Override
public void onClick(View v) {
Context context = getContext();
MainActivity activity = getMainActivity();
if (null == context || null == activity || null == mEditText) {
return;
}
if (mFab == v) {
if (!mInAnimation) {
prepareNewComment();
showEditPanel(true);
}
} else if (mSendImage == v) {
if (!mInAnimation) {
String comment = mEditText.getText().toString();
if (TextUtils.isEmpty(comment)) {
// Comment is empty
return;
}
String url = getGalleryDetailUrl();
if (url == null) {
return;
}
// Request
EhRequest request = new EhRequest()
.setMethod(EhClient.METHOD_GET_COMMENT_GALLERY)
.setArgs(url, comment, mCommentId != 0 ? Long.toString(mCommentId) : null)
.setCallback(new CommentGalleryListener(context, mCommentId));
request.enqueue(this);
hideSoftInput();
hideEditPanel(true);
}
}
}
private void onRefreshGallerySuccess(GalleryCommentList result) {
if (mGalleryDetail == null || mAdapter == null) {
return;
}
mRefreshLayout.setRefreshing(false);
mRefreshingComments = false;
mGalleryDetail.comments = result;
mAdapter.notifyDataSetChanged();
updateView(true);
}
private void onRefreshGalleryFailure() {
if (mAdapter == null) {
return;
}
mRefreshLayout.setRefreshing(false);
mRefreshingComments = false;
int position = mAdapter.getItemCount() - 1;
if (position >= 0) {
mAdapter.notifyItemChanged(position);
}
}
private void onCommentGallerySuccess(GalleryCommentList result) {
if (mGalleryDetail == null || mAdapter == null) {
return;
}
mGalleryDetail.comments = result;
mAdapter.notifyDataSetChanged();
// Remove text
if (mEditText != null) {
mEditText.setText("");
}
updateView(true);
}
private void onVoteCommentSuccess(VoteCommentParser.Result result) {
if (mAdapter == null || mGalleryDetail.comments == null || mGalleryDetail.comments.comments == null) {
return;
}
int position = -1;
for (int i = 0, n = mGalleryDetail.comments.comments.length; i < n; i++) {
GalleryComment comment = mGalleryDetail.comments.comments[i];
if (comment.id == result.id) {
position = i;
break;
}
}
if (-1 == position) {
Log.d(TAG, "Can't find comment with id " + result.id);
return;
}
// Update comment
GalleryComment comment = mGalleryDetail.comments.comments[position];
comment.score = result.score;
if (result.expectVote > 0) {
comment.voteUpEd = 0 != result.vote;
comment.voteDownEd = false;
} else {
comment.voteDownEd = 0 != result.vote;
comment.voteUpEd = false;
}
mAdapter.notifyItemChanged(position);
}
@Override
public void onRefresh() {
if (!mRefreshingComments && mAdapter != null) {
MainActivity activity = (MainActivity) requireActivity();
mRefreshingComments = true;
String url = getGalleryDetailUrl();
if (url != null) {
// Request
EhRequest request = new EhRequest()
.setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)
.setArgs(url)
.setCallback(new RefreshCommentListener(activity));
request.enqueue(this);
}
}
}
private static class InfoHolder extends RecyclerView.ViewHolder {
private final TextView key;
private final TextView value;
public InfoHolder(View itemView) {
super(itemView);
key = (TextView) ViewUtils.$$(itemView, R.id.key);
value = (TextView) ViewUtils.$$(itemView, R.id.value);
}
}
private abstract static class CommentHolder extends RecyclerView.ViewHolder {
public CommentHolder(LayoutInflater inflater, int resId, ViewGroup parent) {
super(inflater.inflate(resId, parent, false));
}
}
private static class MoreCommentHolder extends CommentHolder {
public MoreCommentHolder(LayoutInflater inflater, ViewGroup parent) {
super(inflater, R.layout.item_gallery_comment_more, parent);
}
}
private static class ProgressCommentHolder extends CommentHolder {
public ProgressCommentHolder(LayoutInflater inflater, ViewGroup parent) {
super(inflater, R.layout.item_gallery_comment_progress, parent);
}
}
private class RefreshCommentListener extends EhCallback<GalleryCommentsScene, GalleryDetail> {
public RefreshCommentListener(Context context) {
super(context);
}
@Override
public void onSuccess(GalleryDetail result) {
GalleryCommentsScene scene = GalleryCommentsScene.this;
scene.onRefreshGallerySuccess(result.comments);
}
@Override
public void onFailure(Exception e) {
GalleryCommentsScene scene = GalleryCommentsScene.this;
scene.onRefreshGalleryFailure();
}
@Override
public void onCancel() {
}
}
private class CommentGalleryListener extends EhCallback<GalleryCommentsScene, GalleryCommentList> {
private final long mCommentId;
public CommentGalleryListener(Context context, long commentId) {
super(context);
mCommentId = commentId;
}
@Override
public void onSuccess(GalleryCommentList result) {
showTip(mCommentId != 0 ? R.string.edit_comment_successfully : R.string.comment_successfully, LENGTH_SHORT);
GalleryCommentsScene scene = GalleryCommentsScene.this;
scene.onCommentGallerySuccess(result);
}
@Override
public void onFailure(Exception e) {
showTip(getContent().getString(mCommentId != 0 ? R.string.edit_comment_failed : R.string.comment_failed) + "\n" + ExceptionUtils.getReadableString(e), LENGTH_LONG);
}
@Override
public void onCancel() {
}
}
private class VoteCommentListener extends EhCallback<GalleryCommentsScene, VoteCommentParser.Result> {
public VoteCommentListener(Context context) {
super(context);
}
@Override
public void onSuccess(VoteCommentParser.Result result) {
showTip(result.expectVote > 0 ?
(0 != result.vote ? R.string.vote_up_successfully : R.string.cancel_vote_up_successfully) :
(0 != result.vote ? R.string.vote_down_successfully : R.string.cancel_vote_down_successfully),
LENGTH_SHORT);
GalleryCommentsScene scene = GalleryCommentsScene.this;
scene.onVoteCommentSuccess(result);
}
@Override
public void onFailure(Exception e) {
showTip(R.string.vote_failed, LENGTH_LONG);
}
@Override
public void onCancel() {
}
}
private class ActualCommentHolder extends CommentHolder {
private final TextView user;
private final TextView time;
private final LinkifyTextView comment;
public ActualCommentHolder(LayoutInflater inflater, ViewGroup parent) {
super(inflater, R.layout.item_gallery_comment, parent);
user = itemView.findViewById(R.id.user);
time = itemView.findViewById(R.id.time);
comment = itemView.findViewById(R.id.comment);
}
private CharSequence generateComment(Context context, ObservedTextView textView, GalleryComment comment) {
Spanned sp = Html.fromHtml(comment.comment, Html.FROM_HTML_MODE_LEGACY, new URLImageGetter(textView), null);
SpannableStringBuilder ssb = new SpannableStringBuilder(sp);
if (0 != comment.id && 0 != comment.score) {
int score = comment.score;
String scoreString = score > 0 ? "+" + score : Integer.toString(score);
SpannableString ss = new SpannableString(scoreString);
ss.setSpan(new RelativeSizeSpan(0.8f), 0, scoreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ss.setSpan(new StyleSpan(Typeface.BOLD), 0, scoreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ss.setSpan(new ForegroundColorSpan(ResourcesKt.resolveColor(getTheme(), android.R.attr.textColorSecondary))
, 0, scoreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.append(" ").append(ss);
}
if (comment.lastEdited != 0) {
String str = context.getString(R.string.last_edited, ReadableTime.getTimeAgo(comment.lastEdited));
SpannableString ss = new SpannableString(str);
ss.setSpan(new RelativeSizeSpan(0.8f), 0, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ss.setSpan(new StyleSpan(Typeface.BOLD), 0, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ss.setSpan(new ForegroundColorSpan(ResourcesKt.resolveColor(getTheme(), android.R.attr.textColorSecondary)),
0, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.append("\n\n").append(ss);
}
return TextUrl.handleTextUrl(ssb);
}
public void bind(GalleryComment value) {
user.setText(value.uploader ? getString(R.string.comment_user_uploader, value.user) : value.user);
user.setOnClickListener(v -> {
ListUrlBuilder lub = new ListUrlBuilder();
lub.setMode(ListUrlBuilder.MODE_UPLOADER);
lub.setKeyword(value.user);
navigate(R.id.galleryListScene, GalleryListScene.getStartArgs(lub), true);
});
time.setText(ReadableTime.getTimeAgo(value.time));
comment.setText(generateComment(comment.getContext(), comment, value));
}
}
private class CommentAdapter extends RecyclerView.Adapter<CommentHolder> {
private final LayoutInflater mInflater;
public CommentAdapter() {
mInflater = getLayoutInflater();
AssertUtils.assertNotNull(mInflater);
}
@NonNull
@Override
public CommentHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) {
case TYPE_COMMENT:
return new ActualCommentHolder(mInflater, parent);
case TYPE_MORE:
return new MoreCommentHolder(mInflater, parent);
case TYPE_PROGRESS:
return new ProgressCommentHolder(mInflater, parent);
default:
throw new IllegalStateException("Invalid view type: " + viewType);
}
}
@Override
public void onBindViewHolder(@NonNull CommentHolder holder, int position) {
Context context = getContext();
if (context == null || mGalleryDetail == null || mGalleryDetail.comments == null) {
return;
}
holder.itemView.setOnClickListener(v -> onItemClick(mRecyclerView, holder.itemView, position));
holder.itemView.setClickable(true);
holder.itemView.setFocusable(true);
if (holder instanceof ActualCommentHolder) {
((ActualCommentHolder) holder).bind(mGalleryDetail.comments.comments[position]);
}
}
@Override
public int getItemCount() {
if (mGalleryDetail == null || mGalleryDetail.comments == null || mGalleryDetail.comments.comments == null) {
return 0;
} else if (mGalleryDetail.comments.hasMore) {
return mGalleryDetail.comments.comments.length + 1;
} else {
return mGalleryDetail.comments.comments.length;
}
}
@Override
public int getItemViewType(int position) {
if (position >= mGalleryDetail.comments.comments.length) {
return mRefreshingComments ? TYPE_PROGRESS : TYPE_MORE;
} else {
return TYPE_COMMENT;
}
}
}
class EditPanelOnBackPressedCallback extends OnBackPressedCallback {
public EditPanelOnBackPressedCallback() {
super(false);
}
@Override
public void handleOnBackPressed() {
if (!mInAnimation && null != mEditPanel && mEditPanel.getVisibility() == View.VISIBLE) {
hideEditPanel(true);
}
}
}
}
@@ -0,0 +1,914 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.animation.Animator
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Looper
import android.text.Html
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.StyleSpan
import android.text.style.URLSpan
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.hippo.app.BaseDialogBuilder
import com.hippo.easyrecyclerview.EasyRecyclerView
import com.hippo.ehviewer.R
import com.hippo.ehviewer.UrlOpener
import com.hippo.ehviewer.WindowInsetsAnimationHelper
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhFilter
import com.hippo.ehviewer.client.EhRequest
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryComment
import com.hippo.ehviewer.client.data.GalleryCommentList
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.parser.VoteCommentParser
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.text.URLImageGetter
import com.hippo.util.ExceptionUtils
import com.hippo.util.ReadableTime
import com.hippo.util.TextUrl
import com.hippo.util.addTextToClipboard
import com.hippo.view.ViewTransition
import com.hippo.widget.FabLayout
import com.hippo.widget.LinkifyTextView
import com.hippo.widget.ObservedTextView
import com.hippo.yorozuya.AnimationUtils
import com.hippo.yorozuya.SimpleAnimatorListener
import com.hippo.yorozuya.StringUtils
import com.hippo.yorozuya.ViewUtils
import com.hippo.yorozuya.collect.IntList
import rikka.core.res.resolveColor
import kotlin.math.hypot
class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefreshListener {
private val mCallback = EditPanelOnBackPressedCallback()
private var mGalleryDetail: GalleryDetail? = null
private var mRecyclerView: EasyRecyclerView? = null
private var mFabLayout: FabLayout? = null
private var mFab: FloatingActionButton? = null
private var mEditPanel: View? = null
private var mSendImage: ImageView? = null
private var mEditText: EditText? = null
private var mAdapter: CommentAdapter? = null
private var mViewTransition: ViewTransition? = null
private var mRefreshLayout: SwipeRefreshLayout? = null
private var mSendDrawable: Drawable? = null
private var mPencilDrawable: Drawable? = null
private var mCommentId: Long = 0
private var mInAnimation = false
private var mShowAllComments = false
private var mRefreshingComments = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(mCallback)
if (savedInstanceState == null) {
onInit()
} else {
onRestore(savedInstanceState)
}
}
private fun handleArgs(args: Bundle?) {
if (args == null) {
return
}
mGalleryDetail = args.getParcelable(KEY_GALLERY_DETAIL)
mShowAllComments =
mGalleryDetail != null && mGalleryDetail!!.comments != null && !mGalleryDetail!!.comments!!.hasMore
}
private fun onInit() {
handleArgs(arguments)
}
private fun onRestore(savedInstanceState: Bundle) {
mGalleryDetail = savedInstanceState.getParcelable(KEY_GALLERY_DETAIL)
mShowAllComments =
mGalleryDetail != null && mGalleryDetail!!.comments != null && !mGalleryDetail!!.comments!!.hasMore
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(KEY_GALLERY_DETAIL, mGalleryDetail)
}
override fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.scene_gallery_comments, container, false) as ViewGroup
mRecyclerView = ViewUtils.`$$`(view, R.id.recycler_view) as EasyRecyclerView
setLiftOnScrollTargetView(mRecyclerView)
val tip = ViewUtils.`$$`(view, R.id.tip) as TextView
mEditPanel = ViewUtils.`$$`(view, R.id.edit_panel)
mSendImage = ViewUtils.`$$`(mEditPanel, R.id.send) as ImageView
mEditText = ViewUtils.`$$`(mEditPanel, R.id.edit_text) as EditText
mFabLayout = ViewUtils.`$$`(view, R.id.fab_layout) as FabLayout
mFab = ViewUtils.`$$`(view, R.id.fab) as FloatingActionButton
mRefreshLayout = ViewUtils.`$$`(view, R.id.refresh_layout) as SwipeRefreshLayout
// Workaround for fab and edittext render out of screen
view.removeView(mFabLayout)
view.removeView(mEditPanel!!.parent as View)
assert(container != null)
container!!.addView(mFabLayout)
container.addView(mEditPanel!!.parent as View)
ViewCompat.setWindowInsetsAnimationCallback(
view, WindowInsetsAnimationHelper(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
mEditPanel,
mFabLayout
)
)
mRefreshLayout!!.setOnRefreshListener(this)
val context = requireContext()
val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
tip.setCompoundDrawables(null, drawable, null, null)
mSendDrawable = ContextCompat.getDrawable(context, R.drawable.v_send_dark_x24)
mPencilDrawable = ContextCompat.getDrawable(context, R.drawable.v_pencil_dark_x24)
mAdapter = CommentAdapter()
mRecyclerView!!.adapter = mAdapter
mRecyclerView!!.layoutManager = LinearLayoutManager(
context,
RecyclerView.VERTICAL, false
)
mRecyclerView!!.setHasFixedSize(true)
// Cancel change animator
val itemAnimator = mRecyclerView!!.itemAnimator
if (itemAnimator is DefaultItemAnimator) {
itemAnimator.supportsChangeAnimations = false
}
mSendImage!!.setOnClickListener(this)
mFab!!.setOnClickListener(this)
addAboveSnackView(mEditPanel)
addAboveSnackView(mFabLayout)
mViewTransition = ViewTransition(mRecyclerView, tip)
updateView(false)
return view
}
override fun onDestroyView() {
super.onDestroyView()
mCallback.remove()
if (null != mRecyclerView) {
mRecyclerView!!.stopScroll()
mRecyclerView = null
}
if (null != mEditPanel) {
removeAboveSnackView(mEditPanel)
mEditPanel = null
}
if (null != mFabLayout) {
removeAboveSnackView(mFabLayout)
mFabLayout = null
}
mFab = null
mSendImage = null
mEditText = null
mAdapter = null
mViewTransition = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(R.string.gallery_comments)
setNavigationIcon(R.drawable.v_arrow_left_dark_x24)
}
private fun showFilterCommenterDialog(commenter: String?, position: Int) {
val context = context
if (context == null || commenter == null) {
return
}
BaseDialogBuilder(context)
.setMessage(getString(R.string.filter_the_commenter, commenter))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val filter = Filter()
filter.mode = EhFilter.MODE_COMMENTER
filter.text = commenter
EhFilter.addFilter(filter)
hideComment(position)
showTip(R.string.filter_added, LENGTH_SHORT)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun hideComment(position: Int) {
if (mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null) {
return
}
val oldCommentsList = mGalleryDetail!!.comments!!.comments
val newCommentsList = arrayOfNulls<GalleryComment>(
oldCommentsList!!.size - 1
)
var i = 0
var j = 0
while (i < oldCommentsList.size) {
if (i != position) {
newCommentsList[j] = oldCommentsList[i]
j++
}
i++
}
mGalleryDetail!!.comments!!.comments = newCommentsList.requireNoNulls()
mAdapter!!.notifyDataSetChanged()
updateView(true)
}
private fun voteComment(id: Long, vote: Int) {
val context = context
val activity = mainActivity
if (null == context || null == activity) {
return
}
val request = EhRequest()
.setMethod(EhClient.METHOD_VOTE_COMMENT)
.setArgs(
mGalleryDetail!!.apiUid,
mGalleryDetail!!.apiKey,
mGalleryDetail!!.gid,
mGalleryDetail!!.token,
id,
vote
)
.setCallback(VoteCommentListener(context))
request.enqueue(this)
}
@SuppressLint("InflateParams")
fun showVoteStatusDialog(context: Context?, voteStatus: String?) {
var context = context
val temp = StringUtils.split(voteStatus, ',')
val length = temp.size
val userArray = arrayOfNulls<String>(length)
val voteArray = arrayOfNulls<String>(length)
for (i in 0 until length) {
val str = StringUtils.trim(temp[i])
val index = str.lastIndexOf(' ')
if (index < 0) {
Log.d(TAG, "Something wrong happened about vote state")
userArray[i] = str
voteArray[i] = ""
} else {
userArray[i] = StringUtils.trim(str.substring(0, index))
voteArray[i] = StringUtils.trim(str.substring(index + 1))
}
}
val builder = BaseDialogBuilder(context!!)
context = builder.context
val inflater = LayoutInflater.from(context)
val rv = inflater.inflate(R.layout.dialog_recycler_view, null) as EasyRecyclerView
rv.adapter = object : RecyclerView.Adapter<InfoHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InfoHolder {
return InfoHolder(inflater.inflate(R.layout.item_drawer_favorites, parent, false))
}
override fun onBindViewHolder(holder: InfoHolder, position: Int) {
holder.key.text = userArray[position]
holder.value.text = voteArray[position]
}
override fun getItemCount(): Int {
return length
}
}
rv.layoutManager = LinearLayoutManager(context)
rv.clipToPadding = false
builder.setView(rv).show()
}
private fun showCommentDialog(position: Int) {
val context = context
if (context == null || mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null || position >= mGalleryDetail!!.comments!!.comments!!.size || position < 0) {
return
}
val comment = mGalleryDetail!!.comments!!.comments!![position]
val menu: MutableList<String> = ArrayList()
val menuId = IntList()
val resources = context.resources
menu.add(resources.getString(R.string.copy_comment_text))
menuId.add(R.id.copy)
if (!comment.uploader && !comment.editable) {
menu.add(resources.getString(R.string.block_commenter))
menuId.add(R.id.block_commenter)
}
if (comment.editable) {
menu.add(resources.getString(R.string.edit_comment))
menuId.add(R.id.edit_comment)
}
if (comment.voteUpAble) {
menu.add(resources.getString(if (comment.voteUpEd) R.string.cancel_vote_up else R.string.vote_up))
menuId.add(R.id.vote_up)
}
if (comment.voteDownAble) {
menu.add(resources.getString(if (comment.voteDownEd) R.string.cancel_vote_down else R.string.vote_down))
menuId.add(R.id.vote_down)
}
if (!TextUtils.isEmpty(comment.voteState)) {
menu.add(resources.getString(R.string.check_vote_status))
menuId.add(R.id.check_vote_status)
}
BaseDialogBuilder(context)
.setItems(menu.toTypedArray()) { _: DialogInterface?, which: Int ->
if (which < 0 || which >= menuId.size()) {
return@setItems
}
val id = menuId[which]
if (id == R.id.copy) {
requireActivity().addTextToClipboard(comment.comment, false)
} else if (id == R.id.block_commenter) {
showFilterCommenterDialog(comment.user, position)
} else if (id == R.id.vote_up) {
voteComment(comment.id, 1)
} else if (id == R.id.vote_down) {
voteComment(comment.id, -1)
} else if (id == R.id.check_vote_status) {
showVoteStatusDialog(context, comment.voteState)
} else if (id == R.id.edit_comment) {
prepareEditComment(comment.id)
if (!mInAnimation && mEditPanel != null && mEditPanel!!.visibility != View.VISIBLE) {
showEditPanel(true)
}
}
}.show()
}
fun onItemClick(parent: EasyRecyclerView?, view2: View?, position: Int): Boolean {
val activity = mainActivity ?: return false
val holder = parent!!.getChildViewHolder(view2!!)
if (holder is ActualCommentHolder) {
val span = holder.comment.currentSpan
holder.comment.clearCurrentSpan()
if (span is URLSpan) {
UrlOpener.openUrl(activity, span.url, true, mGalleryDetail)
} else {
showCommentDialog(position)
}
} else if (holder is MoreCommentHolder && !mRefreshingComments && mAdapter != null) {
mRefreshingComments = true
mShowAllComments = true
mAdapter!!.notifyItemChanged(position)
val url = galleryDetailUrl
if (url != null) {
// Request
val request = EhRequest()
.setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)
.setArgs(url)
.setCallback(RefreshCommentListener(activity))
request.enqueue(this)
}
}
return true
}
private fun updateView(animation: Boolean) {
if (null == mViewTransition) {
return
}
if (mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null || mGalleryDetail!!.comments!!.comments!!.isEmpty()) {
mViewTransition!!.showView(1, animation)
} else {
mViewTransition!!.showView(0, animation)
}
}
private fun prepareNewComment() {
mCommentId = 0
if (mSendImage != null) {
mSendImage!!.setImageDrawable(mSendDrawable)
}
}
private fun prepareEditComment(commentId: Long) {
mCommentId = commentId
if (mSendImage != null) {
mSendImage!!.setImageDrawable(mPencilDrawable)
}
}
private fun showEditPanelWithAnimation() {
if (null == mFab || null == mEditPanel) {
return
}
mInAnimation = true
mFab!!.translationX = 0.0f
mFab!!.translationY = 0.0f
mFab!!.scaleX = 1.0f
mFab!!.scaleY = 1.0f
val fabEndX = mEditPanel!!.left + mEditPanel!!.width / 2 - mFab!!.width / 2
val fabEndY = mEditPanel!!.top + mEditPanel!!.height / 2 - mFab!!.height / 2
mFab!!.animate().x(fabEndX.toFloat()).y(fabEndY.toFloat()).scaleX(0.0f).scaleY(0.0f)
.setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)
.setDuration(300L).setListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
if (null == mFab || null == mEditPanel) {
return
}
(mFab as View).visibility = View.INVISIBLE
mEditPanel!!.visibility = View.VISIBLE
val halfW = mEditPanel!!.width / 2
val halfH = mEditPanel!!.height / 2
val animator = ViewAnimationUtils.createCircularReveal(
mEditPanel,
halfW,
halfH,
0f,
hypot(halfW.toDouble(), halfH.toDouble()).toFloat()
).setDuration(300L)
animator.addListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(a: Animator) {
mInAnimation = false
}
})
animator.start()
}
}).start()
}
private fun showEditPanel(animation: Boolean) {
mCallback.isEnabled = true
if (animation) {
showEditPanelWithAnimation()
} else {
if (null == mFab || null == mEditPanel) {
return
}
(mFab as View).visibility = View.INVISIBLE
mEditPanel!!.visibility = View.VISIBLE
}
}
private fun hideEditPanelWithAnimation() {
if (null == mFab || null == mEditPanel) {
return
}
mInAnimation = true
val halfW = mEditPanel!!.width / 2
val halfH = mEditPanel!!.height / 2
val animator = ViewAnimationUtils.createCircularReveal(
mEditPanel,
halfW,
halfH,
hypot(halfW.toDouble(), halfH.toDouble()).toFloat(),
0.0f
).setDuration(300L)
animator.addListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(a: Animator) {
if (null == mFab || null == mEditPanel) {
return
}
if (Looper.myLooper() != Looper.getMainLooper()) {
// Some devices may run this block in non-UI thread.
// It might be a bug of Android OS.
// Check it here to avoid crash.
return
}
mEditPanel!!.visibility = View.GONE
(mFab as View).visibility = View.VISIBLE
val fabStartX = mEditPanel!!.left + mEditPanel!!.width / 2 - mFab!!.width / 2
val fabStartY = mEditPanel!!.top + mEditPanel!!.height / 2 - mFab!!.height / 2
mFab!!.x = fabStartX.toFloat()
mFab!!.y = fabStartY.toFloat()
mFab!!.scaleX = 0.0f
mFab!!.scaleY = 0.0f
mFab!!.rotation = -45.0f
mFab!!.animate().translationX(0.0f).translationY(0.0f).scaleX(1.0f).scaleY(1.0f)
.rotation(0.0f)
.setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)
.setDuration(300L).setListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mInAnimation = false
}
}).start()
}
})
animator.start()
}
private fun hideEditPanel(animation: Boolean) {
mCallback.isEnabled = false
hideSoftInput()
if (animation) {
hideEditPanelWithAnimation()
} else {
if (null == mFab || null == mEditPanel) {
return
}
(mFab as View).visibility = View.VISIBLE
mEditPanel!!.visibility = View.INVISIBLE
}
}
private val galleryDetailUrl: String?
get() = if (mGalleryDetail != null && mGalleryDetail!!.gid != -1L && mGalleryDetail!!.token != null) {
EhUrl.getGalleryDetailUrl(
mGalleryDetail!!.gid,
mGalleryDetail!!.token,
0,
mShowAllComments
)
} else {
null
}
override fun onClick(v: View) {
val context = context
val activity = mainActivity
if (null == context || null == activity || null == mEditText) {
return
}
if (mFab === v) {
if (!mInAnimation) {
prepareNewComment()
showEditPanel(true)
}
} else if (mSendImage === v) {
if (!mInAnimation) {
val comment = mEditText!!.text.toString()
if (TextUtils.isEmpty(comment)) {
// Comment is empty
return
}
val url = galleryDetailUrl ?: return
// Request
val request = EhRequest()
.setMethod(EhClient.METHOD_GET_COMMENT_GALLERY)
.setArgs(
url,
comment,
if (mCommentId != 0L) mCommentId.toString() else null
)
.setCallback(CommentGalleryListener(context, mCommentId))
request.enqueue(this)
hideSoftInput()
hideEditPanel(true)
}
}
}
private fun onRefreshGallerySuccess(result: GalleryCommentList?) {
if (mGalleryDetail == null || mAdapter == null) {
return
}
mRefreshLayout!!.isRefreshing = false
mRefreshingComments = false
mGalleryDetail!!.comments = result
mAdapter!!.notifyDataSetChanged()
updateView(true)
}
private fun onRefreshGalleryFailure() {
if (mAdapter == null) {
return
}
mRefreshLayout!!.isRefreshing = false
mRefreshingComments = false
val position = mAdapter!!.itemCount - 1
if (position >= 0) {
mAdapter!!.notifyItemChanged(position)
}
}
private fun onCommentGallerySuccess(result: GalleryCommentList) {
if (mGalleryDetail == null || mAdapter == null) {
return
}
mGalleryDetail!!.comments = result
mAdapter!!.notifyDataSetChanged()
// Remove text
if (mEditText != null) {
mEditText!!.setText("")
}
updateView(true)
}
private fun onVoteCommentSuccess(result: VoteCommentParser.Result) {
if (mAdapter == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null) {
return
}
var position = -1
var i = 0
val n = mGalleryDetail!!.comments!!.comments!!.size
while (i < n) {
val comment = mGalleryDetail!!.comments!!.comments!![i]
if (comment.id == result.id) {
position = i
break
}
i++
}
if (-1 == position) {
Log.d(TAG, "Can't find comment with id " + result.id)
return
}
// Update comment
val comment = mGalleryDetail!!.comments!!.comments!![position]
comment.score = result.score
if (result.expectVote > 0) {
comment.voteUpEd = 0 != result.vote
comment.voteDownEd = false
} else {
comment.voteDownEd = 0 != result.vote
comment.voteUpEd = false
}
mAdapter!!.notifyItemChanged(position)
}
override fun onRefresh() {
if (!mRefreshingComments && mAdapter != null) {
val activity = requireActivity() as MainActivity
mRefreshingComments = true
val url = galleryDetailUrl
if (url != null) {
// Request
val request = EhRequest()
.setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)
.setArgs(url)
.setCallback(RefreshCommentListener(activity))
request.enqueue(this)
}
}
}
private class InfoHolder(itemView: View?) : RecyclerView.ViewHolder(
itemView!!
) {
val key: TextView
val value: TextView
init {
key = ViewUtils.`$$`(itemView, R.id.key) as TextView
value = ViewUtils.`$$`(itemView, R.id.value) as TextView
}
}
private abstract class CommentHolder(inflater: LayoutInflater, resId: Int, parent: ViewGroup?) :
RecyclerView.ViewHolder(inflater.inflate(resId, parent, false))
private class MoreCommentHolder(inflater: LayoutInflater, parent: ViewGroup?) :
CommentHolder(inflater, R.layout.item_gallery_comment_more, parent)
private class ProgressCommentHolder(inflater: LayoutInflater, parent: ViewGroup?) :
CommentHolder(inflater, R.layout.item_gallery_comment_progress, parent)
private inner class RefreshCommentListener(context: Context) :
EhCallback<GalleryCommentsScene?, GalleryDetail>(context) {
override fun onSuccess(result: GalleryDetail) {
val scene = this@GalleryCommentsScene
scene.onRefreshGallerySuccess(result.comments)
}
override fun onFailure(e: Exception) {
val scene = this@GalleryCommentsScene
scene.onRefreshGalleryFailure()
}
override fun onCancel() {}
}
private inner class CommentGalleryListener(context: Context, private val mCommentId: Long) :
EhCallback<GalleryCommentsScene?, GalleryCommentList>(context) {
override fun onSuccess(result: GalleryCommentList) {
showTip(
if (mCommentId != 0L) R.string.edit_comment_successfully else R.string.comment_successfully,
LENGTH_SHORT
)
val scene = this@GalleryCommentsScene
scene.onCommentGallerySuccess(result)
}
override fun onFailure(e: Exception) {
showTip(
"""
${content.getString(if (mCommentId != 0L) R.string.edit_comment_failed else R.string.comment_failed)}
${ExceptionUtils.getReadableString(e)}
""".trimIndent(), LENGTH_LONG
)
}
override fun onCancel() {}
}
private inner class VoteCommentListener(context: Context) :
EhCallback<GalleryCommentsScene?, VoteCommentParser.Result>(context) {
override fun onSuccess(result: VoteCommentParser.Result) {
showTip(
if (result.expectVote > 0) (if (0 != result.vote) R.string.vote_up_successfully else R.string.cancel_vote_up_successfully) else if (0 != result.vote) R.string.vote_down_successfully else R.string.cancel_vote_down_successfully,
LENGTH_SHORT
)
val scene = this@GalleryCommentsScene
scene.onVoteCommentSuccess(result)
}
override fun onFailure(e: Exception) {
showTip(R.string.vote_failed, LENGTH_LONG)
}
override fun onCancel() {}
}
private inner class ActualCommentHolder(inflater: LayoutInflater, parent: ViewGroup?) :
CommentHolder(inflater, R.layout.item_gallery_comment, parent) {
private val user: TextView = itemView.findViewById(R.id.user)
private val time: TextView = itemView.findViewById(R.id.time)
val comment: LinkifyTextView = itemView.findViewById(R.id.comment)
private fun generateComment(
context: Context,
textView: ObservedTextView,
comment: GalleryComment
): CharSequence {
val sp = Html.fromHtml(
comment.comment,
Html.FROM_HTML_MODE_LEGACY,
URLImageGetter(textView),
null
)
val ssb = SpannableStringBuilder(sp)
if (0L != comment.id && 0 != comment.score) {
val score = comment.score
val scoreString = if (score > 0) "+$score" else score.toString()
val ss = SpannableString(scoreString)
ss.setSpan(
RelativeSizeSpan(0.8f),
0,
scoreString.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ss.setSpan(
StyleSpan(Typeface.BOLD),
0,
scoreString.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ss.setSpan(
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
0,
scoreString.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ssb.append(" ").append(ss)
}
if (comment.lastEdited != 0L) {
val str = context.getString(
R.string.last_edited,
ReadableTime.getTimeAgo(comment.lastEdited)
)
val ss = SpannableString(str)
ss.setSpan(
RelativeSizeSpan(0.8f),
0,
str.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ss.setSpan(
StyleSpan(Typeface.BOLD),
0,
str.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ss.setSpan(
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
0, str.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ssb.append("\n\n").append(ss)
}
return TextUrl.handleTextUrl(ssb)
}
fun bind(value: GalleryComment) {
user.text = if (value.uploader) getString(
R.string.comment_user_uploader,
value.user
) else value.user
user.setOnClickListener {
val lub = ListUrlBuilder()
lub.mode = ListUrlBuilder.MODE_UPLOADER
lub.keyword = value.user
navigate(R.id.galleryListScene, lub.toStartArgs(), true)
}
time.text = ReadableTime.getTimeAgo(value.time)
comment.text = generateComment(comment.context, comment, value)
}
}
private inner class CommentAdapter : RecyclerView.Adapter<CommentHolder>() {
private val mInflater: LayoutInflater = layoutInflater
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentHolder {
return when (viewType) {
TYPE_COMMENT -> ActualCommentHolder(mInflater, parent)
TYPE_MORE -> MoreCommentHolder(mInflater, parent)
TYPE_PROGRESS -> ProgressCommentHolder(mInflater, parent)
else -> throw IllegalStateException("Invalid view type: $viewType")
}
}
override fun onBindViewHolder(holder: CommentHolder, position: Int) {
val context = context
if (context == null || mGalleryDetail == null || mGalleryDetail!!.comments == null) {
return
}
holder.itemView.setOnClickListener {
onItemClick(
mRecyclerView,
holder.itemView,
position
)
}
holder.itemView.isClickable = true
holder.itemView.isFocusable = true
if (holder is ActualCommentHolder) {
holder.bind(mGalleryDetail!!.comments!!.comments!![position])
}
}
override fun getItemCount(): Int {
return if (mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null) {
0
} else if (mGalleryDetail!!.comments!!.hasMore) {
mGalleryDetail!!.comments!!.comments!!.size + 1
} else {
mGalleryDetail!!.comments!!.comments!!.size
}
}
override fun getItemViewType(position: Int): Int {
return if (position >= mGalleryDetail!!.comments!!.comments!!.size) {
if (mRefreshingComments) TYPE_PROGRESS else TYPE_MORE
} else {
TYPE_COMMENT
}
}
}
internal inner class EditPanelOnBackPressedCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
if (!mInAnimation && null != mEditPanel && mEditPanel!!.visibility == View.VISIBLE) {
hideEditPanel(true)
}
}
}
companion object {
val TAG: String = GalleryCommentsScene::class.java.simpleName
const val KEY_API_UID = "api_uid"
const val KEY_API_KEY = "api_key"
const val KEY_GID = "gid"
const val KEY_TOKEN = "token"
const val KEY_COMMENT_LIST = "comment_list"
const val KEY_GALLERY_DETAIL = "gallery_detail"
private const val TYPE_COMMENT = 0
private const val TYPE_MORE = 1
private const val TYPE_PROGRESS = 2
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,59 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.card.MaterialCardView;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.widget.SimpleRatingView;
import com.hippo.widget.LoadImageView;
class GalleryHolder extends RecyclerView.ViewHolder {
public final LoadImageView thumb;
public final TextView title;
public final TextView uploader;
public final SimpleRatingView rating;
public final TextView category;
public final TextView posted;
public final TextView pages;
public final TextView simpleLanguage;
public final ImageView favourited;
public final ImageView downloaded;
public final MaterialCardView card;
public GalleryHolder(View itemView) {
super(itemView);
thumb = itemView.findViewById(R.id.thumb);
title = itemView.findViewById(R.id.title);
uploader = itemView.findViewById(R.id.uploader);
rating = itemView.findViewById(R.id.rating);
category = itemView.findViewById(R.id.category);
posted = itemView.findViewById(R.id.posted);
pages = itemView.findViewById(R.id.pages);
simpleLanguage = itemView.findViewById(R.id.simple_language);
favourited = itemView.findViewById(R.id.favourited);
downloaded = itemView.findViewById(R.id.downloaded);
card = itemView.findViewById(R.id.card);
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import com.hippo.ehviewer.R
import com.hippo.ehviewer.widget.SimpleRatingView
import com.hippo.widget.LoadImageView
internal class GalleryHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@JvmField
val thumb: LoadImageView = itemView.findViewById(R.id.thumb)
@JvmField
val title: TextView? = itemView.findViewById(R.id.title)
@JvmField
val uploader: TextView? = itemView.findViewById(R.id.uploader)
@JvmField
val rating: SimpleRatingView? = itemView.findViewById(R.id.rating)
@JvmField
val category: TextView = itemView.findViewById(R.id.category)
@JvmField
val posted: TextView? = itemView.findViewById(R.id.posted)
@JvmField
val pages: TextView? = itemView.findViewById(R.id.pages)
@JvmField
val simpleLanguage: TextView = itemView.findViewById(R.id.simple_language)
@JvmField
val favourited: ImageView? = itemView.findViewById(R.id.favourited)
@JvmField
val downloaded: ImageView? = itemView.findViewById(R.id.downloaded)
@JvmField
val card: MaterialCardView = itemView.findViewById(R.id.card)
}
@@ -1420,7 +1420,9 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
mAdapter.notifyItemRemoved(position)
}
}
}.setNegativeButton(android.R.string.cancel) { _, _ ->
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.cancel()
}.setOnCancelListener {
mAdapter.notifyItemChanged(position)
}.show()
}
@@ -1473,11 +1475,10 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
}
}
@JvmStatic
fun getStartArgs(lub: ListUrlBuilder): Bundle {
fun ListUrlBuilder.toStartArgs(): Bundle {
val args = Bundle()
args.putString(KEY_ACTION, ACTION_LIST_URL_BUILDER)
args.putParcelable(KEY_LIST_URL_BUILDER, lub)
args.putParcelable(KEY_LIST_URL_BUILDER, this)
return args
}
}
@@ -1,425 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.slider.Slider;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.GalleryPreview;
import com.hippo.ehviewer.client.data.PreviewSet;
import com.hippo.ehviewer.client.exception.EhException;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.widget.ContentLayout;
import com.hippo.widget.LoadImageView;
import com.hippo.widget.recyclerview.AutoGridLayoutManager;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.ViewUtils;
import java.util.ArrayList;
import java.util.Locale;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
public class GalleryPreviewsScene extends BaseToolbarScene {
public static final String KEY_GALLERY_INFO = "gallery_info";
private final static String KEY_HAS_FIRST_REFRESH = "has_first_refresh";
/*---------------
Whole life cycle
---------------*/
@Nullable
private EhClient mClient;
@Nullable
private GalleryInfo mGalleryInfo;
@Nullable
private EasyRecyclerView mRecyclerView;
@Nullable
private GalleryPreviewAdapter mAdapter;
@Nullable
private GalleryPreviewHelper mHelper;
private boolean mHasFirstRefresh = false;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context context = getContext();
AssertUtils.assertNotNull(context);
mClient = EhClient.INSTANCE;
if (savedInstanceState == null) {
onInit();
} else {
onRestore(savedInstanceState);
}
}
private void onInit() {
Bundle args = getArguments();
if (args == null) {
return;
}
mGalleryInfo = args.getParcelable(KEY_GALLERY_INFO);
}
private void onRestore(@NonNull Bundle savedInstanceState) {
mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO);
mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
boolean hasFirstRefresh;
if (mHelper != null && 1 == mHelper.getShownViewIndex()) {
hasFirstRefresh = false;
} else {
hasFirstRefresh = mHasFirstRefresh;
}
outState.putBoolean(KEY_HAS_FIRST_REFRESH, hasFirstRefresh);
outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo);
}
@NonNull
@Override
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
ContentLayout mContentLayout = (ContentLayout) inflater.inflate(
R.layout.scene_gallery_previews, container, false);
mContentLayout.hideFastScroll();
mRecyclerView = mContentLayout.getRecyclerView();
setLiftOnScrollTargetView(mRecyclerView);
Context context = getContext();
AssertUtils.assertNotNull(context);
mAdapter = new GalleryPreviewAdapter();
mRecyclerView.setAdapter(mAdapter);
int columnWidth = Settings.getThumbSize();
AutoGridLayoutManager layoutManager = new AutoGridLayoutManager(context, columnWidth, LayoutUtils.dp2pix(context, 16));
layoutManager.setStrategy(AutoGridLayoutManager.STRATEGY_SUITABLE_SIZE);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setClipToPadding(false);
int padding = LayoutUtils.dp2pix(context, 4);
MarginItemDecoration decoration = new MarginItemDecoration(padding, padding, padding, padding, padding);
mRecyclerView.addItemDecoration(decoration);
mHelper = new GalleryPreviewHelper();
mContentLayout.setHelper(mHelper);
// Only refresh for the first time
if (!mHasFirstRefresh) {
mHasFirstRefresh = true;
mHelper.firstRefresh();
}
return mContentLayout;
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (null != mHelper) {
if (1 == mHelper.getShownViewIndex()) {
mHasFirstRefresh = false;
}
}
if (null != mRecyclerView) {
mRecyclerView.stopScroll();
mRecyclerView = null;
}
mAdapter = null;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setTitle(R.string.gallery_previews);
setNavigationIcon(R.drawable.v_arrow_left_dark_x24);
if (mGalleryInfo != null) {
if (((GalleryDetail) mGalleryInfo).previewPages > 2)
showMenu(R.menu.scene_gallery_previews);
}
}
@Override
public boolean onMenuItemClick(@NonNull MenuItem item) {
Context context = getContext();
if (null == context) {
return false;
}
int id = item.getItemId();
if (id == R.id.action_go_to) {
if (mHelper == null) {
return true;
}
int pages = mHelper.getPages();
if (pages > 1 && mHelper.canGoTo()) {
GoToDialogHelper helper = new GoToDialogHelper(pages, mHelper.getPageForTop());
AlertDialog dialog = new BaseDialogBuilder(context).setTitle(R.string.go_to)
.setView(R.layout.dialog_go_to)
.setPositiveButton(android.R.string.ok, null)
.create();
dialog.show();
helper.setDialog(dialog);
}
return true;
}
return false;
}
public boolean onItemClick(int position) {
Context context = getContext();
if (null != context && null != mHelper && null != mGalleryInfo) {
GalleryPreview p = mHelper.getDataAtEx(position);
if (p != null) {
Intent intent = new Intent(context, ReaderActivity.class);
intent.setAction(ReaderActivity.ACTION_EH);
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, mGalleryInfo);
intent.putExtra(ReaderActivity.KEY_PAGE, p.position);
startActivity(intent);
}
}
return true;
}
private void onGetPreviewSetSuccess(Pair<PreviewSet, Integer> result, int taskId) {
if (null != mHelper && mHelper.isCurrentTask(taskId) && null != mGalleryInfo) {
PreviewSet previewSet = result.first;
int size = previewSet.size();
ArrayList<GalleryPreview> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add(previewSet.getGalleryPreview(mGalleryInfo.getGid(), i));
}
mHelper.onGetPageData(taskId, result.second, 0, null, null, list);
}
}
private void onGetPreviewSetFailure(Exception e, int taskId) {
if (mHelper != null && mHelper.isCurrentTask(taskId)) {
mHelper.onGetException(taskId, e);
}
}
private static class GalleryPreviewHolder extends RecyclerView.ViewHolder {
public LoadImageView image;
public TextView text;
public GalleryPreviewHolder(View itemView) {
super(itemView);
image = itemView.findViewById(R.id.image);
text = itemView.findViewById(R.id.text);
}
}
private class GetPreviewSetListener extends EhCallback<GalleryPreviewsScene, Pair<PreviewSet, Integer>> {
private final int mTaskId;
public GetPreviewSetListener(Context context, int stageId, String sceneTag, int taskId) {
super(context);
mTaskId = taskId;
}
@Override
public void onSuccess(Pair<PreviewSet, Integer> result) {
GalleryPreviewsScene scene = GalleryPreviewsScene.this;
scene.onGetPreviewSetSuccess(result, mTaskId);
}
@Override
public void onFailure(Exception e) {
GalleryPreviewsScene scene = GalleryPreviewsScene.this;
scene.onGetPreviewSetFailure(e, mTaskId);
}
@Override
public void onCancel() {
}
}
private class GalleryPreviewAdapter extends RecyclerView.Adapter<GalleryPreviewHolder> {
private final LayoutInflater mInflater;
public GalleryPreviewAdapter() {
mInflater = getLayoutInflater();
AssertUtils.assertNotNull(mInflater);
}
@NonNull
@Override
public GalleryPreviewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new GalleryPreviewHolder(mInflater.inflate(R.layout.item_gallery_preview, parent, false));
}
@Override
@SuppressLint("SetTextI18n")
public void onBindViewHolder(@NonNull GalleryPreviewHolder holder, int position) {
if (null != mHelper) {
GalleryPreview preview = mHelper.getDataAtEx(position);
if (preview != null) {
preview.load(holder.image);
holder.text.setText(Integer.toString(preview.position + 1));
}
}
holder.itemView.setOnClickListener(v -> onItemClick(position));
}
@Override
public int getItemCount() {
return mHelper != null ? mHelper.size() : 0;
}
}
private class GalleryPreviewHelper extends ContentLayout.ContentHelper<GalleryPreview> {
@Override
protected void getPageData(final int taskId, int type, int page, String index, boolean isNext) {
MainActivity activity = getMainActivity();
if (null == activity || null == mClient || null == mGalleryInfo) {
onGetException(taskId, new EhException(getString(R.string.error_cannot_find_gallery)));
return;
}
String url = EhUrl.getGalleryDetailUrl(mGalleryInfo.getGid(), mGalleryInfo.getToken(), page, false);
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_GET_PREVIEW_SET);
request.setCallback(new GetPreviewSetListener(getContext(),
1, getTag(), taskId));
request.setArgs(url);
request.enqueue(GalleryPreviewsScene.this);
}
@Override
protected Context getContext() {
return GalleryPreviewsScene.this.getContext();
}
@SuppressLint("NotifyDataSetChanged")
@Override
protected void notifyDataSetChanged() {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
@Override
protected void notifyItemRangeInserted(int positionStart, int itemCount) {
if (mAdapter != null) {
mAdapter.notifyItemRangeInserted(positionStart, itemCount);
}
}
@Override
protected boolean isDuplicate(GalleryPreview d1, GalleryPreview d2) {
return false;
}
}
private class GoToDialogHelper implements View.OnClickListener,
DialogInterface.OnDismissListener {
private final int mPages;
private final int mCurrentPage;
@Nullable
private Slider mSlider;
@Nullable
private Dialog mDialog;
private GoToDialogHelper(int pages, int currentPage) {
mPages = pages;
mCurrentPage = currentPage;
}
public void setDialog(@NonNull AlertDialog dialog) {
mDialog = dialog;
((TextView) ViewUtils.$$(dialog, R.id.start)).setText(String.format(Locale.US, "%d", 1));
((TextView) ViewUtils.$$(dialog, R.id.end)).setText(String.format(Locale.US, "%d", mPages));
mSlider = (Slider) ViewUtils.$$(dialog, R.id.slider);
mSlider.setValueTo(mPages);
mSlider.setValue(mCurrentPage + 1);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this);
dialog.setOnDismissListener(this);
}
@Override
public void onClick(View v) {
if (null == mSlider) {
return;
}
int page = (int) (mSlider.getValue() - 1);
if (page >= 0 && page < mPages && mHelper != null) {
mHelper.goTo(page);
if (mDialog != null) {
mDialog.dismiss();
mDialog = null;
}
} else {
showTip(R.string.error_out_of_range, LENGTH_LONG);
}
}
@Override
public void onDismiss(DialogInterface dialog) {
mDialog = null;
mSlider = null;
}
}
}
@@ -0,0 +1,349 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.util.Pair
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.slider.Slider
import com.hippo.app.BaseDialogBuilder
import com.hippo.easyrecyclerview.MarginItemDecoration
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhRequest
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.data.PreviewSet
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.databinding.SceneGalleryPreviewsBinding
import com.hippo.widget.ContentLayout.ContentHelper
import com.hippo.widget.LoadImageView
import com.hippo.widget.recyclerview.AutoGridLayoutManager
import com.hippo.yorozuya.LayoutUtils
import com.hippo.yorozuya.ViewUtils
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import java.util.Locale
class GalleryPreviewsScene : BaseToolbarScene() {
private var _binding: SceneGalleryPreviewsBinding? = null
private val binding
get() = _binding!!
private var mGalleryInfo: GalleryInfo? = null
private var mAdapter: GalleryPreviewAdapter? = null
private var mHelper: GalleryPreviewHelper? = null
private var mHasFirstRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
onInit()
} else {
onRestore(savedInstanceState)
}
}
private fun onInit() {
val args = arguments ?: return
mGalleryInfo = args.getParcelable(KEY_GALLERY_INFO)
}
private fun onRestore(savedInstanceState: Bundle) {
mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO)
mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val hasFirstRefresh: Boolean = if (mHelper != null && 1 == mHelper!!.shownViewIndex) {
false
} else {
mHasFirstRefresh
}
outState.putBoolean(KEY_HAS_FIRST_REFRESH, hasFirstRefresh)
outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo)
}
override fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = SceneGalleryPreviewsBinding.inflate(inflater, container, false)
binding.contentLayout.hideFastScroll()
setLiftOnScrollTargetView(binding.contentLayout)
mAdapter = GalleryPreviewAdapter()
binding.contentLayout.recyclerView.adapter = mAdapter
val columnWidth = Settings.getThumbSize()
val layoutManager =
AutoGridLayoutManager(context, columnWidth, LayoutUtils.dp2pix(context, 16f))
layoutManager.setStrategy(AutoGridLayoutManager.STRATEGY_SUITABLE_SIZE)
binding.contentLayout.recyclerView.layoutManager = layoutManager
binding.contentLayout.recyclerView.clipToPadding = false
val padding = LayoutUtils.dp2pix(context, 4f)
val decoration = MarginItemDecoration(padding, padding, padding, padding, padding)
binding.contentLayout.recyclerView.addItemDecoration(decoration)
mHelper = GalleryPreviewHelper()
binding.contentLayout.setHelper(mHelper)
// Only refresh for the first time
if (!mHasFirstRefresh) {
mHasFirstRefresh = true
mHelper?.firstRefresh()
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
if (null != mHelper) {
if (1 == mHelper!!.shownViewIndex) {
mHasFirstRefresh = false
}
}
binding.contentLayout.recyclerView.stopScroll()
_binding = null
mAdapter = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(R.string.gallery_previews)
setNavigationIcon(R.drawable.v_arrow_left_dark_x24)
if (mGalleryInfo != null) {
if ((mGalleryInfo as GalleryDetail).previewPages > 2) showMenu(R.menu.scene_gallery_previews)
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
val context = context ?: return false
val id = item.itemId
if (id == R.id.action_go_to) {
if (mHelper == null) {
return true
}
val pages = mHelper!!.pages
if (pages > 1 && mHelper!!.canGoTo()) {
val helper = GoToDialogHelper(pages, mHelper!!.pageForTop)
val dialog = BaseDialogBuilder(context).setTitle(R.string.go_to)
.setView(R.layout.dialog_go_to)
.setPositiveButton(android.R.string.ok, null)
.create()
dialog.show()
helper.setDialog(dialog)
}
return true
}
return false
}
fun onItemClick(position: Int): Boolean {
val context = context
if (null != context && null != mHelper && null != mGalleryInfo) {
val p = mHelper!!.getDataAtEx(position)
if (p != null) {
val intent = Intent(context, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_EH
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, mGalleryInfo)
intent.putExtra(ReaderActivity.KEY_PAGE, p.position)
startActivity(intent)
}
}
return true
}
private fun onGetPreviewSetSuccess(result: Pair<PreviewSet, Int>, taskId: Int) {
if (null != mHelper && mHelper!!.isCurrentTask(taskId) && null != mGalleryInfo) {
val previewSet = result.first
val size = previewSet.size()
val list = ArrayList<GalleryPreview>(size)
for (i in 0 until size) {
list.add(previewSet.getGalleryPreview(mGalleryInfo!!.gid, i))
}
mHelper!!.onGetPageData(
taskId, result.second, 0, null, null,
list as List<GalleryPreview?>?
)
}
}
private fun onGetPreviewSetFailure(e: Exception, taskId: Int) {
if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {
mHelper!!.onGetException(taskId, e)
}
}
private class GalleryPreviewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var image: LoadImageView
var text: TextView
init {
image = itemView.findViewById(R.id.image)
text = itemView.findViewById(R.id.text)
}
}
private inner class GetPreviewSetListener(
context: Context,
private val mTaskId: Int
) : EhCallback<GalleryPreviewsScene, Pair<PreviewSet, Int>>(context) {
override fun onSuccess(result: Pair<PreviewSet, Int>) {
val scene = this@GalleryPreviewsScene
scene.onGetPreviewSetSuccess(result, mTaskId)
}
override fun onFailure(e: Exception) {
val scene = this@GalleryPreviewsScene
scene.onGetPreviewSetFailure(e, mTaskId)
}
override fun onCancel() {}
}
private inner class GalleryPreviewAdapter : RecyclerView.Adapter<GalleryPreviewHolder>() {
private val mInflater: LayoutInflater = layoutInflater
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryPreviewHolder {
return GalleryPreviewHolder(
mInflater.inflate(
R.layout.item_gallery_preview,
parent,
false
)
)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: GalleryPreviewHolder, position: Int) {
if (null != mHelper) {
val preview = mHelper!!.getDataAtEx(position)
if (preview != null) {
preview.load(holder.image)
holder.text.text = (preview.position + 1).toString()
}
}
holder.itemView.setOnClickListener { onItemClick(position) }
}
override fun getItemCount(): Int {
return if (mHelper != null) mHelper!!.size() else 0
}
}
private inner class GalleryPreviewHelper : ContentHelper<GalleryPreview>() {
override fun getPageData(
taskId: Int,
type: Int,
page: Int,
index: String?,
isNext: Boolean
) {
val activity = mainActivity
if (null == activity || null == mGalleryInfo) {
onGetException(taskId, EhException(getString(R.string.error_cannot_find_gallery)))
return
}
val url =
EhUrl.getGalleryDetailUrl(mGalleryInfo!!.gid, mGalleryInfo!!.token, page, false)
val request = EhRequest()
request.setMethod(EhClient.METHOD_GET_PREVIEW_SET)
request.setCallback(
GetPreviewSetListener(
context,
taskId
)
)
request.setArgs(url)
request.enqueue(this@GalleryPreviewsScene)
}
override fun getContext(): Context {
return this@GalleryPreviewsScene.requireContext()
}
@SuppressLint("NotifyDataSetChanged")
override fun notifyDataSetChanged() {
if (mAdapter != null) {
mAdapter!!.notifyDataSetChanged()
}
}
override fun notifyItemRangeInserted(positionStart: Int, itemCount: Int) {
if (mAdapter != null) {
mAdapter!!.notifyItemRangeInserted(positionStart, itemCount)
}
}
override fun isDuplicate(d1: GalleryPreview, d2: GalleryPreview): Boolean {
return false
}
}
private inner class GoToDialogHelper(private val mPages: Int, private val mCurrentPage: Int) :
View.OnClickListener, DialogInterface.OnDismissListener {
private var mSlider: Slider? = null
private var mDialog: Dialog? = null
fun setDialog(dialog: AlertDialog) {
mDialog = dialog
(ViewUtils.`$$`(dialog, R.id.start) as TextView).text =
String.format(Locale.US, "%d", 1)
(ViewUtils.`$$`(dialog, R.id.end) as TextView).text =
String.format(Locale.US, "%d", mPages)
mSlider = ViewUtils.`$$`(dialog, R.id.slider) as Slider
mSlider!!.valueTo = mPages.toFloat()
mSlider!!.value = (mCurrentPage + 1).toFloat()
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this)
dialog.setOnDismissListener(this)
}
override fun onClick(v: View) {
if (null == mSlider) {
return
}
val page = (mSlider!!.value - 1).toInt()
if (page in 0 until mPages && mHelper != null) {
mHelper!!.goTo(page)
if (mDialog != null) {
mDialog!!.dismiss()
mDialog = null
}
} else {
showTip(R.string.error_out_of_range, LENGTH_LONG)
}
}
override fun onDismiss(dialog: DialogInterface) {
mDialog = null
mSlider = null
}
}
companion object {
const val KEY_GALLERY_INFO = "gallery_info"
private const val KEY_HAS_FIRST_REFRESH = "has_first_refresh"
}
}
@@ -318,7 +318,7 @@ class HistoryScene : BaseToolbarScene() {
return true
}
private class AddToFavoriteListener(context: Context?) :
private class AddToFavoriteListener(context: Context) :
EhCallback<GalleryListScene?, Void?>(context) {
override fun onSuccess(result: Void?) {
showTip(R.string.add_to_favorite_success, LENGTH_SHORT)
@@ -331,7 +331,7 @@ class HistoryScene : BaseToolbarScene() {
override fun onCancel() {}
}
private class RemoveFromFavoriteListener(context: Context?) :
private class RemoveFromFavoriteListener(context: Context) :
EhCallback<GalleryListScene?, Void?>(context) {
override fun onSuccess(result: Void?) {
showTip(R.string.remove_from_favorite_success, LENGTH_SHORT)
@@ -1,248 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.util.ExceptionUtils;
import com.hippo.view.ViewTransition;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.ViewUtils;
/**
* Only show a progress with jobs in background
*/
public final class ProgressScene extends BaseScene implements View.OnClickListener {
public static final String KEY_ACTION = "action";
public static final String ACTION_GALLERY_TOKEN = "gallery_token";
public static final String KEY_GID = "gid";
public static final String KEY_PTOKEN = "ptoken";
public static final String KEY_PAGE = "page";
private static final String KEY_VALID = "valid";
private static final String KEY_ERROR = "error";
private boolean mValid;
private String mError;
private String mAction;
private long mGid;
private String mPToken;
private int mPage;
@Nullable
private TextView mTip;
@Nullable
private ViewTransition mViewTransition;
@Override
public boolean needShowLeftDrawer() {
return false;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
onInit();
} else {
onRestore(savedInstanceState);
}
}
private boolean doJobs() {
Context context = getContext();
MainActivity activity = getMainActivity();
if (null == context || null == activity) {
return false;
}
if (ACTION_GALLERY_TOKEN.equals(mAction)) {
if (mGid == -1 || mPToken == null || mPage == -1) {
return false;
}
EhRequest request = new EhRequest()
.setMethod(EhClient.METHOD_GET_GALLERY_TOKEN)
.setArgs(mGid, mPToken, mPage)
.setCallback(new GetGalleryTokenListener(context,
1, getTag()));
request.enqueue(this);
return true;
}
return false;
}
private boolean handleArgs(Bundle args) {
if (args == null) {
return false;
}
mAction = args.getString(KEY_ACTION);
if (ACTION_GALLERY_TOKEN.equals(mAction)) {
mGid = args.getLong(KEY_GID, -1);
mPToken = args.getString(KEY_PTOKEN, null);
mPage = args.getInt(KEY_PAGE, -1);
return mGid != -1 && mPToken != null && mPage != -1;
}
return false;
}
private void onInit() {
mValid = handleArgs(getArguments());
if (mValid) {
mValid = doJobs();
}
if (!mValid) {
mError = getString(R.string.error_something_wrong_happened);
}
}
private void onRestore(@NonNull Bundle savedInstanceState) {
mValid = savedInstanceState.getBoolean(KEY_VALID);
mError = savedInstanceState.getString(KEY_ERROR);
mAction = savedInstanceState.getString(KEY_ACTION);
mGid = savedInstanceState.getLong(KEY_GID, -1);
mPToken = savedInstanceState.getString(KEY_PTOKEN, null);
mPage = savedInstanceState.getInt(KEY_PAGE, -1);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_VALID, mValid);
outState.putString(KEY_ERROR, mError);
outState.putString(KEY_ACTION, mAction);
outState.putLong(KEY_GID, mGid);
outState.putString(KEY_PTOKEN, mPToken);
outState.putInt(KEY_PAGE, mPage);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_progress, container, false);
View progress = ViewUtils.$$(view, R.id.progress);
mTip = (TextView) ViewUtils.$$(view, R.id.tip);
Context context = getContext();
AssertUtils.assertNotNull(context);
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
mTip.setCompoundDrawables(null, drawable, null, null);
mTip.setOnClickListener(this);
mTip.setText(mError);
mViewTransition = new ViewTransition(progress, mTip);
if (mValid) {
mViewTransition.showView(0, false);
} else {
mViewTransition.showView(1, false);
}
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mTip = null;
mViewTransition = null;
}
@Override
public void onClick(View v) {
if (mTip == v) {
if (doJobs()) {
mValid = true;
// Show progress
if (null != mViewTransition) {
mViewTransition.showView(0, true);
}
}
}
}
private void onGetGalleryTokenSuccess(String result) {
Bundle arg = new Bundle();
arg.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN);
arg.putLong(GalleryDetailScene.KEY_GID, mGid);
arg.putString(GalleryDetailScene.KEY_TOKEN, result);
arg.putInt(GalleryDetailScene.KEY_PAGE, mPage);
navigate(R.id.galleryDetailScene, arg);
}
private void onGetGalleryTokenFailure(Exception e) {
mValid = false;
Context context = getContext();
if (null != context && null != mViewTransition && null != mTip) {
// Show tip
mError = ExceptionUtils.getReadableString(e);
mViewTransition.showView(1);
mTip.setText(mError);
}
}
private class GetGalleryTokenListener extends EhCallback<ProgressScene, String> {
public GetGalleryTokenListener(Context context, int stageId, String sceneTag) {
super(context);
}
@Override
public void onSuccess(String result) {
ProgressScene scene = ProgressScene.this;
scene.onGetGalleryTokenSuccess(result);
}
@Override
public void onFailure(Exception e) {
ProgressScene scene = ProgressScene.this;
scene.onGetGalleryTokenFailure(e);
}
@Override
public void onCancel() {
}
}
}
@@ -0,0 +1,211 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhRequest
import com.hippo.util.ExceptionUtils
import com.hippo.view.ViewTransition
import com.hippo.yorozuya.AssertUtils
import com.hippo.yorozuya.ViewUtils
/**
* Only show a progress with jobs in background
*/
class ProgressScene : BaseScene(), View.OnClickListener {
private var mValid = false
private var mError: String? = null
private var mAction: String? = null
private var mGid: Long = 0
private var mPToken: String? = null
private var mPage = 0
private var mTip: TextView? = null
private var mViewTransition: ViewTransition? = null
override fun needShowLeftDrawer(): Boolean {
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
onInit()
} else {
onRestore(savedInstanceState)
}
}
private fun doJobs(): Boolean {
val context = context
val activity = mainActivity
if (null == context || null == activity) {
return false
}
if (ACTION_GALLERY_TOKEN == mAction) {
if (mGid == -1L || mPToken == null || mPage == -1) {
return false
}
val request = EhRequest()
.setMethod(EhClient.METHOD_GET_GALLERY_TOKEN)
.setArgs(mGid, mPToken!!, mPage)
.setCallback(
GetGalleryTokenListener(
context
)
)
request.enqueue(this)
return true
}
return false
}
private fun handleArgs(args: Bundle?): Boolean {
if (args == null) {
return false
}
mAction = args.getString(KEY_ACTION)
if (ACTION_GALLERY_TOKEN == mAction) {
mGid = args.getLong(KEY_GID, -1)
mPToken = args.getString(KEY_PTOKEN, null)
mPage = args.getInt(KEY_PAGE, -1)
return mGid != -1L && mPToken != null && mPage != -1
}
return false
}
private fun onInit() {
mValid = handleArgs(arguments)
if (mValid) {
mValid = doJobs()
}
if (!mValid) {
mError = getString(R.string.error_something_wrong_happened)
}
}
private fun onRestore(savedInstanceState: Bundle) {
mValid = savedInstanceState.getBoolean(KEY_VALID)
mError = savedInstanceState.getString(KEY_ERROR)
mAction = savedInstanceState.getString(KEY_ACTION)
mGid = savedInstanceState.getLong(KEY_GID, -1)
mPToken = savedInstanceState.getString(KEY_PTOKEN, null)
mPage = savedInstanceState.getInt(KEY_PAGE, -1)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_VALID, mValid)
outState.putString(KEY_ERROR, mError)
outState.putString(KEY_ACTION, mAction)
outState.putLong(KEY_GID, mGid)
outState.putString(KEY_PTOKEN, mPToken)
outState.putInt(KEY_PAGE, mPage)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.scene_progress, container, false)
val progress = ViewUtils.`$$`(view, R.id.progress)
mTip = ViewUtils.`$$`(view, R.id.tip) as TextView
val context = context
AssertUtils.assertNotNull(context)
val drawable = ContextCompat.getDrawable(context!!, R.drawable.big_sad_pandroid)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
mTip!!.setCompoundDrawables(null, drawable, null, null)
mTip!!.setOnClickListener(this)
mTip!!.text = mError
mViewTransition = ViewTransition(progress, mTip)
if (mValid) {
mViewTransition!!.showView(0, false)
} else {
mViewTransition!!.showView(1, false)
}
return view
}
override fun onDestroyView() {
super.onDestroyView()
mTip = null
mViewTransition = null
}
override fun onClick(v: View) {
if (mTip === v) {
if (doJobs()) {
mValid = true
// Show progress
if (null != mViewTransition) {
mViewTransition!!.showView(0, true)
}
}
}
}
private fun onGetGalleryTokenSuccess(result: String) {
val arg = Bundle()
arg.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)
arg.putLong(GalleryDetailScene.KEY_GID, mGid)
arg.putString(GalleryDetailScene.KEY_TOKEN, result)
arg.putInt(GalleryDetailScene.KEY_PAGE, mPage)
navigate(R.id.galleryDetailScene, arg)
}
private fun onGetGalleryTokenFailure(e: Exception) {
mValid = false
val context = context
if (null != context && null != mViewTransition && null != mTip) {
// Show tip
mError = ExceptionUtils.getReadableString(e)
mViewTransition!!.showView(1)
mTip!!.text = mError
}
}
private inner class GetGalleryTokenListener(
context: Context
) : EhCallback<ProgressScene, String>(context) {
override fun onSuccess(result: String) {
val scene = this@ProgressScene
scene.onGetGalleryTokenSuccess(result)
}
override fun onFailure(e: Exception) {
val scene = this@ProgressScene
scene.onGetGalleryTokenFailure(e)
}
override fun onCancel() {}
}
companion object {
const val KEY_ACTION = "action"
const val ACTION_GALLERY_TOKEN = "gallery_token"
const val KEY_GID = "gid"
const val KEY_PTOKEN = "ptoken"
const val KEY_PAGE = "page"
private const val KEY_VALID = "valid"
private const val KEY_ERROR = "error"
}
}
@@ -1,36 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
public final class TransitionNameFactory {
public static String getThumbTransitionName(long gid) {
return "thumb:" + gid;
}
public static String getTitleTransitionName(long gid) {
return "title:" + gid;
}
public static String getUploaderTransitionName(long gid) {
return "uploader:" + gid;
}
public static String getCategoryTransitionName(long gid) {
return "category:" + gid;
}
}
+34 -7
View File
@@ -25,8 +25,14 @@ import android.graphics.ImageDecoder.Source
import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import coil.decode.FrameDelayRewritingSource
import com.hippo.Native
import com.hippo.UriArchiveAccessor
import com.hippo.ehviewer.EhApplication
import okio.Buffer
import okio.BufferedSource
import okio.buffer
import okio.source
import java.io.FileInputStream
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
@@ -80,20 +86,41 @@ class Image private constructor(source: Source, private val byteBuffer: ByteBuff
@Throws(DecodeException::class)
@JvmStatic
fun decode(stream: FileInputStream): Image {
val src = ImageDecoder.createSource(
stream.channel.map(
FileChannel.MapMode.READ_ONLY, 0,
stream.available().toLong()
)
val buffer = stream.channel.map(
FileChannel.MapMode.READ_ONLY, 0,
stream.available().toLong()
)
val source = if (checkIsGif(buffer)) {
rewriteSource(stream.source().buffer())
} else {
buffer
}
val src = ImageDecoder.createSource(source)
return Image(src)
}
@Throws(DecodeException::class)
@JvmStatic
fun decode(buffer: ByteBuffer): Image {
val src = ImageDecoder.createSource(buffer)
return Image(src, buffer)
return if (checkIsGif(buffer)) {
val rewritten = rewriteSource(Buffer().apply {
write(buffer)
UriArchiveAccessor.releaseByteBuffer(buffer)
})
Image(ImageDecoder.createSource(rewritten))
} else {
Image(ImageDecoder.createSource(buffer), buffer)
}
}
private fun rewriteSource(source: BufferedSource): ByteBuffer {
val bufferedSource = FrameDelayRewritingSource(source).buffer()
return ByteBuffer.wrap(bufferedSource.use { it.readByteArray() })
}
private fun checkIsGif(buffer: ByteBuffer): Boolean {
check(buffer.isDirect)
return Native.isGif(buffer)
}
}
}
@@ -1,181 +0,0 @@
/*
* Copyright 2017 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.network
import android.content.Context
import com.hippo.yorozuya.ObjectUtils
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import java.util.regex.Pattern
open class CookieRepository(context: Context, name: String) : CookieJar {
private val db: CookieDatabase
private val map: MutableMap<String, CookieSet>
@Synchronized
fun addCookie(cookie: Cookie) {
// For cookie database
var toAdd: Cookie? = null
var toUpdate: Cookie? = null
var toRemove: Cookie? = null
var set = map[cookie.domain]
if (set == null) {
set = CookieSet()
map[cookie.domain] = set
}
if (cookie.expiresAt <= System.currentTimeMillis()) {
toRemove = set.remove(cookie)
// If the cookie is not persistent, it's not in database
if (toRemove != null && !toRemove.persistent) {
toRemove = null
}
} else {
toAdd = cookie
toUpdate = set.add(cookie)
// If the cookie is not persistent, it's not in database
if (!toAdd.persistent) toAdd = null
if (toUpdate != null && !toUpdate.persistent) toUpdate = null
// Remove the cookie if it updates to null
if (toAdd == null && toUpdate != null) {
toRemove = toUpdate
toUpdate = null
}
}
if (toRemove != null) {
db.remove(toRemove)
}
if (toAdd != null) {
if (toUpdate != null) {
db.update(toUpdate, toAdd)
} else {
db.add(toAdd)
}
}
}
fun getCookieHeader(url: HttpUrl): String {
val cookies = getCookies(url)
val cookieHeader = StringBuilder()
var i = 0
val size = cookies.size
while (i < size) {
if (i > 0) {
cookieHeader.append("; ")
}
val cookie = cookies[i]
cookieHeader.append(cookie.name).append('=').append(cookie.value)
i++
}
return cookieHeader.toString()
}
@Synchronized
fun getCookies(url: HttpUrl): List<Cookie> {
val accepted: MutableList<Cookie> = ArrayList()
val expired: MutableList<Cookie> = ArrayList()
for ((domain, cookieSet) in map) {
if (domainMatch(url, domain)) {
cookieSet[url, accepted, expired]
}
}
for (cookie in expired) {
if (cookie.persistent) {
db.remove(cookie)
}
}
// RFC 6265 Section-5.4 step 2, sort the cookie-list
// Cookies with longer paths are listed before cookies with shorter paths.
// Ignore creation-time, we don't store them.
accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }
return accepted
}
fun contains(url: HttpUrl, name: String?): Boolean {
for (cookie in getCookies(url)) {
if (ObjectUtils.equal(cookie.name, name)) {
return true
}
}
return false
}
/**
* Remove all cookies in this `CookieRepository`.
*/
@Synchronized
fun clear() {
map.clear()
db.clear()
}
@Synchronized
fun close() {
db.close()
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
addCookie(cookie)
}
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return getCookies(url)
}
companion object {
/**
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
* of Android's private InetAddress#isNumeric API.
*
*
* This matches IPv6 addresses as a hex string containing at least one colon, and possibly
* including dots after the first colon. It matches IPv4 addresses as strings containing only
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
* verification).
*/
private val VERIFY_AS_IP_ADDRESS =
Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
/**
* Returns true if `host` is not a host name and might be an IP address.
*/
private fun verifyAsIpAddress(host: String): Boolean {
return VERIFY_AS_IP_ADDRESS.matcher(host).matches()
}
// okhttp3.Cookie.domainMatch(HttpUrl, String)
@JvmStatic
protected fun domainMatch(url: HttpUrl, domain: String?): Boolean {
val urlHost = url.host
return if (urlHost == domain) {
true // As in 'example.com' matching 'example.com'.
} else urlHost.endsWith(domain!!)
&& urlHost[urlHost.length - domain.length - 1] == '.' && !verifyAsIpAddress(
urlHost
)
// As in 'example.com' matching 'www.example.com'.
}
}
init {
db = CookieDatabase(context, name)
map = db.allCookies
}
}
@@ -19,7 +19,9 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/gallery_detail_header" />
<include
android:id="@+id/header"
layout="@layout/gallery_detail_header" />
<FrameLayout
android:layout_width="match_parent"
@@ -31,13 +33,21 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/gallery_detail_actions" />
<include
android:id="@+id/actions"
layout="@layout/gallery_detail_actions" />
<include layout="@layout/gallery_detail_tags" />
<include
android:id="@+id/tags"
layout="@layout/gallery_detail_tags" />
<include layout="@layout/gallery_detail_comments" />
<include
android:id="@+id/comments"
layout="@layout/gallery_detail_comments" />
<include layout="@layout/gallery_detail_previews" />
<include
android:id="@+id/previews"
layout="@layout/gallery_detail_previews" />
</LinearLayout>
+2 -2
View File
@@ -16,14 +16,14 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Material3.CardView.Filled"
style="@style/Widget.Material3.CardView.Outlined"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:checkable="true"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="@android:color/transparent">
app:strokeColor="@android:color/transparent">
<RelativeLayout
android:layout_width="match_parent"
@@ -26,7 +26,9 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/gallery_detail_content" />
<include
android:id="@+id/content"
layout="@layout/gallery_detail_content" />
</androidx.core.widget.NestedScrollView>
-4
View File
@@ -141,8 +141,4 @@
<attr name="summary" format="reference|string" />
</declare-styleable>
<declare-styleable name="DirectionalViewPager">
<attr name="android:orientation" />
</declare-styleable>
</resources>
+1
View File
@@ -12,6 +12,7 @@ dependencyResolutionManagement {
mavenCentral()
maven { url "https://jcenter.bintray.com" }
maven { url "https://jitpack.io" }
maven { url "https://androidx.dev/storage/compose-compiler/repository/" }
}
}
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")