Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f695c4598a | ||
|
|
eada1bc023 | ||
|
|
0ebb55551c | ||
|
|
20bce419c9 | ||
|
|
ca3fc370c3 | ||
|
|
94a8b78d1a | ||
|
|
06e9ff6fcf | ||
|
|
699e74656d | ||
|
|
b3ad6ef8b8 | ||
|
|
673c8e25f5 | ||
|
|
333c8e812d | ||
|
|
f0f7a88cfb | ||
|
|
0661d78bce | ||
|
|
dc4aece272 | ||
|
|
57a312b0b4 | ||
|
|
b9897ed6ac | ||
|
|
733230f037 | ||
|
|
fc6ba3e6ad | ||
|
|
2d7c539c5f | ||
|
|
7f34965026 | ||
|
|
de8b915cff | ||
|
|
d56040581d | ||
|
|
9869b7bbb5 | ||
|
|
7df97c28c0 | ||
|
|
018a559f78 | ||
|
|
3b32ad5fed | ||
|
|
f796d2a842 | ||
|
|
2f7e84e126 | ||
|
|
996ddbf3db | ||
|
|
7779ba2ac5 | ||
|
|
4dadeab762 | ||
|
|
f4d3131ef3 | ||
|
|
efa2ac803e | ||
|
|
e0be9d46bd | ||
|
|
106b27dbec | ||
|
|
f764567523 | ||
|
|
245f8669bc | ||
|
|
b265d9bba6 | ||
|
|
72228574f9 | ||
|
|
fba1ac3d90 | ||
|
|
4a73fb565e | ||
|
|
13011ed4af | ||
|
|
b2eccdefd3 | ||
|
|
dd2d6a977b | ||
|
|
38cd1b6040 | ||
|
|
1a28bbcde8 | ||
|
|
91013daadc | ||
|
|
1ad435e79d | ||
|
|
fd5c300c53 | ||
|
|
aa7d9cf29c | ||
|
|
8db1c3400e | ||
|
|
e723336706 | ||
|
|
b5cd4c36ab | ||
|
|
d1928f2647 | ||
|
|
61621ae00b |
No files matched your search
+21
-61
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -141,8 +141,4 @@
|
||||
<attr name="summary" format="reference|string" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="DirectionalViewPager">
|
||||
<attr name="android:orientation" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user