Support tag translations
This commit is contained in:
1 parent
3904406d74
commit
c580938806
15 files changed
+491
-2
No files matched your search
@@ -156,4 +156,19 @@ public class AppConfig {
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File getFilesDir(String name) {
|
||||
File dir = sContext.getFilesDir();
|
||||
if (dir == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
dir = new File(dir, name);
|
||||
if (dir.isDirectory() || dir.mkdirs()) {
|
||||
return dir;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,8 @@ public class EhApplication extends RecordingApplication {
|
||||
private static final boolean DEBUG_PRINT_IMAGE_COUNT = false;
|
||||
private static final long DEBUG_PRINT_INTERVAL = 3000L;
|
||||
|
||||
private static EhApplication instance;
|
||||
|
||||
private final IntIdGenerator mIdGenerator = new IntIdGenerator();
|
||||
private final HashMap<Integer, Object> mGlobalStuffMap = new HashMap<>();
|
||||
private EhCookieStore mEhCookieStore;
|
||||
@@ -92,9 +94,15 @@ public class EhApplication extends RecordingApplication {
|
||||
|
||||
private final List<Activity> mActivityList = new ArrayList<>();
|
||||
|
||||
public static EhApplication getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public void onCreate() {
|
||||
instance = this;
|
||||
|
||||
super.onCreate();
|
||||
|
||||
GetText.initialize(this);
|
||||
|
||||
@@ -340,6 +340,13 @@ public class Settings {
|
||||
return getBoolean(KEY_SHOW_GALLERY_PAGES, DEFAULT_SHOW_GALLERY_PAGES);
|
||||
}
|
||||
|
||||
public static final String KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations";
|
||||
private static final boolean DEFAULT_SHOW_TAG_TRANSLATIONS = false;
|
||||
|
||||
public static boolean getShowTagTranslations() {
|
||||
return getBoolean(KEY_SHOW_TAG_TRANSLATIONS, DEFAULT_SHOW_TAG_TRANSLATIONS);
|
||||
}
|
||||
|
||||
public static final String KEY_DEFAULT_CATEGORIES = "default_categories";
|
||||
public static final int DEFAULT_DEFAULT_CATEGORIES = EhUtils.ALL_CATEGORY;
|
||||
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* Copyright 2019 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.support.annotation.Nullable;
|
||||
import android.util.Base64;
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.util.IoThreadPoolExecutor;
|
||||
import com.hippo.util.TextUrl;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.BufferedSource;
|
||||
import okio.Okio;
|
||||
|
||||
public class EhTagDatabase {
|
||||
|
||||
private final String name;
|
||||
private final byte[] tags;
|
||||
|
||||
public EhTagDatabase(String name, BufferedSource source) throws IOException {
|
||||
this.name = name;
|
||||
int totalBytes = source.readInt();
|
||||
tags = new byte[totalBytes];
|
||||
source.readFully(tags);
|
||||
}
|
||||
|
||||
public String getTranslation(String tag) {
|
||||
return search(tags, tag.getBytes(TextUrl.UTF_8));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String search(byte[] tags, byte[] tag) {
|
||||
int low = 0;
|
||||
int high = tags.length;
|
||||
while (low < high) {
|
||||
int start = (low + high) / 2;
|
||||
// Look for the starting '\n'
|
||||
while (start > -1 && tags[start] != '\n') {
|
||||
start--;
|
||||
}
|
||||
start++;
|
||||
|
||||
// Look for the middle '\r'.
|
||||
int middle = 1;
|
||||
while (tags[start + middle] != '\r') {
|
||||
middle++;
|
||||
}
|
||||
|
||||
// Look for the ending '\n'
|
||||
int end = middle + 1;
|
||||
while (tags[start + end] != '\n') {
|
||||
end++;
|
||||
}
|
||||
|
||||
int compare;
|
||||
int tagIndex = 0;
|
||||
int curIndex = start;
|
||||
|
||||
for (;;) {
|
||||
int tagByte = tag[tagIndex] & 0xff;
|
||||
int curByte = tags[curIndex] & 0xff;
|
||||
compare = tagByte - curByte;
|
||||
if (compare != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
tagIndex++;
|
||||
curIndex++;
|
||||
if (tagIndex == tag.length && curIndex == start + middle) {
|
||||
break;
|
||||
}
|
||||
if (tagIndex == tag.length) {
|
||||
compare = -1;
|
||||
break;
|
||||
}
|
||||
if (curIndex == start + middle) {
|
||||
compare = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (compare < 0) {
|
||||
high = start - 1;
|
||||
} else if (compare > 0) {
|
||||
low = start + end + 1;
|
||||
} else {
|
||||
byte[] bytes = Base64.decode(tags, start + middle + 1, end - middle - 1, Base64.DEFAULT);
|
||||
return new String(bytes, TextUrl.UTF_8);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static final String[] METADATA_ZH_CN = new String[] {
|
||||
"tag-translations-zh-rCN.sha1",
|
||||
"https://github.com/seven332/EhViewer/raw/tag-translations/tag-translations/tag-translations-zh-rCN.sha1",
|
||||
"tag-translations-zh-rCN",
|
||||
"https://github.com/seven332/EhViewer/raw/tag-translations/tag-translations/tag-translations-zh-rCN",
|
||||
};
|
||||
|
||||
private static final Map<String, String> NAMESPACE_TO_PREFIX = new HashMap<>();
|
||||
|
||||
static {
|
||||
NAMESPACE_TO_PREFIX.put("artist", "a:");
|
||||
NAMESPACE_TO_PREFIX.put("character", "c:");
|
||||
NAMESPACE_TO_PREFIX.put("female", "f:");
|
||||
NAMESPACE_TO_PREFIX.put("group", "g:");
|
||||
NAMESPACE_TO_PREFIX.put("language", "l:");
|
||||
NAMESPACE_TO_PREFIX.put("male", "m:");
|
||||
NAMESPACE_TO_PREFIX.put("misc", "");
|
||||
NAMESPACE_TO_PREFIX.put("parody", "p:");
|
||||
NAMESPACE_TO_PREFIX.put("reclass", "r:");
|
||||
}
|
||||
|
||||
private static volatile EhTagDatabase instance;
|
||||
// TODO more lock for different language
|
||||
private static Lock lock = new ReentrantLock();
|
||||
|
||||
@Nullable
|
||||
public static EhTagDatabase getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String namespaceToPrefix(String namespace) {
|
||||
return NAMESPACE_TO_PREFIX.get(namespace);
|
||||
}
|
||||
|
||||
private static String[] getMetadata() {
|
||||
Locale locale = Locale.getDefault();
|
||||
if ("zh".equals(locale.getLanguage()) && "CN".equals(locale.getCountry())) {
|
||||
return METADATA_ZH_CN;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isPossible() {
|
||||
return getMetadata() != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static byte[] getFileContent(File file, int length) {
|
||||
try (BufferedSource source = Okio.buffer(Okio.source(file))) {
|
||||
byte[] content = new byte[length];
|
||||
source.readFully(content);
|
||||
return content;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static byte[] getFileSha1(File file) {
|
||||
try (InputStream is = new FileInputStream(file)) {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
int n;
|
||||
byte[] buffer = new byte[4 * 1024];
|
||||
while ((n = is.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, n);
|
||||
}
|
||||
return digest.digest();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean equals(byte[] b1, byte[] b2) {
|
||||
if (b1 == null && b2 == null) {
|
||||
return true;
|
||||
}
|
||||
if (b1 == null || b2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (b1.length != b2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < b1.length; i++) {
|
||||
if (b1[i] != b2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean checkData(File sha1File, File dataFile) {
|
||||
byte[] s1 = getFileContent(sha1File, 20);
|
||||
if (s1 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] s2 = getFileSha1(dataFile);
|
||||
if (s2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return equals(s1, s2);
|
||||
}
|
||||
|
||||
private static boolean save(OkHttpClient client, String url, File file) {
|
||||
Request request = new Request.Builder().url(url).build();
|
||||
Call call = client.newCall(request);
|
||||
try (Response response = call.execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
return false;
|
||||
}
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (InputStream is = body.byteStream(); OutputStream os = new FileOutputStream(file)) {
|
||||
IOUtils.copy(is, os);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void update() {
|
||||
String[] urls = getMetadata();
|
||||
if (urls == null || urls.length != 4) {
|
||||
// Clear tags if it's not possible
|
||||
instance = null;
|
||||
return;
|
||||
}
|
||||
|
||||
String sha1Name = urls[0];
|
||||
String sha1Url = urls[1];
|
||||
String dataName = urls[2];
|
||||
String dataUrl = urls[3];
|
||||
|
||||
// Clear tags if name if different
|
||||
EhTagDatabase tmp = instance;
|
||||
if (tmp != null && !tmp.name.equals(dataName)) {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
IoThreadPoolExecutor.getInstance().execute(() -> {
|
||||
if (!lock.tryLock()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
File dir = AppConfig.getFilesDir("tag-translations");
|
||||
if (dir == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check current sha1 and current data
|
||||
File sha1File = new File(dir, sha1Name);
|
||||
File dataFile = new File(dir, dataName);
|
||||
if (!checkData(sha1File, dataFile)) {
|
||||
FileUtils.delete(sha1File);
|
||||
FileUtils.delete(dataFile);
|
||||
}
|
||||
|
||||
// Read current EhTagDatabase
|
||||
if (instance == null && dataFile.exists()) {
|
||||
try (BufferedSource source = Okio.buffer(Okio.source(dataFile))) {
|
||||
instance = new EhTagDatabase(dataName, source);
|
||||
} catch (IOException e) {
|
||||
FileUtils.delete(sha1File);
|
||||
FileUtils.delete(dataFile);
|
||||
}
|
||||
}
|
||||
|
||||
OkHttpClient client = EhApplication.getOkHttpClient(EhApplication.getInstance());
|
||||
|
||||
// Save new sha1
|
||||
File tempSha1File = new File(dir, sha1Name + ".tmp");
|
||||
if (!save(client, sha1Url, tempSha1File)) {
|
||||
FileUtils.delete(tempSha1File);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check new sha1 and current data
|
||||
if (checkData(tempSha1File, dataFile)) {
|
||||
// The data is the same
|
||||
FileUtils.delete(tempSha1File);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save new data
|
||||
File tempDataFile = new File(dir, dataName + ".tmp");
|
||||
if (!save(client, dataUrl, tempDataFile)) {
|
||||
FileUtils.delete(tempDataFile);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check new sha1 and new data
|
||||
if (!checkData(tempSha1File, tempDataFile)) {
|
||||
FileUtils.delete(tempSha1File);
|
||||
FileUtils.delete(tempDataFile);
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace current sha1 and current data with new sha1 and new data
|
||||
FileUtils.delete(sha1File);
|
||||
FileUtils.delete(dataFile);
|
||||
tempSha1File.renameTo(sha1File);
|
||||
tempDataFile.renameTo(dataFile);
|
||||
|
||||
// Read new EhTagDatabase
|
||||
try (BufferedSource source = Okio.buffer(Okio.source(dataFile))) {
|
||||
instance = new EhTagDatabase(dataName, source);
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhTagDatabase;
|
||||
import com.hippo.ehviewer.client.EhUrlOpener;
|
||||
import com.hippo.ehviewer.client.EhUtils;
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder;
|
||||
@@ -362,6 +363,8 @@ public final class MainActivity extends StageActivity
|
||||
} else {
|
||||
onRestore(savedInstanceState);
|
||||
}
|
||||
|
||||
EhTagDatabase.update();
|
||||
}
|
||||
|
||||
private String getThemeText() {
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.preference.PreferenceFragment;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhTagDatabase;
|
||||
|
||||
public class EhFragment extends PreferenceFragment
|
||||
implements Preference.OnPreferenceChangeListener {
|
||||
@@ -39,6 +40,7 @@ public class EhFragment extends PreferenceFragment
|
||||
Preference listMode = findPreference(Settings.KEY_LIST_MODE);
|
||||
Preference detailSize = findPreference(Settings.KEY_DETAIL_SIZE);
|
||||
Preference thumbSize = findPreference(Settings.KEY_THUMB_SIZE);
|
||||
Preference showTagTranslations = findPreference(Settings.KEY_SHOW_TAG_TRANSLATIONS);
|
||||
|
||||
theme.setOnPreferenceChangeListener(this);
|
||||
applyNavBarThemeColor.setOnPreferenceChangeListener(this);
|
||||
@@ -46,10 +48,15 @@ public class EhFragment extends PreferenceFragment
|
||||
listMode.setOnPreferenceChangeListener(this);
|
||||
detailSize.setOnPreferenceChangeListener(this);
|
||||
thumbSize.setOnPreferenceChangeListener(this);
|
||||
showTagTranslations.setOnPreferenceChangeListener(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
getPreferenceScreen().removePreference(applyNavBarThemeColor);
|
||||
}
|
||||
|
||||
if (!EhTagDatabase.isPossible()) {
|
||||
getPreferenceScreen().removePreference(showTagTranslations);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,6 +78,10 @@ public class EhFragment extends PreferenceFragment
|
||||
getActivity().setResult(Activity.RESULT_OK);
|
||||
} else if (Settings.KEY_THUMB_SIZE.equals(key)) {
|
||||
getActivity().setResult(Activity.RESULT_OK);
|
||||
} else if (Settings.KEY_SHOW_TAG_TRANSLATIONS.equals(key)) {
|
||||
if (Boolean.TRUE.equals(newValue)) {
|
||||
EhTagDatabase.update();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ import com.hippo.ehviewer.client.EhCacheKeyFactory;
|
||||
import com.hippo.ehviewer.client.EhClient;
|
||||
import com.hippo.ehviewer.client.EhFilter;
|
||||
import com.hippo.ehviewer.client.EhRequest;
|
||||
import com.hippo.ehviewer.client.EhTagDatabase;
|
||||
import com.hippo.ehviewer.client.EhUrl;
|
||||
import com.hippo.ehviewer.client.EhUtils;
|
||||
import com.hippo.ehviewer.client.data.GalleryComment;
|
||||
@@ -868,6 +869,7 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
mNoTags.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
EhTagDatabase ehTags = Settings.getShowTagTranslations() ? EhTagDatabase.getInstance() : null;
|
||||
int colorTag = AttrResources.getAttrColor(context, R.attr.tagBackgroundColor);
|
||||
int colorName = AttrResources.getAttrColor(context, R.attr.tagGroupBackgroundColor);
|
||||
for (GalleryTagGroup tg : tagGroups) {
|
||||
@@ -875,18 +877,34 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
ll.setOrientation(LinearLayout.HORIZONTAL);
|
||||
mTags.addView(ll);
|
||||
|
||||
String readableTagName = null;
|
||||
if (ehTags != null) {
|
||||
readableTagName = ehTags.getTranslation("n:" + tg.groupName);
|
||||
}
|
||||
|
||||
TextView tgName = (TextView) inflater.inflate(R.layout.item_gallery_tag, ll, false);
|
||||
ll.addView(tgName);
|
||||
tgName.setText(tg.groupName);
|
||||
tgName.setText(readableTagName != null ? readableTagName : tg.groupName);
|
||||
tgName.setBackgroundDrawable(new RoundSideRectDrawable(colorName));
|
||||
|
||||
String prefix = EhTagDatabase.namespaceToPrefix(tg.groupName);
|
||||
if (prefix == null) {
|
||||
prefix = "";
|
||||
}
|
||||
|
||||
AutoWrapLayout awl = new AutoWrapLayout(context);
|
||||
ll.addView(awl, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
for (int j = 0, z = tg.size(); j < z; j++) {
|
||||
TextView tag = (TextView) inflater.inflate(R.layout.item_gallery_tag, awl, false);
|
||||
awl.addView(tag);
|
||||
String tagStr = tg.getTagAt(j);
|
||||
tag.setText(tagStr);
|
||||
|
||||
String readableTag = null;
|
||||
if (ehTags != null) {
|
||||
readableTag = ehTags.getTranslation(prefix + tagStr);
|
||||
}
|
||||
|
||||
tag.setText(readableTag != null ? readableTag : tagStr);
|
||||
tag.setBackgroundDrawable(new RoundSideRectDrawable(colorTag));
|
||||
tag.setTag(R.id.tag, tg.groupName + ":" + tagStr);
|
||||
tag.setOnClickListener(this);
|
||||
|
||||
@@ -20,11 +20,14 @@ import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.URLSpan;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class TextUrl {
|
||||
|
||||
public static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
private static final Pattern URL_PATTERN = Pattern.compile("(http|https)://[a-z0-9A-Z%-]+(\\.[a-z0-9A-Z%-]+)+(:\\d{1,5})?(/[a-zA-Z0-9-_~:#@!&',;=%/\\*\\.\\?\\+\\$\\[\\]\\(\\)]+)?/?");
|
||||
|
||||
public static CharSequence handleTextUrl(CharSequence content) {
|
||||
|
||||
Reference in new issue
Block a user