Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
148b1a6344 | ||
|
|
1fe0b079cd | ||
|
|
87505aa989 | ||
|
|
3d7a71647e | ||
|
|
c9d2f698b6 | ||
|
|
233adbddae | ||
|
|
6900d8a8c9 | ||
|
|
640f75d8d9 | ||
|
|
4381eb8c3d | ||
|
|
3026b16d70 | ||
|
|
ac3662a469 | ||
|
|
912e6c5a5b | ||
|
|
6a73a51f63 | ||
|
|
768b6bdda7 | ||
|
|
9ea7cd5f7c | ||
|
|
a91547ace7 | ||
|
|
ed58085b8f | ||
|
|
944e267795 | ||
|
|
325d6df2d3 | ||
|
|
3ba08bf003 | ||
|
|
86d00cc0d1 | ||
|
|
4ee518451c | ||
|
|
7b167b1612 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
name: EhViewer
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Update ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: arm64-${{ github.sha }}
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
|
||||
- name: Update x86_64
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Pull Request Check
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Java 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Setup Android SDK Tools
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- run: |
|
||||
git submodule update --init --recursive
|
||||
gradle assembleRelease
|
||||
|
||||
- name: Update ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
|
||||
- name: Update x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
|
||||
- name: Update ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
|
||||
- name: Update x86
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86-release.apk
|
||||
@@ -36,6 +36,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v0.1.14
|
||||
with:
|
||||
body: Bump Version
|
||||
discussion_category_name: Announcements
|
||||
files: |
|
||||
app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
|
||||
+1
-1
@@ -12,4 +12,4 @@
|
||||
url = https://github.com/Ehviewer-Overhauled/libarchive
|
||||
[submodule "native/src/main/jni/liblzma"]
|
||||
path = native/src/main/jni/liblzma
|
||||
url = https://git.tukaani.org/xz.git
|
||||
url = https://github.com/Ehviewer-Overhauled/xz
|
||||
@@ -4,8 +4,8 @@
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions">
|
||||
<img src="https://img.shields.io/github/workflow/status/Ehviewer-Overhauled/Ehviewer/EhViewer" alt="Github Actions">
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
|
||||
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
|
||||
</a>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
|
||||
@@ -52,6 +52,8 @@ Here is the libraries
|
||||
|
||||
- [AOSP](http://source.android.com/)
|
||||
- [android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
|
||||
- [material-components-android](https://github.com/material-components/material-components-android)
|
||||
- [material-design-icons](https://github.com/google/material-design-icons)
|
||||
- [Apache Commons Lang](https://commons.apache.org/proper/commons-lang/)
|
||||
- [apng](http://apng.sourceforge.net/)
|
||||
- [giflib](http://giflib.sourceforge.net)
|
||||
|
||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
||||
applicationId "moe.tarsin.ehviewer"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 33
|
||||
versionCode 114529
|
||||
versionName "1.7.28.2"
|
||||
versionCode 114532
|
||||
versionName "1.7.29.0"
|
||||
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,11 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
|
||||
<!-- Required to maintain app compatibility. -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
android:required="true" />
|
||||
|
||||
@@ -49,7 +49,7 @@ class EditTextDialogBuilder constructor(
|
||||
return mDialog as AlertDialog
|
||||
}
|
||||
|
||||
override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent): Boolean {
|
||||
override fun onEditorAction(v: TextView?, p1: Int, event: KeyEvent?): Boolean {
|
||||
return if (mDialog != null) {
|
||||
val button = mDialog!!.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
button?.performClick()
|
||||
|
||||
@@ -41,6 +41,7 @@ import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class Settings {
|
||||
|
||||
@@ -271,6 +272,11 @@ public class Settings {
|
||||
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
|
||||
private static final String KEY_REQUEST_NEWS = "request_news";
|
||||
private static final boolean DEFAULT_REQUEST_NEWS = true;
|
||||
|
||||
|
||||
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
|
||||
|
||||
|
||||
private static Context sContext;
|
||||
private static SharedPreferences sSettingsPre;
|
||||
private static EhConfig sEhConfig;
|
||||
@@ -384,6 +390,21 @@ public class Settings {
|
||||
sSettingsPre.edit().putString(key, value).apply();
|
||||
}
|
||||
|
||||
public static Set<String> getStringSet(String key) {
|
||||
return sSettingsPre.getStringSet(key, null);
|
||||
}
|
||||
|
||||
public static void putStringToStringSet(String key, String value) {
|
||||
Set<String> set = getStringSet(key);
|
||||
if (set == null)
|
||||
set = Set.of(value);
|
||||
else if (set.contains(value))
|
||||
return;
|
||||
else
|
||||
set.add(value);
|
||||
sSettingsPre.edit().putStringSet(key, set).apply();
|
||||
}
|
||||
|
||||
public static int getIntFromStr(String key, int defValue) {
|
||||
try {
|
||||
return NumberUtils.parseIntSafely(sSettingsPre.getString(key, Integer.toString(defValue)), defValue);
|
||||
@@ -1139,4 +1160,12 @@ public class Settings {
|
||||
public static void putClipboardTextHashCode(int value) {
|
||||
putInt(KEY_CLIPBOARD_TEXT_HASH_CODE, value);
|
||||
}
|
||||
|
||||
public static Set<String> getArchivePasswds() {
|
||||
return getStringSet(KEY_ARCHIVE_PASSWDS);
|
||||
}
|
||||
|
||||
public static void putPasswdToArchivePasswds(String value) {
|
||||
putStringToStringSet(KEY_ARCHIVE_PASSWDS, value);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Process;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -26,22 +27,26 @@ import androidx.annotation.Nullable;
|
||||
import com.hippo.UriArchiveAccessor;
|
||||
import com.hippo.ehviewer.GetText;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.glgallery.GalleryPageView;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.thread.PVLock;
|
||||
import com.hippo.yorozuya.thread.PriorityThread;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
|
||||
private static final AtomicInteger sIdGenerator = new AtomicInteger();
|
||||
public static Handler showPasswd;
|
||||
public static String passwd;
|
||||
public static PVLock pv = new PVLock(0);
|
||||
private final UriArchiveAccessor archiveAccessor;
|
||||
private final Stack<Integer> requests = new Stack<>();
|
||||
private final AtomicInteger extractingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX);
|
||||
@@ -56,7 +61,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
UriArchiveAccessor archiveAccessor1;
|
||||
try {
|
||||
archiveAccessor1 = new UriArchiveAccessor(context, uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
} catch (Exception e) {
|
||||
archiveAccessor1 = null;
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -90,6 +95,11 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
decodeThread.interrupt();
|
||||
decodeThread = null;
|
||||
}
|
||||
try {
|
||||
archiveAccessor.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -159,9 +169,27 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
}
|
||||
|
||||
private class ArchiveTask implements Runnable {
|
||||
private void waitPasswd() throws InterruptedException {
|
||||
showPasswd.sendEmptyMessage(0);
|
||||
pv.p();
|
||||
}
|
||||
|
||||
private void notifyError() {
|
||||
showPasswd.sendEmptyMessage(1);
|
||||
}
|
||||
|
||||
private void notifyDismiss() {
|
||||
showPasswd.sendEmptyMessage(2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
size = archiveAccessor.open();
|
||||
try {
|
||||
size = archiveAccessor.open();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
size = 0;
|
||||
}
|
||||
if (size <= 0) {
|
||||
size = STATE_ERROR;
|
||||
error = GetText.getString(R.string.error_reading_failed);
|
||||
@@ -171,6 +199,34 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
// Update size and notify changed
|
||||
notifyDataChanged();
|
||||
|
||||
if (archiveAccessor.needPassword()) {
|
||||
boolean need_request = true;
|
||||
Set<String> set = Settings.getArchivePasswds();
|
||||
if (set != null) {
|
||||
for (String passwd : set) {
|
||||
if (archiveAccessor.providePassword(passwd)) {
|
||||
need_request = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!Thread.currentThread().isInterrupted() && need_request) {
|
||||
try {
|
||||
waitPasswd();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
if (!archiveAccessor.providePassword(passwd))
|
||||
notifyError();
|
||||
else {
|
||||
Settings.putPasswdToArchivePasswds(passwd);
|
||||
notifyDismiss();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
int index;
|
||||
synchronized (requests) {
|
||||
@@ -194,7 +250,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
Pipe pipe = new Pipe(4096 * 16);
|
||||
Pipe pipe = new Pipe();
|
||||
|
||||
synchronized (streams) {
|
||||
if (streams.get(index) != null) {
|
||||
@@ -204,7 +260,8 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
streams.notify();
|
||||
}
|
||||
|
||||
archiveAccessor.extractTargetIndexToOutputStream(index, pipe.getOutputStream());
|
||||
archiveAccessor.extracttoOutputStream(index, pipe.getOutputFd());
|
||||
pipe.closeOutputFd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,141 +16,37 @@
|
||||
|
||||
package com.hippo.ehviewer.gallery;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
class Pipe {
|
||||
|
||||
private final int capacity;
|
||||
private final byte[] buffer;
|
||||
|
||||
private int head = 0;
|
||||
private int tail = 0;
|
||||
private boolean full = false;
|
||||
|
||||
private boolean inClosed = false;
|
||||
private boolean outClosed = false;
|
||||
|
||||
private final InputStream inputStream = new InputStream() {
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
synchronized (Pipe.this) {
|
||||
byte[] bytes = new byte[1];
|
||||
if (read(bytes, 0, 1) != -1) {
|
||||
return bytes[0];
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
private ParcelFileDescriptor[] fd;
|
||||
{
|
||||
try {
|
||||
fd = ParcelFileDescriptor.createPipe();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] b, int off, int len) throws IOException {
|
||||
synchronized (Pipe.this) {
|
||||
for (; ; ) {
|
||||
if (inClosed) {
|
||||
throw new IOException("The InputStream is closed");
|
||||
}
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (head == tail && !full) {
|
||||
if (outClosed) {
|
||||
// No bytes available and the OutputStream is closed. So it's the end.
|
||||
return -1;
|
||||
} else {
|
||||
// Wait for OutputStream write bytes
|
||||
try {
|
||||
Pipe.this.wait();
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException("The thread interrupted", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int read = Math.min(len, (head < tail ? tail : capacity) - head);
|
||||
System.arraycopy(buffer, head, b, off, read);
|
||||
head += read;
|
||||
if (head == capacity) {
|
||||
head = 0;
|
||||
}
|
||||
full = false;
|
||||
Pipe.this.notifyAll();
|
||||
return read;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
synchronized (Pipe.this) {
|
||||
inClosed = true;
|
||||
Pipe.this.notifyAll();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final OutputStream outputStream = new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
synchronized (Pipe.this) {
|
||||
byte[] bytes = new byte[]{(byte) b};
|
||||
write(bytes, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(@NonNull byte[] b, int off, int len) throws IOException {
|
||||
synchronized (Pipe.this) {
|
||||
while (len != 0) {
|
||||
if (head == tail && full) {
|
||||
// The buffer is full, wait for InputStream read bytes
|
||||
try {
|
||||
Pipe.this.wait();
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException("The thread interrupted", e);
|
||||
}
|
||||
} else {
|
||||
int write = Math.min(len, (head <= tail ? capacity : head) - tail);
|
||||
System.arraycopy(b, off, buffer, tail, write);
|
||||
off += write;
|
||||
len -= write;
|
||||
tail += write;
|
||||
if (tail == capacity) {
|
||||
tail = 0;
|
||||
}
|
||||
if (head == tail) {
|
||||
full = true;
|
||||
}
|
||||
Pipe.this.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
synchronized (Pipe.this) {
|
||||
outClosed = true;
|
||||
Pipe.this.notifyAll();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Pipe(int capacity) {
|
||||
this.capacity = capacity;
|
||||
this.buffer = new byte[capacity];
|
||||
}
|
||||
|
||||
private final InputStream inputStream = new FileInputStream(fd[0].getFileDescriptor());
|
||||
|
||||
InputStream getInputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
OutputStream getOutputStream() {
|
||||
return outputStream;
|
||||
int getOutputFd() {
|
||||
return fd[1].getFd();
|
||||
}
|
||||
|
||||
void closeOutputFd() {
|
||||
try {
|
||||
fd[1].close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,8 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import android.view.KeyEvent;
|
||||
@@ -67,6 +69,7 @@ import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.materialswitch.MaterialSwitch;
|
||||
import com.hippo.app.EditTextDialogBuilder;
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.BuildConfig;
|
||||
import com.hippo.ehviewer.R;
|
||||
@@ -103,6 +106,7 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import rikka.core.res.ConfigurationKt;
|
||||
import rikka.core.res.ResourcesKt;
|
||||
@@ -314,10 +318,19 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
onRestore(savedInstanceState);
|
||||
}
|
||||
|
||||
builder = new EditTextDialogBuilder(this, null, getString(R.string.archive_passwd));
|
||||
builder.setTitle(getString(R.string.archive_need_passwd));
|
||||
builder.setPositiveButton(getString(android.R.string.ok), null);
|
||||
dialog = builder.create();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
|
||||
if (mGalleryProvider == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
ArchiveGalleryProvider.showPasswd = new ShowPasswdDialogHandler(this);
|
||||
|
||||
mGalleryProvider.start();
|
||||
|
||||
// Get start page
|
||||
@@ -1293,7 +1306,69 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
}
|
||||
}
|
||||
|
||||
private EditTextDialogBuilder builder;
|
||||
|
||||
private boolean dialogShown = false;
|
||||
|
||||
private AlertDialog dialog;
|
||||
|
||||
private void showPasswdDialog() {
|
||||
if (!dialogShown) {
|
||||
dialogShown = true;
|
||||
dialog.show();
|
||||
if (dialog.getButton(AlertDialog.BUTTON_POSITIVE) != null) {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
|
||||
GalleryActivity.this.onProvidePasswd();
|
||||
});
|
||||
}
|
||||
dialog.setOnCancelListener(v -> finish());
|
||||
}
|
||||
}
|
||||
|
||||
private void onProvidePasswd() {
|
||||
String passwd = builder.getText();
|
||||
if (passwd.isEmpty())
|
||||
builder.setError(getString(R.string.passwd_cannot_be_empty));
|
||||
else {
|
||||
ArchiveGalleryProvider.passwd = passwd;
|
||||
ArchiveGalleryProvider.pv.v();
|
||||
}
|
||||
}
|
||||
|
||||
private void onPasswdWrong() {
|
||||
builder.setError(getString(R.string.passwd_wrong));
|
||||
}
|
||||
|
||||
private void onPasswdCorrect() {
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
private static class ShowPasswdDialogHandler extends Handler {
|
||||
|
||||
//弱引用持有HandlerActivity , GC 回收时会被回收掉
|
||||
private final WeakReference<GalleryActivity> weakReference;
|
||||
|
||||
public ShowPasswdDialogHandler(GalleryActivity activity) {
|
||||
this.weakReference = new WeakReference(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
GalleryActivity activity = weakReference.get();
|
||||
super.handleMessage(msg);
|
||||
if (null != activity) {
|
||||
switch (msg.what) {
|
||||
case 0:
|
||||
activity.showPasswdDialog();
|
||||
break;
|
||||
case 1:
|
||||
activity.onPasswdWrong();
|
||||
break;
|
||||
case 2:
|
||||
activity.onPasswdCorrect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.slider.Slider;
|
||||
import com.hippo.easyrecyclerview.EasyRecyclerView;
|
||||
import com.hippo.easyrecyclerview.MarginItemDecoration;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
@@ -53,7 +54,6 @@ import com.hippo.ehviewer.ui.MainActivity;
|
||||
import com.hippo.scene.SceneFragment;
|
||||
import com.hippo.widget.ContentLayout;
|
||||
import com.hippo.widget.LoadImageView;
|
||||
import com.hippo.widget.Slider;
|
||||
import com.hippo.widget.recyclerview.AutoGridLayoutManager;
|
||||
import com.hippo.yorozuya.AssertUtils;
|
||||
import com.hippo.yorozuya.LayoutUtils;
|
||||
@@ -209,7 +209,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
|
||||
return true;
|
||||
}
|
||||
int pages = mHelper.getPages();
|
||||
if (pages > 0 && mHelper.canGoTo()) {
|
||||
if (pages > 1 && mHelper.canGoTo()) {
|
||||
GoToDialogHelper helper = new GoToDialogHelper(pages, mHelper.getPageForTop());
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context).setTitle(R.string.go_to)
|
||||
.setView(R.layout.dialog_go_to)
|
||||
@@ -418,8 +418,8 @@ public class GalleryPreviewsScene extends ToolbarScene {
|
||||
((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.setRange(1, mPages);
|
||||
mSlider.setProgress(mCurrentPage + 1);
|
||||
mSlider.setValueTo(mPages);
|
||||
mSlider.setValue(mCurrentPage + 1);
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this);
|
||||
dialog.setOnDismissListener(this);
|
||||
@@ -431,7 +431,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
|
||||
return;
|
||||
}
|
||||
|
||||
int page = mSlider.getProgress() - 1;
|
||||
int page = (int) (mSlider.getValue() - 1);
|
||||
if (page >= 0 && page < mPages && mHelper != null) {
|
||||
mHelper.goTo(page);
|
||||
if (mDialog != null) {
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
public class AdvanceSearchTable extends LinearLayout {
|
||||
|
||||
public static final int SNAME = 0x1;
|
||||
public static final int STAGS = 0x2;
|
||||
public static final int SDESC = 0x4;
|
||||
public static final int STORR = 0x8;
|
||||
public static final int STO = 0x10;
|
||||
public static final int SDT1 = 0x20;
|
||||
public static final int SDT2 = 0x40;
|
||||
public static final int SH = 0x80;
|
||||
public static final int SFL = 0x100;
|
||||
public static final int SFU = 0x200;
|
||||
public static final int SFT = 0x400;
|
||||
private static final String STATE_KEY_SUPER = "super";
|
||||
private static final String STATE_KEY_ADVANCE_SEARCH = "advance_search";
|
||||
private static final String STATE_KEY_MIN_RATING = "min_rating";
|
||||
private static final String STATE_KEY_PAGE_FROM = "page_from";
|
||||
private static final String STATE_KEY_PAGE_TO = "page_to";
|
||||
private CheckBox mSname;
|
||||
private CheckBox mStags;
|
||||
private CheckBox mSdesc;
|
||||
private CheckBox mStorr;
|
||||
private CheckBox mSto;
|
||||
private CheckBox mSdt1;
|
||||
private CheckBox mSdt2;
|
||||
private CheckBox mSh;
|
||||
private CheckBox mSr;
|
||||
private Spinner mMinRating;
|
||||
private CheckBox mSp;
|
||||
private EditText mSpf;
|
||||
private EditText mSpt;
|
||||
private CheckBox mSfl;
|
||||
private CheckBox mSfu;
|
||||
private CheckBox mSft;
|
||||
|
||||
public AdvanceSearchTable(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public AdvanceSearchTable(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public void init(Context context) {
|
||||
setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
inflater.inflate(R.layout.widget_advance_search_table, this);
|
||||
|
||||
ViewGroup row0 = (ViewGroup) getChildAt(0);
|
||||
mSname = (CheckBox) row0.getChildAt(0);
|
||||
mStags = (CheckBox) row0.getChildAt(1);
|
||||
|
||||
ViewGroup row1 = (ViewGroup) getChildAt(1);
|
||||
mSdesc = (CheckBox) row1.getChildAt(0);
|
||||
mStorr = (CheckBox) row1.getChildAt(1);
|
||||
|
||||
ViewGroup row2 = (ViewGroup) getChildAt(2);
|
||||
mSto = (CheckBox) row2.getChildAt(0);
|
||||
mSdt1 = (CheckBox) row2.getChildAt(1);
|
||||
|
||||
ViewGroup row3 = (ViewGroup) getChildAt(3);
|
||||
mSdt2 = (CheckBox) row3.getChildAt(0);
|
||||
mSh = (CheckBox) row3.getChildAt(1);
|
||||
|
||||
ViewGroup row4 = (ViewGroup) getChildAt(4);
|
||||
mSr = (CheckBox) row4.getChildAt(0);
|
||||
mMinRating = (Spinner) row4.getChildAt(1);
|
||||
|
||||
ViewGroup row5 = (ViewGroup) getChildAt(5);
|
||||
mSp = (CheckBox) row5.getChildAt(0);
|
||||
mSpf = (EditText) row5.getChildAt(1);
|
||||
mSpt = (EditText) row5.getChildAt(3);
|
||||
|
||||
ViewGroup row7 = (ViewGroup) getChildAt(7);
|
||||
mSfl = (CheckBox) row7.getChildAt(0);
|
||||
mSfu = (CheckBox) row7.getChildAt(1);
|
||||
mSft = (CheckBox) row7.getChildAt(2);
|
||||
|
||||
// Avoid java.lang.IllegalStateException: focus search returned a view that wasn't able to take focus!
|
||||
mSpt.setOnEditorActionListener((v, actionId, event) -> {
|
||||
View nextView = v.focusSearch(View.FOCUS_DOWN);
|
||||
if (nextView != null) {
|
||||
nextView.requestFocus(View.FOCUS_DOWN);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public int getAdvanceSearch() {
|
||||
int advanceSearch = 0;
|
||||
if (mSname.isChecked()) advanceSearch |= SNAME;
|
||||
if (mStags.isChecked()) advanceSearch |= STAGS;
|
||||
if (mSdesc.isChecked()) advanceSearch |= SDESC;
|
||||
if (mStorr.isChecked()) advanceSearch |= STORR;
|
||||
if (mSto.isChecked()) advanceSearch |= STO;
|
||||
if (mSdt1.isChecked()) advanceSearch |= SDT1;
|
||||
if (mSdt2.isChecked()) advanceSearch |= SDT2;
|
||||
if (mSh.isChecked()) advanceSearch |= SH;
|
||||
if (mSfl.isChecked()) advanceSearch |= SFL;
|
||||
if (mSfu.isChecked()) advanceSearch |= SFU;
|
||||
if (mSft.isChecked()) advanceSearch |= SFT;
|
||||
return advanceSearch;
|
||||
}
|
||||
|
||||
public void setAdvanceSearch(int advanceSearch) {
|
||||
mSname.setChecked(NumberUtils.int2boolean(advanceSearch & SNAME));
|
||||
mStags.setChecked(NumberUtils.int2boolean(advanceSearch & STAGS));
|
||||
mSdesc.setChecked(NumberUtils.int2boolean(advanceSearch & SDESC));
|
||||
mStorr.setChecked(NumberUtils.int2boolean(advanceSearch & STORR));
|
||||
mSto.setChecked(NumberUtils.int2boolean(advanceSearch & STO));
|
||||
mSdt1.setChecked(NumberUtils.int2boolean(advanceSearch & SDT1));
|
||||
mSdt2.setChecked(NumberUtils.int2boolean(advanceSearch & SDT2));
|
||||
mSh.setChecked(NumberUtils.int2boolean(advanceSearch & SH));
|
||||
mSfl.setChecked(NumberUtils.int2boolean(advanceSearch & SFL));
|
||||
mSfu.setChecked(NumberUtils.int2boolean(advanceSearch & SFU));
|
||||
mSft.setChecked(NumberUtils.int2boolean(advanceSearch & SFT));
|
||||
}
|
||||
|
||||
public int getMinRating() {
|
||||
int position = mMinRating.getSelectedItemPosition();
|
||||
if (mSr.isChecked() && position >= 0) {
|
||||
return position + 2;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public void setMinRating(int minRating) {
|
||||
if (minRating >= 2 && minRating <= 5) {
|
||||
mSr.setChecked(true);
|
||||
mMinRating.setSelection(minRating - 2);
|
||||
} else {
|
||||
mSr.setChecked(false);
|
||||
}
|
||||
}
|
||||
|
||||
public int getPageFrom() {
|
||||
if (mSp.isChecked()) {
|
||||
return NumberUtils.parseIntSafely(mSpf.getText().toString(), -1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void setPageFrom(int pageFrom) {
|
||||
if (pageFrom > 0) {
|
||||
mSpf.setText(Integer.toString(pageFrom));
|
||||
mSp.setChecked(true);
|
||||
} else {
|
||||
mSp.setChecked(false);
|
||||
mSpf.setText(null);
|
||||
}
|
||||
}
|
||||
|
||||
public int getPageTo() {
|
||||
if (mSp.isChecked()) {
|
||||
return NumberUtils.parseIntSafely(mSpt.getText().toString(), -1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void setPageTo(int pageTo) {
|
||||
if (pageTo > 0) {
|
||||
mSpt.setText(Integer.toString(pageTo));
|
||||
mSp.setChecked(true);
|
||||
} else {
|
||||
mSp.setChecked(false);
|
||||
mSpt.setText(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final Bundle state = new Bundle();
|
||||
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState());
|
||||
state.putInt(STATE_KEY_ADVANCE_SEARCH, getAdvanceSearch());
|
||||
state.putInt(STATE_KEY_MIN_RATING, getMinRating());
|
||||
state.putInt(STATE_KEY_PAGE_FROM, getPageFrom());
|
||||
state.putInt(STATE_KEY_PAGE_TO, getPageTo());
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state) {
|
||||
if (state instanceof Bundle) {
|
||||
final Bundle savedState = (Bundle) state;
|
||||
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
|
||||
setAdvanceSearch(savedState.getInt(STATE_KEY_ADVANCE_SEARCH));
|
||||
setMinRating(savedState.getInt(STATE_KEY_MIN_RATING));
|
||||
setPageFrom(savedState.getInt(STATE_KEY_PAGE_FROM));
|
||||
setPageTo(savedState.getInt(STATE_KEY_PAGE_TO));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.yorozuya.NumberUtils
|
||||
|
||||
class AdvanceSearchTable @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs) {
|
||||
private var mSname: CheckBox
|
||||
private var mStags: CheckBox
|
||||
private var mSdesc: CheckBox
|
||||
private var mStorr: CheckBox
|
||||
private var mSto: CheckBox
|
||||
private var mSdt1: CheckBox
|
||||
private var mSdt2: CheckBox
|
||||
private var mSh: CheckBox
|
||||
private var mSr: CheckBox
|
||||
private var mMinRating: TextInputLayout
|
||||
private var mSp: CheckBox
|
||||
private var mSpf: EditText
|
||||
private var mSpt: EditText
|
||||
private var mSfl: CheckBox
|
||||
private var mSfu: CheckBox
|
||||
private var mSft: CheckBox
|
||||
private val mArray: Array<String> = context.resources.getStringArray(R.array.search_min_rating)
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
val inflater = LayoutInflater.from(context)
|
||||
inflater.inflate(R.layout.widget_advance_search_table, this)
|
||||
val row0 = getChildAt(0) as ViewGroup
|
||||
mSname = row0.getChildAt(0) as CheckBox
|
||||
mStags = row0.getChildAt(1) as CheckBox
|
||||
val row1 = getChildAt(1) as ViewGroup
|
||||
mSdesc = row1.getChildAt(0) as CheckBox
|
||||
mStorr = row1.getChildAt(1) as CheckBox
|
||||
val row2 = getChildAt(2) as ViewGroup
|
||||
mSto = row2.getChildAt(0) as CheckBox
|
||||
mSdt1 = row2.getChildAt(1) as CheckBox
|
||||
val row3 = getChildAt(3) as ViewGroup
|
||||
mSdt2 = row3.getChildAt(0) as CheckBox
|
||||
mSh = row3.getChildAt(1) as CheckBox
|
||||
val row4 = getChildAt(4) as ViewGroup
|
||||
mSr = row4.getChildAt(0) as CheckBox
|
||||
mMinRating = row4.getChildAt(1) as TextInputLayout
|
||||
val row5 = getChildAt(5) as ViewGroup
|
||||
mSp = row5.getChildAt(0) as CheckBox
|
||||
mSpf = row5.getChildAt(1) as EditText
|
||||
mSpt = row5.getChildAt(3) as EditText
|
||||
val row7 = getChildAt(7) as ViewGroup
|
||||
mSfl = row7.getChildAt(0) as CheckBox
|
||||
mSfu = row7.getChildAt(1) as CheckBox
|
||||
mSft = row7.getChildAt(2) as CheckBox
|
||||
|
||||
mSpt.setOnEditorActionListener { v: TextView, _: Int, _: KeyEvent? ->
|
||||
val nextView = v.focusSearch(
|
||||
FOCUS_DOWN
|
||||
)
|
||||
nextView?.requestFocus(FOCUS_DOWN)
|
||||
true
|
||||
}
|
||||
|
||||
mMinRating.isEnabled = mSr.isChecked
|
||||
mSr.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean ->
|
||||
mMinRating.isEnabled = isChecked
|
||||
}
|
||||
|
||||
mSpt.isEnabled = mSp.isChecked
|
||||
mSpf.isEnabled = mSp.isChecked
|
||||
mSp.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean ->
|
||||
mSpt.isEnabled = isChecked
|
||||
mSpf.isEnabled = isChecked
|
||||
}
|
||||
}
|
||||
|
||||
var advanceSearch: Int
|
||||
get() {
|
||||
var advanceSearch = 0
|
||||
if (mSname.isChecked) advanceSearch = advanceSearch or SNAME
|
||||
if (mStags.isChecked) advanceSearch = advanceSearch or STAGS
|
||||
if (mSdesc.isChecked) advanceSearch = advanceSearch or SDESC
|
||||
if (mStorr.isChecked) advanceSearch = advanceSearch or STORR
|
||||
if (mSto.isChecked) advanceSearch = advanceSearch or STO
|
||||
if (mSdt1.isChecked) advanceSearch = advanceSearch or SDT1
|
||||
if (mSdt2.isChecked) advanceSearch = advanceSearch or SDT2
|
||||
if (mSh.isChecked) advanceSearch = advanceSearch or SH
|
||||
if (mSfl.isChecked) advanceSearch = advanceSearch or SFL
|
||||
if (mSfu.isChecked) advanceSearch = advanceSearch or SFU
|
||||
if (mSft.isChecked) advanceSearch = advanceSearch or SFT
|
||||
return advanceSearch
|
||||
}
|
||||
set(advanceSearch) {
|
||||
mSname.isChecked =
|
||||
NumberUtils.int2boolean(advanceSearch and SNAME)
|
||||
mStags.isChecked =
|
||||
NumberUtils.int2boolean(advanceSearch and STAGS)
|
||||
mSdesc.isChecked =
|
||||
NumberUtils.int2boolean(advanceSearch and SDESC)
|
||||
mStorr.isChecked = NumberUtils.int2boolean(advanceSearch and STORR)
|
||||
mSto.isChecked = NumberUtils.int2boolean(advanceSearch and STO)
|
||||
mSdt1.isChecked = NumberUtils.int2boolean(advanceSearch and SDT1)
|
||||
mSdt2.isChecked = NumberUtils.int2boolean(advanceSearch and SDT2)
|
||||
mSh.isChecked = NumberUtils.int2boolean(advanceSearch and SH)
|
||||
mSfl.isChecked = NumberUtils.int2boolean(advanceSearch and SFL)
|
||||
mSfu.isChecked = NumberUtils.int2boolean(advanceSearch and SFU)
|
||||
mSft.isChecked = NumberUtils.int2boolean(advanceSearch and SFT)
|
||||
}
|
||||
var minRating: Int
|
||||
get() {
|
||||
val position = mArray.indexOf(mMinRating.editText!!.text.toString())
|
||||
return if (mSr.isChecked && position >= 0) {
|
||||
position + 2
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
set(minRating) {
|
||||
if (minRating in 2..5) {
|
||||
mSr.isChecked = true
|
||||
(mMinRating.editText!! as AutoCompleteTextView).setText(
|
||||
mArray[minRating - 2],
|
||||
false
|
||||
)
|
||||
} else {
|
||||
mSr.isChecked = false
|
||||
}
|
||||
}
|
||||
var pageFrom: Int
|
||||
get() = if (mSp.isChecked) {
|
||||
NumberUtils.parseIntSafely(mSpf.text.toString(), -1)
|
||||
} else -1
|
||||
set(pageFrom) {
|
||||
if (pageFrom > 0) {
|
||||
mSpf.setText(pageFrom.toString())
|
||||
mSp.isChecked = true
|
||||
} else {
|
||||
mSp.isChecked = false
|
||||
mSpf.text = null
|
||||
}
|
||||
}
|
||||
var pageTo: Int
|
||||
get() = if (mSp.isChecked) {
|
||||
NumberUtils.parseIntSafely(mSpt.text.toString(), -1)
|
||||
} else -1
|
||||
set(pageTo) {
|
||||
if (pageTo > 0) {
|
||||
mSpt.setText(pageTo.toString())
|
||||
mSp.isChecked = true
|
||||
} else {
|
||||
mSp.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SNAME = 0x1
|
||||
const val STAGS = 0x2
|
||||
const val SDESC = 0x4
|
||||
const val STORR = 0x8
|
||||
const val STO = 0x10
|
||||
const val SDT1 = 0x20
|
||||
const val SDT2 = 0x40
|
||||
const val SH = 0x80
|
||||
const val SFL = 0x100
|
||||
const val SFU = 0x200
|
||||
const val SFT = 0x400
|
||||
}
|
||||
}
|
||||
@@ -1,515 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 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.widget;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.yorozuya.AnimationUtils;
|
||||
import com.hippo.yorozuya.LayoutUtils;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
import com.hippo.yorozuya.SimpleHandler;
|
||||
|
||||
public class Slider extends View {
|
||||
|
||||
private static final char[] CHARACTERS = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
|
||||
};
|
||||
|
||||
private static final int BUBBLE_WIDTH = 26;
|
||||
private static final int BUBBLE_HEIGHT = 32;
|
||||
private final RectF mLeftRectF = new RectF();
|
||||
private final RectF mRightRectF = new RectF();
|
||||
private final int[] mTemp = new int[2];
|
||||
private Context mContext;
|
||||
private Paint mPaint;
|
||||
private Paint mBgPaint;
|
||||
private PopupWindow mPopup;
|
||||
private BubbleView mBubble;
|
||||
private int mStart;
|
||||
private int mEnd;
|
||||
private int mProgress;
|
||||
private float mPercent;
|
||||
private int mDrawProgress;
|
||||
private float mDrawPercent;
|
||||
private int mTargetProgress;
|
||||
private float mThickness;
|
||||
private float mRadius;
|
||||
private float mCharWidth;
|
||||
private float mCharHeight;
|
||||
private int mBubbleWidth;
|
||||
private int mBubbleHeight;
|
||||
private int mBubbleMinWidth;
|
||||
private int mBubbleMinHeight;
|
||||
private int mPopupX;
|
||||
private int mPopupY;
|
||||
private int mPopupWidth;
|
||||
private boolean mReverse = false;
|
||||
|
||||
private boolean mShowBubble;
|
||||
|
||||
private float mDrawBubbleScale = 0f;
|
||||
|
||||
private ValueAnimator mProgressAnimation;
|
||||
private ValueAnimator mBubbleScaleAnimation;
|
||||
|
||||
private OnSetProgressListener mListener;
|
||||
|
||||
private CheckForShowBubble mCheckForShowBubble;
|
||||
|
||||
public Slider(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public Slider(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void init(Context context, AttributeSet attrs) {
|
||||
mContext = context;
|
||||
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||
|
||||
Resources resources = context.getResources();
|
||||
mBubbleMinWidth = resources.getDimensionPixelOffset(R.dimen.slider_bubble_width);
|
||||
mBubbleMinHeight = resources.getDimensionPixelOffset(R.dimen.slider_bubble_height);
|
||||
|
||||
mBubble = new BubbleView(context, textPaint);
|
||||
mBubble.setScaleX(0.0f);
|
||||
mBubble.setScaleY(0.0f);
|
||||
RelativeLayout relativeLayout = new RelativeLayout(context);
|
||||
relativeLayout.addView(mBubble);
|
||||
relativeLayout.setBackgroundDrawable(null);
|
||||
mPopup = new PopupWindow(relativeLayout);
|
||||
mPopup.setOutsideTouchable(false);
|
||||
mPopup.setTouchable(false);
|
||||
mPopup.setFocusable(false);
|
||||
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slider);
|
||||
textPaint.setColor(a.getColor(R.styleable.Slider_textColor, Color.WHITE));
|
||||
textPaint.setTextSize(a.getDimensionPixelSize(R.styleable.Slider_textSize, 12));
|
||||
|
||||
updateTextSize();
|
||||
|
||||
setRange(a.getInteger(R.styleable.Slider_start, 0), a.getInteger(R.styleable.Slider_end, 0));
|
||||
setProgress(a.getInteger(R.styleable.Slider_slider_progress, 0));
|
||||
mThickness = a.getDimension(R.styleable.Slider_thickness, 2);
|
||||
mRadius = a.getDimension(R.styleable.Slider_radius, 6);
|
||||
|
||||
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
a.recycle();
|
||||
|
||||
mProgressAnimation = new ValueAnimator();
|
||||
mProgressAnimation.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);
|
||||
mProgressAnimation.addUpdateListener(animation -> {
|
||||
float value = (Float) animation.getAnimatedValue();
|
||||
mDrawPercent = value;
|
||||
mDrawProgress = Math.round(MathUtils.lerp((float) mStart, mEnd, value));
|
||||
updateBubblePosition();
|
||||
mBubble.setProgress(mDrawProgress);
|
||||
invalidate();
|
||||
});
|
||||
|
||||
mBubbleScaleAnimation = new ValueAnimator();
|
||||
mBubbleScaleAnimation.addUpdateListener(animation -> {
|
||||
float value = (Float) animation.getAnimatedValue();
|
||||
mDrawBubbleScale = value;
|
||||
mBubble.setScaleX(value);
|
||||
mBubble.setScaleY(value);
|
||||
invalidate();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateTextSize() {
|
||||
int length = CHARACTERS.length;
|
||||
float[] widths = new float[length];
|
||||
mPaint.getTextWidths(CHARACTERS, 0, length, widths);
|
||||
mCharWidth = 0.0f;
|
||||
for (float f : widths) {
|
||||
mCharWidth = Math.max(mCharWidth, f);
|
||||
}
|
||||
|
||||
Paint.FontMetrics fm = mPaint.getFontMetrics();
|
||||
mCharHeight = fm.bottom - fm.top;
|
||||
}
|
||||
|
||||
private void updateBubbleSize() {
|
||||
int oldWidth = mBubbleWidth;
|
||||
int oldHeight = mBubbleHeight;
|
||||
mBubbleWidth = (int) Math.max(mBubbleMinWidth,
|
||||
Integer.toString(mEnd).length() * mCharWidth + LayoutUtils.dp2pix(mContext, 8));
|
||||
mBubbleHeight = (int) Math.max(mBubbleMinHeight,
|
||||
mCharHeight + LayoutUtils.dp2pix(mContext, 8));
|
||||
|
||||
if (oldWidth != mBubbleWidth && oldHeight != mBubbleHeight) {
|
||||
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mBubble.getLayoutParams();
|
||||
lp.width = mBubbleWidth;
|
||||
lp.height = mBubbleHeight;
|
||||
mBubble.setLayoutParams(lp);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePopup() {
|
||||
int width = getWidth();
|
||||
int paddingTop = getPaddingTop();
|
||||
int paddingBottom = getPaddingBottom();
|
||||
|
||||
getLocationInWindow(mTemp);
|
||||
|
||||
mPopupWidth = (int) (width - mRadius - mRadius + mBubbleWidth);
|
||||
int popupHeight = mBubbleHeight;
|
||||
mPopupX = (int) (mTemp[0] + mRadius - (mBubbleWidth / 2));
|
||||
mPopupY = (int) (mTemp[1] - popupHeight + paddingTop +
|
||||
((getHeight() - paddingTop - paddingBottom) / 2) -
|
||||
mRadius - LayoutUtils.dp2pix(mContext, 2));
|
||||
|
||||
mPopup.update(mPopupX, mPopupY, mPopupWidth, popupHeight, false);
|
||||
}
|
||||
|
||||
private void updateBubblePosition() {
|
||||
float x = ((mPopupWidth - mBubbleWidth) * (mReverse ? (1.0f - mDrawPercent) : mDrawPercent));
|
||||
mBubble.setX(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
updatePopup();
|
||||
updateBubblePosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
mPopup.showAtLocation(this, Gravity.TOP | Gravity.LEFT, mPopupX, mPopupY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
mPopup.dismiss();
|
||||
}
|
||||
|
||||
private void startProgressAnimation(float percent) {
|
||||
float currentValue;
|
||||
if (mProgressAnimation.isRunning()) {
|
||||
mProgressAnimation.setCurrentPlayTime(mProgressAnimation.getCurrentPlayTime());
|
||||
Object value = mProgressAnimation.getAnimatedValue();
|
||||
if (value instanceof Float) {
|
||||
currentValue = (float) value;
|
||||
} else {
|
||||
currentValue = mDrawPercent;
|
||||
}
|
||||
} else {
|
||||
currentValue = mDrawPercent;
|
||||
}
|
||||
mProgressAnimation.cancel();
|
||||
mProgressAnimation.setFloatValues(currentValue, percent);
|
||||
mProgressAnimation.setDuration(Math.min(500, (long) (50 * getWidth() * Math.abs(currentValue - percent))));
|
||||
mProgressAnimation.start();
|
||||
}
|
||||
|
||||
private void startShowBubbleAnimation() {
|
||||
mBubbleScaleAnimation.cancel();
|
||||
mBubbleScaleAnimation.setFloatValues(mDrawBubbleScale, 1.0f);
|
||||
mBubbleScaleAnimation.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);
|
||||
mBubbleScaleAnimation.setDuration((long) (300 * Math.abs(mDrawBubbleScale - 1.0f)));
|
||||
mBubbleScaleAnimation.start();
|
||||
}
|
||||
|
||||
private void startHideBubbleAnimation() {
|
||||
mBubbleScaleAnimation.cancel();
|
||||
mBubbleScaleAnimation.setFloatValues(mDrawBubbleScale, 0.0f);
|
||||
mBubbleScaleAnimation.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR);
|
||||
mBubbleScaleAnimation.setDuration((long) (300 * Math.abs(mDrawBubbleScale - 0.0f)));
|
||||
mBubbleScaleAnimation.start();
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
mPaint.setColor(color);
|
||||
mBubble.setColor(color);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setRange(int start, int end) {
|
||||
mStart = start;
|
||||
mEnd = end;
|
||||
setProgress(mProgress);
|
||||
|
||||
updateBubbleSize();
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return mProgress;
|
||||
}
|
||||
|
||||
public void setProgress(int progress) {
|
||||
progress = MathUtils.clamp(progress, mStart, mEnd);
|
||||
int oldProgress = mProgress;
|
||||
if (mProgress != progress) {
|
||||
mProgress = progress;
|
||||
mPercent = MathUtils.delerp(mStart, mEnd, mProgress);
|
||||
mTargetProgress = progress;
|
||||
|
||||
if (mProgressAnimation == null) {
|
||||
// For init
|
||||
mDrawPercent = mPercent;
|
||||
mDrawProgress = mProgress;
|
||||
updateBubblePosition();
|
||||
mBubble.setProgress(mDrawProgress);
|
||||
} else {
|
||||
startProgressAnimation(mPercent);
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
if (mListener != null) {
|
||||
mListener.onSetProgress(this, progress, oldProgress, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void setReverse(boolean reverse) {
|
||||
if (mReverse != reverse) {
|
||||
mReverse = reverse;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnSetProgressListener(OnSetProgressListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull Canvas canvas) {
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
if (width < LayoutUtils.dp2pix(mContext, 24)) {
|
||||
canvas.drawRect(0, 0, width, getHeight(), mPaint);
|
||||
} else {
|
||||
int paddingLeft = getPaddingLeft();
|
||||
int paddingTop = getPaddingTop();
|
||||
int paddingRight = getPaddingRight();
|
||||
int paddingBottom = getPaddingBottom();
|
||||
float thickness = mThickness;
|
||||
float radius = mRadius;
|
||||
float halfThickness = thickness / 2;
|
||||
|
||||
int saved = canvas.save();
|
||||
|
||||
canvas.translate(0, paddingTop + ((height - paddingTop - paddingBottom) / 2));
|
||||
|
||||
float currentX = paddingLeft + radius + (width - radius - radius - paddingLeft - paddingRight) *
|
||||
(mReverse ? (1.0f - mDrawPercent) : mDrawPercent);
|
||||
|
||||
mLeftRectF.set(paddingLeft + radius, -halfThickness, currentX, halfThickness);
|
||||
mRightRectF.set(currentX, -halfThickness, width - paddingRight - radius, halfThickness);
|
||||
|
||||
// Draw bar
|
||||
if (mReverse) {
|
||||
canvas.drawRect(mRightRectF, mPaint);
|
||||
canvas.drawRect(mLeftRectF, mBgPaint);
|
||||
} else {
|
||||
canvas.drawRect(mLeftRectF, mPaint);
|
||||
canvas.drawRect(mRightRectF, mBgPaint);
|
||||
}
|
||||
|
||||
// Draw controller
|
||||
float scale = 1.0f - mDrawBubbleScale;
|
||||
if (scale != 0.0f) {
|
||||
canvas.scale(scale, scale, currentX, 0);
|
||||
canvas.drawCircle(currentX, 0, radius, mPaint);
|
||||
}
|
||||
|
||||
canvas.restoreToCount(saved);
|
||||
}
|
||||
}
|
||||
|
||||
private void setShowBubble(boolean showBubble) {
|
||||
if (mShowBubble != showBubble) {
|
||||
mShowBubble = showBubble;
|
||||
if (showBubble) {
|
||||
startShowBubbleAnimation();
|
||||
} else {
|
||||
startHideBubbleAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
||||
int action = event.getAction();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
|
||||
if (mListener != null) {
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
mListener.onFingerDown();
|
||||
} else if (action == MotionEvent.ACTION_UP) {
|
||||
mListener.onFingerUp();
|
||||
}
|
||||
}
|
||||
|
||||
int paddingLeft = getPaddingLeft();
|
||||
int paddingRight = getPaddingRight();
|
||||
float radius = mRadius;
|
||||
float x = event.getX();
|
||||
int progress = Math.round(MathUtils.lerp((float) mStart, (float) mEnd,
|
||||
MathUtils.clamp((mReverse ? (getWidth() - paddingLeft - radius - x) : (x - radius - paddingLeft)) /
|
||||
(getWidth() - radius - radius - paddingLeft - paddingRight), 0.0f, 1.0f)));
|
||||
float percent = MathUtils.delerp(mStart, mEnd, progress);
|
||||
|
||||
// ACTION_CANCEL not changed
|
||||
if (action == MotionEvent.ACTION_CANCEL) {
|
||||
progress = mProgress;
|
||||
percent = mPercent;
|
||||
}
|
||||
|
||||
if (mTargetProgress != progress) {
|
||||
mTargetProgress = progress;
|
||||
startProgressAnimation(percent);
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onSetProgress(this, mProgress, progress, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||
SimpleHandler.getInstance().removeCallbacks(mCheckForShowBubble);
|
||||
setShowBubble(false);
|
||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
||||
if (mCheckForShowBubble == null) {
|
||||
mCheckForShowBubble = new CheckForShowBubble();
|
||||
}
|
||||
SimpleHandler.getInstance().postDelayed(mCheckForShowBubble, ViewConfiguration.getTapTimeout());
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
int oldProgress = mProgress;
|
||||
if (mProgress != progress) {
|
||||
mProgress = progress;
|
||||
mPercent = mDrawPercent;
|
||||
}
|
||||
if (mListener != null) {
|
||||
mListener.onSetProgress(this, progress, oldProgress, true, true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public interface OnSetProgressListener {
|
||||
|
||||
void onSetProgress(Slider slider, int newProgress, int oldProgress, boolean byUser, boolean confirm);
|
||||
|
||||
void onFingerDown();
|
||||
|
||||
void onFingerUp();
|
||||
}
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
private static class BubbleView extends AppCompatImageView {
|
||||
|
||||
private static final float TEXT_CENTER = (float) BUBBLE_WIDTH / 2.0f / BUBBLE_HEIGHT;
|
||||
|
||||
private final Drawable mDrawable;
|
||||
|
||||
private final Paint mTextPaint;
|
||||
private final Rect mRect = new Rect();
|
||||
private String mProgressStr = "";
|
||||
|
||||
public BubbleView(Context context, Paint paint) {
|
||||
super(context);
|
||||
setImageResource(R.drawable.v_slider_bubble);
|
||||
mDrawable = DrawableCompat.wrap(getDrawable());
|
||||
setImageDrawable(mDrawable);
|
||||
mTextPaint = paint;
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
DrawableCompat.setTint(mDrawable, color);
|
||||
}
|
||||
|
||||
public void setProgress(int progress) {
|
||||
String str = Integer.toString(progress);
|
||||
if (!str.equals(mProgressStr)) {
|
||||
mProgressStr = str;
|
||||
mTextPaint.getTextBounds(str, 0, str.length(), mRect);
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
setPivotX(w / 2);
|
||||
setPivotY(h);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
int x = width / 2;
|
||||
int y = (int) ((height * TEXT_CENTER) + (mRect.height() / 2));
|
||||
canvas.drawText(mProgressStr, x, y, mTextPaint);
|
||||
}
|
||||
}
|
||||
|
||||
private final class CheckForShowBubble implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
setShowBubble(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,13 +28,14 @@
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_horizontal" />
|
||||
|
||||
<com.hippo.widget.Slider
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider"
|
||||
style="@style/Slider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_weight="1" />
|
||||
android:layout_weight="1"
|
||||
android:stepSize="1"
|
||||
android:valueFrom="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/end"
|
||||
|
||||
@@ -94,62 +94,69 @@
|
||||
android:focusable="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TableLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<TableRow>
|
||||
<TextView
|
||||
android:id="@+id/language"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/language"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:singleLine="true" />
|
||||
<TextView
|
||||
android:id="@+id/size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:gravity="end"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/language"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toEndOf="@id/language" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:singleLine="true" />
|
||||
|
||||
</TableRow>
|
||||
<TextView
|
||||
android:id="@+id/favoredTimes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="start"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/language" />
|
||||
|
||||
<TableRow android:paddingTop="8dp">
|
||||
<TextView
|
||||
android:id="@+id/pages"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:gravity="end"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/favoredTimes"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toEndOf="@id/favoredTimes" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/favoredTimes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:singleLine="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pages"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:singleLine="true" />
|
||||
<TextView
|
||||
android:id="@+id/posted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/favoredTimes" />
|
||||
|
||||
</TableRow>
|
||||
|
||||
<TableRow android:paddingTop="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/posted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:singleLine="true" />
|
||||
|
||||
</TableRow>
|
||||
</TableLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -139,13 +140,22 @@
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/search_sr" />
|
||||
|
||||
<Spinner
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="@dimen/advance_search_table_item_margin"
|
||||
android:layout_marginStart="@dimen/advance_search_table_item_margin"
|
||||
android:layout_weight="1"
|
||||
android:entries="@array/search_min_rating" />
|
||||
android:hint="@string/search_sr">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none"
|
||||
app:simpleItems="@array/search_min_rating" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -764,4 +764,9 @@
|
||||
<string name="settings_privacy_require_unlock">需要解锁</string>
|
||||
<string name="proxy_type_name">代理类型</string>
|
||||
<string name="filter_label">屏蔽项类型</string>
|
||||
|
||||
<string name="archive_passwd">归档文件密码</string>
|
||||
<string name="archive_need_passwd">归档文件需要密码</string>
|
||||
<string name="passwd_wrong">密码错误</string>
|
||||
<string name="passwd_cannot_be_empty">密码不能为空</string>
|
||||
</resources>
|
||||
@@ -828,4 +828,9 @@
|
||||
<string name="settings_privacy_require_unlock">Require Unlock</string>
|
||||
<string name="proxy_type_name">Proxy Type</string>
|
||||
<string name="filter_label">Blocker Type</string>
|
||||
|
||||
<string name="archive_passwd">password</string>
|
||||
<string name="archive_need_passwd">Archive need password</string>
|
||||
<string name="passwd_wrong">Password Wrong</string>
|
||||
<string name="passwd_cannot_be_empty">Password can\'t be empty</string>
|
||||
</resources>
|
||||
@@ -51,12 +51,6 @@
|
||||
<item name="android:gravity">center_vertical</item>
|
||||
</style>
|
||||
|
||||
<style name="Slider">
|
||||
<item name="thickness">2dp</item>
|
||||
<item name="radius">6dp</item>
|
||||
<item name="textSize">12sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Indicating">
|
||||
<item name="indicatorHeight">1dp</item>
|
||||
<item name="indicatorColor">?attr/dividerColor</item>
|
||||
|
||||
@@ -28,8 +28,6 @@ public class Native {
|
||||
|
||||
public static native String getliblzmaVersion();
|
||||
|
||||
public static native String getlibzstdVersion();
|
||||
|
||||
public static native String getlibjpeg_turboVersion();
|
||||
|
||||
public static native String getzlibVersion();
|
||||
|
||||
@@ -21,103 +21,44 @@ package com.hippo;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class UriArchiveAccessor implements Closeable {
|
||||
public static int OPEN_OK = 1;
|
||||
public static int OPEN_ERR = 2;
|
||||
private final OsReadableFile osf;
|
||||
public class UriArchiveAccessor {
|
||||
ParcelFileDescriptor pfd;
|
||||
FileDescriptor fd;
|
||||
long archiveAddr;
|
||||
Context ctx;
|
||||
Uri uri;
|
||||
|
||||
public UriArchiveAccessor(Context ctx, Uri uri) throws FileNotFoundException {
|
||||
osf = new OsReadableFile(ctx, uri);
|
||||
public UriArchiveAccessor(Context ctx, Uri uri) {
|
||||
this.ctx = ctx;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public int open() {
|
||||
return openArchive(osf);
|
||||
public int open() throws Exception {
|
||||
pfd = ctx.getContentResolver().openFileDescriptor(uri, "r");
|
||||
fd = pfd.getFileDescriptor();
|
||||
archiveAddr = Os.mmap(0, pfd.getStatSize(), OsConstants.PROT_READ, OsConstants.MAP_PRIVATE, fd, 0);
|
||||
return openArchive(archiveAddr, pfd.getStatSize());
|
||||
}
|
||||
|
||||
public void extractTargetIndexToOutputStream(int index, OutputStream os) {
|
||||
extracttoOutputStream(osf, index, os);
|
||||
}
|
||||
private native int openArchive(long addr, long size);
|
||||
|
||||
private native int openArchive(OsReadableFile osf);
|
||||
public native void extracttoOutputStream(int index, int fd);
|
||||
|
||||
private native int extracttoOutputStream(OsReadableFile osf, int index, OutputStream os);
|
||||
public native boolean needPassword();
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
osf.close();
|
||||
}
|
||||
public native boolean providePassword(String str);
|
||||
|
||||
static class OsReadableFile {
|
||||
ParcelFileDescriptor pfd;
|
||||
FileDescriptor fd;
|
||||
private native void closeArchive();
|
||||
|
||||
public OsReadableFile(Context ctx, Uri uri) throws FileNotFoundException {
|
||||
pfd = ctx.getContentResolver().openFileDescriptor(uri, "r");
|
||||
fd = pfd.getFileDescriptor();
|
||||
}
|
||||
|
||||
public int read(byte[] b, int off, int len) throws IOException, ErrnoException {
|
||||
return Os.read(fd, b, off, len);
|
||||
}
|
||||
|
||||
public int read(byte[] b) throws IOException, ErrnoException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
public long length() throws IOException, ErrnoException {
|
||||
return Os.fstat(fd).st_size;
|
||||
}
|
||||
|
||||
public long seek(long pos, int whence) throws IOException, ErrnoException {
|
||||
try {
|
||||
return Os.lseek(fd, pos, whence);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return -30;
|
||||
}
|
||||
}
|
||||
|
||||
public void rewind() throws IOException, ErrnoException {
|
||||
Os.lseek(fd, 0, OsConstants.SEEK_SET);
|
||||
}
|
||||
|
||||
public long skip(long n) throws IOException, ErrnoException {
|
||||
long pos;
|
||||
long len;
|
||||
long newpos;
|
||||
|
||||
if (n <= 0) {
|
||||
return 0;
|
||||
}
|
||||
pos = Os.lseek(fd, 0L, OsConstants.SEEK_CUR);
|
||||
len = length();
|
||||
newpos = pos + n;
|
||||
if (newpos > len) {
|
||||
newpos = len;
|
||||
}
|
||||
seek(newpos, OsConstants.SEEK_SET);
|
||||
|
||||
/* return the actual number of bytes skipped */
|
||||
return newpos - pos;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
pfd.close();
|
||||
try {
|
||||
Os.close(fd);
|
||||
} catch (ErrnoException e) {
|
||||
throw new IOException("An ErrnoException occurs", e);
|
||||
}
|
||||
}
|
||||
public void close() throws Exception {
|
||||
closeArchive();
|
||||
Os.munmap(archiveAddr, pfd.getStatSize());
|
||||
Os.close(fd);
|
||||
pfd.close();
|
||||
}
|
||||
}
|
||||
+109
-103
@@ -23,6 +23,9 @@
|
||||
#include <android/log.h>
|
||||
#include <archive.h>
|
||||
#include <archive_entry.h>
|
||||
#include <sys/mman.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define LOG_TAG "libarchive_wrapper"
|
||||
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ,__VA_ARGS__)
|
||||
@@ -32,19 +35,13 @@
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG ,__VA_ARGS__)
|
||||
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG ,__VA_ARGS__)
|
||||
|
||||
#define BLOCK_SIZE 4096 * 16
|
||||
|
||||
typedef struct archive_inst_sc {
|
||||
struct archive *archive;
|
||||
JNIEnv *env;
|
||||
jobject file;
|
||||
jmethodID readID;
|
||||
jmethodID seekID;
|
||||
jmethodID skipID;
|
||||
void *read_buffer;
|
||||
void *output_buffer;
|
||||
jbyteArray jbr;
|
||||
} archive_inst;
|
||||
static JNIEnv *env;
|
||||
static int cur_index = 0; // Current entry we haven't read any data from it yet
|
||||
static struct archive *arc = NULL;
|
||||
static bool need_encrypt = false;
|
||||
static char *passwd = NULL;
|
||||
static void *archiveAddr = NULL;
|
||||
static jlong archiveSize = 0;
|
||||
|
||||
static int filename_is_playable_file(const char *name) {
|
||||
const char *dotptr = strrchr(name, '.');
|
||||
@@ -63,124 +60,133 @@ static int filename_is_playable_file(const char *name) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static long archive_list_all_entries(struct archive_inst_sc *inst) {
|
||||
struct archive *a = inst->archive;
|
||||
static long archive_list_all_entries() {
|
||||
struct archive_entry *entry;
|
||||
long count = 0;
|
||||
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
|
||||
while (archive_read_next_header(arc, &entry) == ARCHIVE_OK) {
|
||||
if (filename_is_playable_file(archive_entry_pathname(entry)))
|
||||
count++;
|
||||
}
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(inst->archive));
|
||||
LOGE("%s", archive_error_string(arc));
|
||||
return count;
|
||||
}
|
||||
|
||||
ssize_t ins_read(struct archive *a, void *client_data_ptr, const void **buff) {
|
||||
archive_inst *arc = client_data_ptr;
|
||||
int32_t r = (*arc->env)->CallIntMethod(arc->env, arc->file, arc->readID, arc->jbr);
|
||||
(*arc->env)->GetByteArrayRegion(arc->env, arc->jbr, 0, r, arc->read_buffer);
|
||||
*buff = arc->read_buffer;
|
||||
return r;
|
||||
static void archive_release() {
|
||||
if (arc) {
|
||||
archive_read_close(arc);
|
||||
archive_read_free(arc);
|
||||
arc = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int ins_close(struct archive *a, void *client_data) {
|
||||
archive_inst *arc = client_data;
|
||||
JNIEnv *env = arc->env;
|
||||
jmethodID rewind = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, arc->file), "rewind",
|
||||
"()V");
|
||||
(*arc->env)->CallVoidMethod(arc->env, arc->file, rewind);
|
||||
return ARCHIVE_OK;
|
||||
}
|
||||
|
||||
la_int64_t ins_seek(struct archive *a, void *client_data, la_int64_t offset, int whence) {
|
||||
archive_inst *arc = client_data;
|
||||
jlong ret = (*arc->env)->CallLongMethod(arc->env, arc->file, arc->seekID, offset, whence);
|
||||
return ret;
|
||||
}
|
||||
|
||||
la_int64_t ins_skip(struct archive *a, void *client_data, la_int64_t request) {
|
||||
archive_inst *arc = client_data;
|
||||
jlong ret = (*arc->env)->CallLongMethod(arc->env, arc->file, arc->skipID, request);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static archive_inst *archive_create_inst(JNIEnv *env, jobject file) {
|
||||
archive_inst *inst = malloc(sizeof(archive_inst));
|
||||
inst->archive = archive_read_new();
|
||||
archive_read_set_seek_callback(inst->archive, ins_seek);
|
||||
archive_read_set_skip_callback(inst->archive, ins_skip);
|
||||
archive_read_support_format_all(inst->archive);
|
||||
archive_read_support_filter_all(inst->archive);
|
||||
inst->env = env;
|
||||
inst->file = file;
|
||||
inst->readID = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, file), "read", "([B)I");
|
||||
inst->seekID = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, file), "seek", "(JI)J");
|
||||
inst->skipID = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, file), "skip", "(J)J");
|
||||
inst->jbr = (*env)->NewByteArray(env, BLOCK_SIZE);
|
||||
inst->read_buffer = malloc(BLOCK_SIZE);
|
||||
inst->output_buffer = malloc(BLOCK_SIZE);
|
||||
return inst;
|
||||
}
|
||||
|
||||
static void archive_destroy_inst(archive_inst *inst) {
|
||||
JNIEnv *env = inst->env;
|
||||
archive_read_free(inst->archive);
|
||||
(*env)->DeleteLocalRef(env, inst->jbr);
|
||||
free(inst->read_buffer);
|
||||
free(inst->output_buffer);
|
||||
free(inst);
|
||||
static int archive_alloc() {
|
||||
archive_release();
|
||||
arc = archive_read_new();
|
||||
archive_read_support_format_all(arc);
|
||||
archive_read_support_filter_all(arc);
|
||||
if (passwd)
|
||||
archive_read_add_passphrase(arc, passwd);
|
||||
return archive_read_open_memory(arc, archiveAddr, archiveSize);
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jobject osf) {
|
||||
archive_inst *arc = archive_create_inst(env, osf);
|
||||
long r = archive_read_open(arc->archive, arc, NULL, ins_read, ins_close);
|
||||
Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *_, jobject thiz, jlong addr, jlong size) {
|
||||
archiveAddr = (void *) addr;
|
||||
archiveSize = size;
|
||||
env = _;
|
||||
madvise(archiveAddr, archiveSize, MADV_SEQUENTIAL | MADV_WILLNEED);
|
||||
long r = archive_alloc();
|
||||
if (r) {
|
||||
r = 0;
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(arc->archive));
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(arc));
|
||||
} else {
|
||||
r = archive_list_all_entries(arc);
|
||||
r = archive_list_all_entries();
|
||||
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
|
||||
|
||||
// We must read through the file|vm then we can know whether it is encrypted
|
||||
int encryptRet = archive_read_has_encrypted_entries(arc);
|
||||
switch (encryptRet) {
|
||||
case 1: // At lease 1 encrypted entry
|
||||
need_encrypt = true;
|
||||
break;
|
||||
case 0: // format supports but no encrypted entry found
|
||||
case ARCHIVE_READ_FORMAT_ENCRYPTION_DONT_KNOW: // Sometimes we might not read through whole file|vm, but it means so far we haven't meet any encrypted entry yet
|
||||
default:
|
||||
need_encrypt = false;
|
||||
}
|
||||
LOGI("%s%d", "archive_read_has_encrypted_entries returns ", encryptRet);
|
||||
}
|
||||
archive_read_close(arc->archive);
|
||||
archive_destroy_inst(arc);
|
||||
archive_release();
|
||||
return r;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_extracttoOutputStream(JNIEnv *env, jobject thiz, jobject osf,
|
||||
jint index, jobject os) {
|
||||
int count = 0;
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_extracttoOutputStream(JNIEnv *_, jobject thiz, jint index,
|
||||
jint fd) {
|
||||
struct archive_entry *entry;
|
||||
archive_inst *arc = archive_create_inst(env, osf);
|
||||
la_ssize_t size;
|
||||
jmethodID writeID = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, os), "write", "([B)V");
|
||||
jmethodID flushID = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, os), "flush", "()V");
|
||||
jmethodID closeID = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, os), "close", "()V");
|
||||
archive_read_open(arc->archive, arc, NULL, ins_read, ins_close);
|
||||
while (archive_read_next_header(arc->archive, &entry) == ARCHIVE_OK) {
|
||||
size_t size;
|
||||
int ret;
|
||||
la_int64_t offset;
|
||||
const void *buff;
|
||||
if (!arc || index < cur_index) {
|
||||
archive_alloc();
|
||||
cur_index = 0;
|
||||
}
|
||||
while (archive_read_next_header(arc, &entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(entry)))
|
||||
continue;
|
||||
if (count++ == index) {
|
||||
if (cur_index++ == index) {
|
||||
for (;;) {
|
||||
size = archive_read_data(arc->archive, arc->output_buffer, BLOCK_SIZE);
|
||||
if (size < 0) {
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(arc->archive));
|
||||
goto free;
|
||||
}
|
||||
(*arc->env)->SetByteArrayRegion(env, arc->jbr, 0, size, arc->output_buffer);
|
||||
(*arc->env)->CallVoidMethod(env, os, writeID, arc->jbr);
|
||||
(*arc->env)->CallVoidMethod(env, os, flushID);
|
||||
if (size < BLOCK_SIZE)
|
||||
ret = archive_read_data_block(arc, &buff, &size, &offset);
|
||||
if (ret == ARCHIVE_EOF)
|
||||
break;
|
||||
if (ret != ARCHIVE_OK) {
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(arc));
|
||||
break;
|
||||
}
|
||||
write(fd, buff, size);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
free:
|
||||
(*arc->env)->CallVoidMethod(env, os, closeID);
|
||||
archive_read_close(arc->archive);
|
||||
archive_destroy_inst(arc);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *jniEnv, jobject thiz) {
|
||||
archive_release();
|
||||
free(passwd);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_needPassword(JNIEnv *_, jobject thiz) {
|
||||
return need_encrypt;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *_, jobject thiz, jstring str) {
|
||||
struct archive_entry *entry;
|
||||
jboolean ret = true;
|
||||
int len = (*env)->GetStringUTFLength(env, str);
|
||||
if (passwd)
|
||||
free(passwd);
|
||||
passwd = calloc(len, sizeof(char));
|
||||
strcpy(passwd, (*env)->GetStringUTFChars(env, str, NULL));
|
||||
LOGI("%s%s", "Passwd is ", passwd);
|
||||
archive_alloc();
|
||||
while (archive_read_next_header(arc, &entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(entry)))
|
||||
continue;
|
||||
if (!archive_entry_is_encrypted(entry))
|
||||
continue;
|
||||
void *tmpBuf = malloc(4096);
|
||||
if (archive_read_data(arc, tmpBuf, 4096) < ARCHIVE_OK) {
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(arc));
|
||||
ret = false;
|
||||
}
|
||||
free(tmpBuf);
|
||||
break;
|
||||
}
|
||||
archive_release();
|
||||
return ret;
|
||||
}
|
||||
Reference in new issue
Block a user