Browse Source

Initial library

master
Benjamin Marlé 7 months ago
commit
fb30ecf408
85 changed files with 7079 additions and 0 deletions
  1. +6
    -0
      .classpath
  2. +16
    -0
      .gitignore
  3. +23
    -0
      .project
  4. +62
    -0
      build.gradle
  5. +0
    -0
      consumer-rules.pro
  6. +19
    -0
      gradle.properties
  7. BIN
      gradle/wrapper/gradle-wrapper.jar
  8. +6
    -0
      gradle/wrapper/gradle-wrapper.properties
  9. +172
    -0
      gradlew
  10. +84
    -0
      gradlew.bat
  11. BIN
      libs/JRikai-1.0.jar
  12. BIN
      libs/eb4j-core-1.0.5.jar
  13. +21
    -0
      proguard-rules.pro
  14. +1
    -0
      settings.gradle
  15. +15
    -0
      src/main/AndroidManifest.xml
  16. +41
    -0
      src/main/java/org/zorgblub/anki/AnkiDroidConfig.java
  17. +205
    -0
      src/main/java/org/zorgblub/anki/AnkiDroidHelper.java
  18. +49
    -0
      src/main/java/org/zorgblub/rikai/DictionaryService.java
  19. +573
    -0
      src/main/java/org/zorgblub/rikai/DictionaryServiceImpl.java
  20. +96
    -0
      src/main/java/org/zorgblub/rikai/DroidEdictEntry.java
  21. +14
    -0
      src/main/java/org/zorgblub/rikai/DroidEntry.java
  22. +49
    -0
      src/main/java/org/zorgblub/rikai/DroidEpwingDictionary.java
  23. +36
    -0
      src/main/java/org/zorgblub/rikai/DroidEpwingEntry.java
  24. +62
    -0
      src/main/java/org/zorgblub/rikai/DroidKanjiDictionary.java
  25. +100
    -0
      src/main/java/org/zorgblub/rikai/DroidKanjiEntry.java
  26. +58
    -0
      src/main/java/org/zorgblub/rikai/DroidNamesDictionary.java
  27. +94
    -0
      src/main/java/org/zorgblub/rikai/DroidSqliteDatabase.java
  28. +60
    -0
      src/main/java/org/zorgblub/rikai/DroidWordEdictDictionary.java
  29. +67
    -0
      src/main/java/org/zorgblub/rikai/DroidWordnetDictionary.java
  30. +122
    -0
      src/main/java/org/zorgblub/rikai/DroidWordnetEntry.java
  31. +16
    -0
      src/main/java/org/zorgblub/rikai/HtmlEntryUtils.java
  32. +713
    -0
      src/main/java/org/zorgblub/rikai/ImageUtil.java
  33. +506
    -0
      src/main/java/org/zorgblub/rikai/SpannableHook.java
  34. +160
    -0
      src/main/java/org/zorgblub/rikai/download/Downloader.java
  35. +30
    -0
      src/main/java/org/zorgblub/rikai/download/OnFinishTaskListener.java
  36. +32
    -0
      src/main/java/org/zorgblub/rikai/download/OnProgressListener.java
  37. +125
    -0
      src/main/java/org/zorgblub/rikai/download/SimpleDownloader.java
  38. +118
    -0
      src/main/java/org/zorgblub/rikai/download/SimpleExtractor.java
  39. +144
    -0
      src/main/java/org/zorgblub/rikai/download/Unzipper.java
  40. +57
    -0
      src/main/java/org/zorgblub/rikai/download/settings/DeinflectorSettings.java
  41. +92
    -0
      src/main/java/org/zorgblub/rikai/download/settings/DictionarySettings.java
  42. +54
    -0
      src/main/java/org/zorgblub/rikai/download/settings/DictionaryType.java
  43. +73
    -0
      src/main/java/org/zorgblub/rikai/download/settings/DownloadableSettings.java
  44. +95
    -0
      src/main/java/org/zorgblub/rikai/download/settings/EdictSettings.java
  45. +56
    -0
      src/main/java/org/zorgblub/rikai/download/settings/EnamdictSettings.java
  46. +76
    -0
      src/main/java/org/zorgblub/rikai/download/settings/EpwingSettings.java
  47. +87
    -0
      src/main/java/org/zorgblub/rikai/download/settings/KanjidicSettings.java
  48. +110
    -0
      src/main/java/org/zorgblub/rikai/download/settings/WordnetSettings.java
  49. +40
    -0
      src/main/java/org/zorgblub/rikai/download/settings/ui/DictionaryConfigActivity.java
  50. +136
    -0
      src/main/java/org/zorgblub/rikai/download/settings/ui/DictionaryConfigFragment.java
  51. +149
    -0
      src/main/java/org/zorgblub/rikai/download/settings/ui/DictionaryConfigItemAdapter.java
  52. +83
    -0
      src/main/java/org/zorgblub/rikai/download/settings/ui/dialog/DictionaryConfigDialog.java
  53. +37
    -0
      src/main/java/org/zorgblub/rikai/download/settings/ui/dialog/EdictConfigDialog.java
  54. +21
    -0
      src/main/java/org/zorgblub/rikai/download/settings/ui/dialog/EnamdictConfigDialog.java
  55. +78
    -0
      src/main/java/org/zorgblub/rikai/download/settings/ui/dialog/EpwingConfigDialog.java
  56. +35
    -0
      src/main/java/org/zorgblub/rikai/download/settings/ui/dialog/KanjidicConfigDialog.java
  57. +77
    -0
      src/main/java/org/zorgblub/rikai/download/settings/ui/dialog/WordnetConfigDialog.java
  58. +92
    -0
      src/main/java/org/zorgblub/rikai/glosslist/DictionaryEntryAdapter.java
  59. +111
    -0
      src/main/java/org/zorgblub/rikai/glosslist/DictionaryListView.java
  60. +197
    -0
      src/main/java/org/zorgblub/rikai/glosslist/DictionaryPagerAdapter.java
  61. +262
    -0
      src/main/java/org/zorgblub/rikai/glosslist/DictionaryPane.java
  62. +227
    -0
      src/main/java/org/zorgblub/rikai/glosslist/DraggablePane.java
  63. +128
    -0
      src/main/java/org/zorgblub/rikai/glosslist/PinchableListView.java
  64. +58
    -0
      src/main/java/org/zorgblub/rikai/glosslist/SelectedWord.java
  65. +9
    -0
      src/main/java/org/zorgblub/rikai/glosslist/SizeChangeListener.java
  66. +4
    -0
      src/main/java/org/zorgblub/rikai/glosslist/TextPosition.java
  67. BIN
      src/main/res/drawable/add.png
  68. BIN
      src/main/res/drawable/cross.png
  69. +8
    -0
      src/main/res/drawable/dictionary_list_item_selector.xml
  70. BIN
      src/main/res/drawable/typhon_icon.png
  71. +29
    -0
      src/main/res/layout/definition_row.xml
  72. +73
    -0
      src/main/res/layout/dictionary_edict_settings.xml
  73. +51
    -0
      src/main/res/layout/dictionary_enamdict_settings.xml
  74. +75
    -0
      src/main/res/layout/dictionary_epwing_settings.xml
  75. +74
    -0
      src/main/res/layout/dictionary_kanjidic_settings.xml
  76. +6
    -0
      src/main/res/layout/dictionary_lang_item.xml
  77. +36
    -0
      src/main/res/layout/dictionary_list_activity.xml
  78. +22
    -0
      src/main/res/layout/dictionary_list_fragment.xml
  79. +31
    -0
      src/main/res/layout/dictionary_list_item.xml
  80. +18
    -0
      src/main/res/layout/dictionary_loading.xml
  81. +134
    -0
      src/main/res/layout/dictionary_wordnet_settings.xml
  82. +70
    -0
      src/main/res/layout/expandable_listview.xml
  83. +12
    -0
      src/main/res/values/attrs.xml
  84. +23
    -0
      src/main/res/values/colors.xml
  85. +78
    -0
      src/main/res/values/strings.xml

+ 6
- 0
.classpath View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

+ 16
- 0
.gitignore View File

@ -0,0 +1,16 @@
/build
.gradle
.idea
.settings
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild
app/src/main/assets/*.jar
app/src/main/assets/*.dex

+ 23
- 0
.project View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Typhonlib</name>
<comment>Project typhonlib created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

+ 62
- 0
build.gradle View File

@ -0,0 +1,62 @@
buildscript {
repositories {
mavenLocal()
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.github.axet:gradle-android-dx:0.0.4'
}
}
allprojects {
repositories {
mavenLocal()
google()
jcenter()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
minSdkVersion 15
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
def room_version = "2.2.5"
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'com.github.woxthebox:draglistview:1.2.1'
implementation group: 'commons-io', name: 'commons-io', version: '2.5'
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation "androidx.room:room-runtime:$room_version"
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.ichi2.anki:api:1.1.0alpha6'
testImplementation 'junit:junit:4.12'
}

+ 0
- 0
consumer-rules.pro View File


+ 19
- 0
gradle.properties View File

@ -0,0 +1,19 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true

BIN
gradle/wrapper/gradle-wrapper.jar View File


+ 6
- 0
gradle/wrapper/gradle-wrapper.properties View File

@ -0,0 +1,6 @@
#Wed Jul 08 12:03:41 EET 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

+ 172
- 0
gradlew View File

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

+ 84
- 0
gradlew.bat View File

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

BIN
libs/JRikai-1.0.jar View File


BIN
libs/eb4j-core-1.0.5.jar View File


+ 21
- 0
proguard-rules.pro View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

+ 1
- 0
settings.gradle View File

@ -0,0 +1 @@
rootProject.name = "Typhonlib"

+ 15
- 0
src/main/AndroidManifest.xml View File

@ -0,0 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.zorgblub.rikai">
<application>
<activity android:name="org.zorgblub.rikai.download.settings.ui.DictionaryConfigActivity"
>
<intent-filter>
<action android:name="org.zorgblub.rikai.download.settings.ui.DictionaryConfigActivity" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

+ 41
- 0
src/main/java/org/zorgblub/anki/AnkiDroidConfig.java View File

@ -0,0 +1,41 @@
package org.zorgblub.anki;
public final class
AnkiDroidConfig {
// Name of model which will be created in AnkiDroid
public static final String MODEL_NAME = "org.zorgblub.Typhon";
// Optional space separated list of tags to add to every note
public static final String TAGS = "Typhon";
// List of field names that will be used in AnkiDroid model
public static final String[] FIELDS = {"Expression","Reading","Meaning","Sentence", "Reason", "Deinflected"};
// List of card names that will be used in AnkiDroid (one for each direction of learning)
public static final String[] CARD_NAMES = {"Japanese>English"};
// CSS to share between all the cards (optional). User will need to install the NotoSans font by themselves
public static final String CSS = ".card {\n" +
" font-family: NotoSansJP;\n" +
" font-size: 24px;\n" +
" text-align: center;\n" +
" color: black;\n" +
" background-color: white;\n" +
" word-wrap: break-word;\n" +
"}\n" +
"@font-face { font-family: \"NotoSansJP\"; src: url('_NotoSansJP-Regular.otf'); }\n" +
"@font-face { font-family: \"NotoSansJP\"; src: url('_NotoSansJP-Bold.otf'); font-weight: bold; }\n" +
"\n" +
".big { font-size: 64px; }\n" +
".emph { font-weight: \"bold\"; }\n" +
".small { font-size: 18px;}\n";
// Template for the question of each card
static final String QFMT1 = "<div class=big>{{Expression}}</div><br>{{Sentence}}";
public static final String[] QFMT = {QFMT1};
// Template for the answer (use identical for both sides)
static final String AFMT1 = "{{Meaning}}<br><br>\n" +
"<div class=small>\n" +
"{{Expression}} ({{Reading}})<br><br>\n" +
"{{#Reason}}Deinflection: {{Deinflected}}{{Reason}}{{/Reason}}\n" +
"</div>";
public static final String[] AFMT = {AFMT1};
// Define two keys which will be used when using legacy ACTION_SEND intent
public static final String FRONT_SIDE_KEY = FIELDS[0];
public static final String BACK_SIDE_KEY = FIELDS[2];
}

+ 205
- 0
src/main/java/org/zorgblub/anki/AnkiDroidHelper.java View File

@ -0,0 +1,205 @@
package org.zorgblub.anki;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.appcompat.app.AppCompatActivity;
import android.util.SparseArray;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import com.ichi2.anki.api.AddContentApi;
import com.ichi2.anki.api.NoteInfo;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import static com.ichi2.anki.api.AddContentApi.READ_WRITE_PERMISSION;
public class AnkiDroidHelper {
private static final String DECK_REF_DB = "org.zorgblub.rikai.anki.decks";
private static final String MODEL_REF_DB = "org.zorgblub.rikai.anki.models";
private AddContentApi mApi;
private Context mContext;
public AnkiDroidHelper(Context context) {
mContext = context.getApplicationContext();
mApi = new AddContentApi(mContext);
}
public AddContentApi getApi() {
return mApi;
}
/**
* Whether or not the API is available to use.
* The API could be unavailable if AnkiDroid is not installed or the user explicitly disabled the API
* @return true if the API is available to use
*/
public static boolean isApiAvailable(Context context) {
return AddContentApi.getAnkiDroidPackageName(context) != null;
}
/**
* Whether or not we should request full access to the AnkiDroid API
*/
public boolean shouldRequestPermission() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
}
return ContextCompat.checkSelfPermission(mContext, READ_WRITE_PERMISSION) != PackageManager.PERMISSION_GRANTED;
}
/**
* Request permission from the user to access the AnkiDroid API (for SDK 23+)
* @param callbackActivity An Activity which implements onRequestPermissionsResult()
* @param callbackCode The callback code to be used in onRequestPermissionsResult()
*/
public void requestPermission(FragmentActivity callbackActivity, int callbackCode) {
ActivityCompat.requestPermissions(callbackActivity, new String[]{READ_WRITE_PERMISSION}, callbackCode);
}
/**
* Save a mapping from deckName to getDeckId in the SharedPreferences
*/
public void storeDeckReference(String deckName, long deckId) {
final SharedPreferences decksDb = mContext.getSharedPreferences(DECK_REF_DB, Context.MODE_PRIVATE);
decksDb.edit().putLong(deckName, deckId).apply();
}
/**
* Save a mapping from modelName to modelId in the SharedPreferences
*/
public void storeModelReference(String modelName, long modelId) {
final SharedPreferences modelsDb = mContext.getSharedPreferences(MODEL_REF_DB, Context.MODE_PRIVATE);
modelsDb.edit().putLong(modelName, modelId).apply();
}
/**
* Remove the duplicates from a list of note fields and tags
* @param fields List of fields to remove duplicates from
* @param tags List of tags to remove duplicates from
* @param modelId ID of model to search for duplicates on
*/
public void removeDuplicates(LinkedList<String []> fields, LinkedList<Set<String>> tags, long modelId) {
// Build a list of the duplicate keys (first fields) and find all notes that have a match with each key
List<String> keys = new ArrayList<>(fields.size());
for (String[] f: fields) {
keys.add(f[0]);
}
SparseArray<List<NoteInfo>> duplicateNotes = getApi().findDuplicateNotes(modelId, keys);
// Do some sanity checks
if (tags.size() != fields.size()) {
throw new IllegalStateException("List of tags must be the same length as the list of fields");
}
if (duplicateNotes == null || duplicateNotes.size() == 0 || fields.size() == 0 || tags.size() == 0) {
return;
}
if (duplicateNotes.keyAt(duplicateNotes.size() - 1) >= fields.size()) {
throw new IllegalStateException("The array of duplicates goes outside the bounds of the original lists");
}
// Iterate through the fields and tags LinkedLists, removing those that had a duplicate
ListIterator<String[]> fieldIterator = fields.listIterator();
ListIterator<Set<String>> tagIterator = tags.listIterator();
int listIndex = -1;
for (int i = 0; i < duplicateNotes.size(); i++) {
int duplicateIndex = duplicateNotes.keyAt(i);
while (listIndex < duplicateIndex) {
fieldIterator.next();
tagIterator.next();
listIndex++;
}
fieldIterator.remove();
tagIterator.remove();
}
}
/**
* Try to find the given model by name, accounting for renaming of the model:
* If there's a model with this modelName that is known to have previously been created (by this app)
* and the corresponding model ID exists and has the required number of fields
* then return that ID (even though it may have since been renamed)
* If there's a model from #getModelList with modelName and required number of fields then return its ID
* Otherwise return null
* @param modelName the name of the model to find
* @param numFields the minimum number of fields the model is required to have
* @return the model ID or null if something went wrong
*/
public Long findModelIdByName(String modelName, int numFields) {
SharedPreferences modelsDb = mContext.getSharedPreferences(MODEL_REF_DB, Context.MODE_PRIVATE);
long prefsModelId = modelsDb.getLong(modelName, -1L);
// if we have a reference saved to modelName and it exists and has at least numFields then return it
if ((prefsModelId != -1L)
&& (mApi.getModelName(prefsModelId) != null)
&& (mApi.getFieldList(prefsModelId) != null)
&& (mApi.getFieldList(prefsModelId).length >= numFields)) { // could potentially have been renamed
return prefsModelId;
}
Map<Long, String> modelList = mApi.getModelList(numFields);
if (modelList != null) {
for (Map.Entry<Long, String> entry : modelList.entrySet()) {
if (entry.getValue().equals(modelName)) {
return entry.getKey(); // first model wins
}
}
}
// model no longer exists (by name nor old id), the number of fields was reduced, or API error
return null;
}
/**
* Try to find the given deck by name, accounting for potential renaming of the deck by the user as follows:
* If there's a deck with deckName then return it's ID
* If there's no deck with deckName, but a ref to deckName is stored in SharedPreferences, and that deck exist in
* AnkiDroid (i.e. it was renamed), then use that deck.Note: this deck will not be found if your app is re-installed
* If there's no reference to deckName anywhere then return null
* @param deckName the name of the deck to find
* @return the did of the deck in Anki
*/
public Long findDeckIdByName(String deckName) {
SharedPreferences decksDb = mContext.getSharedPreferences(DECK_REF_DB, Context.MODE_PRIVATE);
// Look for deckName in the deck list
Long did = getDeckId(deckName);
if (did != null) {
// If the deck was found then return it's id
return did;
} else {
// Otherwise try to check if we have a reference to a deck that was renamed and return that
did = decksDb.getLong(deckName, -1);
if (did != -1 && mApi.getDeckName(did) != null) {
return did;
} else {
// If the deck really doesn't exist then return null
return null;
}
}
}
/**
* Get the ID of the deck which matches the name
* @param deckName Exact name of deck (note: deck names are unique in Anki)
* @return the ID of the deck that has given name, or null if no deck was found or API error
*/
private Long getDeckId(String deckName) {
Map<Long, String> deckList = mApi.getDeckList();
if (deckList != null) {
for (Map.Entry<Long, String> entry : deckList.entrySet()) {
if (entry.getValue().equalsIgnoreCase(deckName)) {
return entry.getKey();
}
}
}
return null;
}
}

+ 49
- 0
src/main/java/org/zorgblub/rikai/DictionaryService.java View File

@ -0,0 +1,49 @@
package org.zorgblub.rikai;
import android.content.Context;
import android.util.Pair;
import org.rikai.dictionary.AbstractEntry;
import org.rikai.dictionary.Dictionary;
import org.rikai.dictionary.Entries;
import org.zorgblub.rikai.download.settings.DictionarySettings;
import org.zorgblub.rikai.download.settings.DownloadableSettings;
import org.zorgblub.rikai.glosslist.SelectedWord;
import java.util.List;
/**
* Created by Benjamin on 23/03/2016.
*/
public interface DictionaryService {
void initDictionaries(Context context);
DictionaryServiceImpl.DictionaryStatus checkDictionaries(List<DictionarySettings> list);
void downloadAndExtract(List<DownloadableSettings> list, Context context);
Dictionary getDictionary(int index);
int getNbDictionaries();
void setDictionaryListener(DictionaryServiceImpl.DictionaryListener dictionaryListener);
Entries query(int dicIndex, SelectedWord word);
Pair<SelectedWord, Entries> getLastMatch(int dicIndex);
boolean saveInAnki(AbstractEntry abstractEntry, Context context, SelectedWord selectedWord, String bookTitle) throws IllegalAccessException;
void setCurrentDictionary(int index);
void setMessageListener(DictionaryServiceImpl.MessageListener messageListener);
List<DownloadableSettings> getDownloadableSettings();
List<DictionarySettings> getSettings();
void saveSettings(List<DictionarySettings> settings);
long getLastUpdateTimestamp();
}

+ 573
- 0
src/main/java/org/zorgblub/rikai/DictionaryServiceImpl.java View File

@ -0,0 +1,573 @@
package org.zorgblub.rikai;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.appcompat.app.AlertDialog;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.ichi2.anki.api.AddContentApi;
import org.rikai.dictionary.AbstractEntry;
import org.rikai.dictionary.Dictionary;
import org.rikai.dictionary.DictionaryException;
import org.rikai.dictionary.DictionaryNotLoadedException;
import org.rikai.dictionary.Entries;
import org.rikai.dictionary.db.DatabaseException;
import org.rikai.dictionary.edict.EdictEntry;
import org.zorgblub.anki.AnkiDroidConfig;
import org.zorgblub.anki.AnkiDroidHelper;
import org.zorgblub.rikai.download.SimpleDownloader;
import org.zorgblub.rikai.download.SimpleExtractor;
import org.zorgblub.rikai.download.settings.DictionarySettings;
import org.zorgblub.rikai.download.settings.DictionaryType;
import org.zorgblub.rikai.download.settings.DownloadableSettings;
import org.zorgblub.rikai.glosslist.SelectedWord;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Created by Benjamin on 23/03/2016.
*/
public class DictionaryServiceImpl implements DictionaryService {
public static final String ANKI_DECK_NAME = "Typhon";
private static String DICTIONARY_VERSION = "DICTIONARY_VERSION";
private static String DICTIONARY_SETTINGS = "DICTIONARY_SETTINGS";
public interface RikaiConfig {
String getAnkiDeckName();
String getDictionaryVersion();
void setDictionaryVersion(String version);
String getDictionarySettings();
void setDictionarySettings(String settings);
}
public class DefaultRikaiConfig implements RikaiConfig {
@Override
public String getAnkiDeckName() {
return ANKI_DECK_NAME;
}
@Override
public String getDictionaryVersion() {
// Defaults to current dictionary version to allow for manual install (this probably should go away)
return sharedPreferences.getString(DICTIONARY_VERSION, DownloadableSettings.getDictionaryVersion());
}
@Override
public void setDictionaryVersion(String version) {
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putString(DICTIONARY_VERSION, version);
edit.apply();
}
@Override
public String getDictionarySettings() {
return sharedPreferences.getString(DICTIONARY_SETTINGS, null);
}
@Override
public void setDictionarySettings(String settings) {
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putString(DICTIONARY_SETTINGS, settings);
edit.apply();
}
}
private RikaiConfig config = new DefaultRikaiConfig();
private Context context;
private ArrayList<Dictionary> dictionaries = new ArrayList<>();
private DictionaryListener dictionaryListener;
private MessageListener messageListener;
private int currentDictionary;
private SparseArray<Pair<SelectedWord, Entries>> matchesCaches = new SparseArray<>();
private boolean initialized;
private SharedPreferences sharedPreferences;
private long lastUpdate = System.currentTimeMillis();
private static final Logger LOG = Logger.getLogger("DictionaryService");
private DictionaryServiceImpl() {
}
@Override
public void initDictionaries(Context context) {
if (initialized) {
fireDictionaryLoaded();
return;
}
sharedPreferences = context.getSharedPreferences("TYPHON", Context.MODE_PRIVATE);
List<DictionarySettings> dicSettings = getSettings();
DictionaryStatus status = checkDictionaries(dicSettings);
fireDictionaryChecked(status);
if (status.equals(DictionaryStatus.OK)) {
loadDictionaries(dicSettings, context);
}
}
@Override
public List<DictionarySettings> getSettings() {
List<DictionarySettings> dicSettings;
String dictionarySettingsStr = config.getDictionarySettings();
if (dictionarySettingsStr == null || dictionarySettingsStr.length() == 0)
return getDefaultDictionaries();
return deserializeSettings(dictionarySettingsStr);
}
@Override
public void saveSettings(List<DictionarySettings> settings) {
String settingsStr = serializeSettings(settings);
config.setDictionarySettings(settingsStr);
this.lastUpdate = System.currentTimeMillis();
}
@Override
public long getLastUpdateTimestamp() {
return lastUpdate;
}
protected String serializeSettings(List<DictionarySettings> settings) {
Type targetClassType = new TypeToken<ArrayList<DictionarySettings>>() {
}.getType();
GsonBuilder builder = new GsonBuilder();
Gson gson = builder.create();
return gson.toJson(settings, targetClassType);
}
protected List<DictionarySettings> deserializeSettings(String settingsStr) {
GsonBuilder builder = new GsonBuilder().registerTypeAdapter(DictionarySettings.class,
new JsonDeserializer<DictionarySettings>() {
@Override
public DictionarySettings deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject asJsonObject = json.getAsJsonObject();
JsonElement typeOfSettings = asJsonObject.get("type");
if (typeOfSettings == null)
return context.deserialize(json, typeOfT);
String type = typeOfSettings.getAsString();
DictionaryType dictionaryType = DictionaryType.valueOf(type);
DictionarySettings deserialize = context.deserialize(json, dictionaryType.getSettingsClass());
return deserialize;
}
});
Gson gson = builder.create();
Type targetClassType = new TypeToken<ArrayList<DictionarySettings>>() {
}.getType();
return gson.fromJson(settingsStr, targetClassType);
}
protected List<DictionarySettings> getDefaultDictionaries() {
List<DictionarySettings> list = new ArrayList<>();
for (DictionaryType type : DictionaryType.values()) {
DictionarySettings implementation = type.getImplementation();
if (!implementation.isDownloadable())
continue;
list.add(implementation);
}
return list;
}
private void loadDictionaries(List<DictionarySettings> list, Context context) {
for (DictionarySettings settings : list) {
try {
Dictionary dictionary = settings.newInstance();
if (dictionary == null) {
continue;
}
dictionaries.add(dictionary);
} catch (FileNotFoundException e) {
LOG.log(Level.SEVERE, "Could not find dictionary data", e);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Could not read dictionary data", e);
} catch (DatabaseException e) {
LOG.log(Level.SEVERE, "Could not load dictionary data", e);
}
}
Runnable task = new Runnable() {
@Override
public void run() {
for (Dictionary dictionary : dictionaries) {
try {
dictionary.load();
} catch (DictionaryException e) {
dictionaries.remove(dictionary);
}
}
fireDictionaryLoaded();
initialized = true;
}
};
Thread thread = new Thread(task);
thread.run();
}
public enum DictionaryStatus {
OK,
UPDATE_NEEDED,
NOT_EXISTENT
}
@Override
public DictionaryStatus checkDictionaries(List<DictionarySettings> list) {
boolean allExistent = true;
for (DictionarySettings settings : list) {
if (settings.isDownloadable() && !settings.exists()) {
allExistent = false;
break;
}
}
if (allExistent) {
if (config.getDictionaryVersion().compareToIgnoreCase(DownloadableSettings.getDictionaryVersion()) < 0) {
return DictionaryStatus.UPDATE_NEEDED;
} else {
return DictionaryStatus.OK;
}
}
return DictionaryStatus.NOT_EXISTENT;
}
@Override
public List<DownloadableSettings> getDownloadableSettings() {
List<DownloadableSettings> list = new ArrayList<>();
for (DictionarySettings settings : this.getSettings()) {
if (settings instanceof DownloadableSettings) {
list.add((DownloadableSettings) settings);
}
}
return list;
}
@Override
public void downloadAndExtract(List<DownloadableSettings> list, Context context) {
// TODO Separate the UI dialogs with the download business
// TODO trouble dialog with more details would be good
try {
DownloadableSettings.deleteAll();
} catch (IOException e) {
showDownloadTroubleDialog(context);
}
File dataPath = DictionarySettings.getDataPath();
if (!dataPath.exists()) {
if (!dataPath.mkdirs()) {
showDownloadTroubleDialog(context);
return;
}
}
final SimpleDownloader downloader = new SimpleDownloader(context);
downloader.setOnFinishTaskListener((boolean success) -> {
if (success) {
extract(list, context);
} else {
DownloadableSettings.getZipFile().delete();
showDownloadTroubleDialog(context);
}
});
downloader.execute(DownloadableSettings.getDownloadUrl(), DownloadableSettings.getZipFile().getAbsolutePath());
}
public void extract(List<DownloadableSettings> dictInfo, Context context) {
Set<String> filenames = new HashSet<>();
for (DownloadableSettings settings : dictInfo) {
File[] files = settings.getFiles();
for (File file :
files) {
filenames.add(file.getName());
}
}
final SimpleExtractor extractor = new SimpleExtractor(context, filenames);
extractor.setOnFinishTasklistener((boolean success) -> {
if (success) {
DownloadableSettings.deleteZip();
config.setDictionaryVersion(DownloadableSettings.getDictionaryVersion());
initDictionaries(context);
} else {
for (DownloadableSettings settings : dictInfo) {
settings.delete();
}
showDownloadTroubleDialog(context);
}
});
extractor.execute(DownloadableSettings.getZipFile().getAbsolutePath());
}
private void showDownloadTroubleDialog(Context context) {
new AlertDialog.Builder(context)
.setMessage(R.string.dm_dict_alternate_download_address)
.setPositiveButton(R.string.msg_ok, null)
.create()
.show();
// TODO Add retry button
// TODO Give details about the error that happened to
}
@Override
public Dictionary getDictionary(int index) {
return this.dictionaries.get(index);
}
@Override
public int getNbDictionaries() {
return this.dictionaries.size();
}
public interface DictionaryListener {
void onDictionaryChecked(DictionaryStatus status);
void onDictionaryLoaded();
void onCurrentDictionaryChanged(int index);
void onCurrentMatchChanged(SelectedWord word, Entries match);
}
private void fireDictionaryChecked(DictionaryStatus status) {
if (dictionaryListener == null)
return;
dictionaryListener.onDictionaryChecked(status);
}
private void fireDictionaryLoaded() {
if (dictionaryListener == null)
return;
dictionaryListener.onDictionaryLoaded();
}
private void fireCurrentDictionaryChanged(int index) {
if (dictionaryListener == null)
return;
dictionaryListener.onCurrentDictionaryChanged(index);
}
private void fireCurrentMatchChanged(SelectedWord word, Entries match) {
if (dictionaryListener == null)
return;
dictionaryListener.onCurrentMatchChanged(word, match);
}
@Override
public void setDictionaryListener(DictionaryListener dictionaryListener) {
this.dictionaryListener = dictionaryListener;
}
public interface MessageListener {
void onMessage(int id);
void onMessage(int id, Object... params);
}
public void fireOnMessage(int id) {
if (this.messageListener != null)
this.messageListener.onMessage(id);
}
public void fireOnMessage(int id, Object... params) {
if (this.messageListener != null)
this.messageListener.onMessage(id, params);
}
public void setMessageListener(MessageListener messageListener) {
this.messageListener = messageListener;
}
public void setCurrentDictionary(int index) {
currentDictionary = index;
fireCurrentDictionaryChanged(index);
Pair<SelectedWord, Entries> cacheHit = matchesCaches.get(index);
if (cacheHit != null) {
fireCurrentMatchChanged(cacheHit.first, cacheHit.second);
}
}
public Entries query(int dicIndex, SelectedWord word) {
Dictionary dictionary = this.getDictionary(dicIndex);
Entries entries;
Pair<SelectedWord, Entries> cacheHit = matchesCaches.get(dicIndex);
if (cacheHit != null && cacheHit.first.equals(word)) {
return cacheHit.second;
}
try {
entries = dictionary.query(word.getText().toString());
if (entries.size() == 0) {
entries.add(new DroidEdictEntry("No word found for this dictionary")); // TODO translate
}
} catch (DictionaryNotLoadedException e) {
entries = new Entries();
entries.add(new DroidEdictEntry("Dictionary not yet loaded")); // TODO translate
}
if (dicIndex == currentDictionary) {
fireCurrentMatchChanged(word, entries);
}
matchesCaches.put(dicIndex, new Pair<SelectedWord, Entries>(word, entries));
return entries;
}
@Override
public Pair<SelectedWord, Entries> getLastMatch(int dicIndex) {
return matchesCaches.get(dicIndex);
}
@Override
public boolean saveInAnki(AbstractEntry abstractEntry, Context context, SelectedWord selectedWord, String bookTitle) {
// Currently working only for edict
if (!(abstractEntry instanceof EdictEntry)) {
fireOnMessage(R.string.anki_card_add_not_supported);
return false;
}
if (!AnkiDroidHelper.isApiAvailable(context)) {
// AnkiDroid not installed
fireOnMessage(R.string.anki_not_installed);
throw new UnsupportedOperationException("Anki is not available");
}
EdictEntry entry = (EdictEntry) abstractEntry;
try {
// Get api instance
final AnkiDroidHelper ankiHelper = new AnkiDroidHelper(context);
if(ankiHelper.shouldRequestPermission()){
fireOnMessage(R.string.anki_permission_denied);
throw new IllegalAccessException("The application does not possess the permission to add an anki entry");
}
final AddContentApi api = ankiHelper.getApi();
// Look for our deck, add a new one if it doesn't exist
String ankiDeckName = config.getAnkiDeckName();
Long did = ankiHelper.findDeckIdByName(ankiDeckName);
if (did == null) {
did = api.addNewDeck(ankiDeckName);
ankiHelper.storeDeckReference(ankiDeckName, did);
}
// Look for our model, add a new one if it doesn't exist
Long mid = ankiHelper.findModelIdByName(AnkiDroidConfig.MODEL_NAME, AnkiDroidConfig.FIELDS.length);
if (mid == null) {
mid = api.addNewCustomModel(AnkiDroidConfig.MODEL_NAME, AnkiDroidConfig.FIELDS,
AnkiDroidConfig.CARD_NAMES, AnkiDroidConfig.QFMT, AnkiDroidConfig.AFMT,
AnkiDroidConfig.CSS, did, null);
ankiHelper.storeModelReference(AnkiDroidConfig.MODEL_NAME, mid);
}
// Double-check that everything was added correctly
String[] fieldNames = api.getFieldList(mid);
if (mid == null || did == null || fieldNames == null) {
fireOnMessage(R.string.anki_card_add_fail);
return false;
}
String sentence = selectedWord.getContextSentence().toString();
String originalWord = entry.getOriginalWord();
sentence = sentence.replace(originalWord, "<span class=\"emph\">" + originalWord + "</span>");
String[] flds = {originalWord, entry.getReading(), entry.getGloss(), sentence, entry.getReason(), entry.getWord()};
// Add a new note using the current field map
// Only add item if there aren't any duplicates
Set<String> tags = new HashSet<>();
tags.add(AnkiDroidConfig.TAGS);
tags.add(bookTitle.replaceAll("[^A-Za-z0-9]", "_"));
Long noteUri = api.addNote(mid, did, flds, tags);
if (noteUri != null) {
fireOnMessage(R.string.anki_card_added, flds[0]);
}
LinkedList<String[]> linkedflds = new LinkedList<>();
linkedflds.add(flds);
LinkedList<Set<String>> linkedtags = new LinkedList<>();
linkedtags.add(tags);
ankiHelper.removeDuplicates(linkedflds, linkedtags, mid);
} catch (Exception e) {
Log.e("AnkiCardAdd", "Exception adding cards to AnkiDroid", e);
fireOnMessage(R.string.anki_card_add_fail);
return false;
}
return true;
}
/* Singleton pattern */
private static DictionaryService instance;
public static DictionaryService get() {
if (instance == null) instance = getSync();
return instance;
}
private static synchronized DictionaryService getSync() {
if (instance == null) instance = new DictionaryServiceImpl();
return instance;
}
public static synchronized void reset() {
instance = null;
}
}

+ 96
- 0
src/main/java/org/zorgblub/rikai/DroidEdictEntry.java View File

@ -0,0 +1,96 @@
package org.zorgblub.rikai;
import android.graphics.Color;
import android.text.Html;
import android.text.Spanned;
import org.rikai.deinflector.DeinflectedWord;
import org.rikai.dictionary.edict.EdictEntry;
import static org.zorgblub.rikai.HtmlEntryUtils.wrapColor;
/**
* Created by Benjamin on 16/09/2015.
*/
public class DroidEdictEntry extends EdictEntry implements DroidEntry {
// default value are set to the rikaichan style
private int kanjiColor = -4724737;
private int kanaColor = -4128832;
private int definitionColor = -1;
private int reasonColor = -8032;
public DroidEdictEntry() {
}
public DroidEdictEntry(DeinflectedWord variant, String word, String reading, String gloss, String reason) {
super(variant, word, reading, gloss, reason);
}
public DroidEdictEntry(String msg){
super(new DeinflectedWord(msg), msg, "", "", "");
}
public int getKanjiColor() {
return kanjiColor;
}
public void setKanjiColor(int kanjiColor) {
this.kanjiColor = kanjiColor;
}
public int getKanaColor() {
return kanaColor;
}
public int getReasonColor() {
return reasonColor;
}
public int getDefinitionColor() {
return definitionColor;
}
public void setDefinitionColor(int definitionColor) {
this.definitionColor = definitionColor;
}
public void setKanaColor(int kanaColor) {
this.kanaColor = kanaColor;
}
public void setReasonColor(int reasonColor) {
this.reasonColor = reasonColor;
}
@Override
public int getBackgroundColor() {
return Color.BLACK;
}
@Override
public String toStringCompact() {
StringBuilder result = new StringBuilder(this.getLength());
result.append(wrapColor(kanjiColor, this.getWord()));
if (this.getReading().length() != 0) {
result.append(" [").append(wrapColor(kanaColor, this.getReading())).append("]");
}
if (this.getReason().length() != 0) {
result.append(" (").append(wrapColor(reasonColor, this.getReason())).append(")");
}
result.append("<br/>").append(wrapColor(definitionColor, this.getGloss()));
return result.toString();
}
@Override
public Spanned render() {
return Html.fromHtml(this.toStringCompact());
}
}

+ 14
- 0
src/main/java/org/zorgblub/rikai/DroidEntry.java View File

@ -0,0 +1,14 @@
package org.zorgblub.rikai;
import android.text.Spanned;
/**
* Created by Benjamin on 20/10/2015.
*/
public interface DroidEntry {
int getBackgroundColor();
Spanned render();
}

+ 49
- 0
src/main/java/org/zorgblub/rikai/DroidEpwingDictionary.java View File

@ -0,0 +1,49 @@
package org.zorgblub.rikai;
import org.rikai.dictionary.epwing.EpwingDictionary;
import org.rikai.dictionary.epwing.EpwingEntry;
import fuku.eb4j.Result;
import fuku.eb4j.SubBook;
/**
* Created by Benjamin on 20/03/2016.
*/
public class DroidEpwingDictionary<T> extends EpwingDictionary {
private String name;
private SpannableHook spannableHook;
public DroidEpwingDictionary(String path) {
super(path, null);
spannableHook = new SpannableHook();
//TODO spannableHook.setContext(Typhon.get().getApplicationContext());
setHook(spannableHook);
}
@Override
public void load() {
super.load();
SubBook subBook = this.getSubBook();
spannableHook.setSubBook(subBook);
}
@Override
protected EpwingEntry makeEntry(String originalWord, Result result, SubBook subBook) {
return new DroidEpwingEntry(originalWord, result, subBook, this.getHook());
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return this.getName();
}
}

+ 36
- 0
src/main/java/org/zorgblub/rikai/DroidEpwingEntry.java View File

@ -0,0 +1,36 @@
package org.zorgblub.rikai;
import android.graphics.Color;
import android.text.Spannable;
import android.text.Spanned;
import org.rikai.dictionary.epwing.EpwingEntry;
import fuku.eb4j.Result;
import fuku.eb4j.SubBook;
import fuku.eb4j.hook.Hook;
/**
* Created by Benjamin on 21/03/2016.
*/
public class DroidEpwingEntry extends EpwingEntry<Spannable> implements DroidEntry {
private Spannable cachedRendering;
public DroidEpwingEntry(String originalWord, Result result, SubBook subBook, Hook<Spannable> hook) {
super(originalWord, result, subBook, hook);
}
@Override
public Spanned render() {
if(cachedRendering == null){
cachedRendering = getText();
}
return cachedRendering;
}
@Override
public int getBackgroundColor() {
return Color.BLACK;
}
}

+ 62
- 0
src/main/java/org/zorgblub/rikai/DroidKanjiDictionary.java View File

@ -0,0 +1,62 @@
package org.zorgblub.rikai;
import android.content.res.Resources;
import org.rikai.dictionary.kanji.KanjiDictionary;
import org.rikai.dictionary.kanji.KanjiEntry;
import java.io.FileNotFoundException;
/**
* Created by Benjamin on 17/09/2015.
*/
public class DroidKanjiDictionary extends KanjiDictionary {
private Resources resources;
private int kanjiColor;
private int kanaColor;
private int definitionColor;
private int indexColor;
private boolean heisig6 = true;
private String name = "Kanjidic";
public DroidKanjiDictionary(String path, int maxNbQueries, Resources resources, boolean heisig6) throws FileNotFoundException {
super(path, maxNbQueries);
this.resources = resources;
kanjiColor = this.resources.getColor(R.color.kanji);
kanaColor = this.resources.getColor(R.color.kana);
definitionColor = this.resources.getColor(R.color.definition);
indexColor = this.resources.getColor(R.color.index);
this.heisig6 = heisig6;
}
@Override
protected KanjiEntry makeEntry(char kanjiChar) {
DroidKanjiEntry droidEdictEntry = new DroidKanjiEntry(kanjiChar);
droidEdictEntry.setKanjiColor(kanjiColor);
droidEdictEntry.setKanaColor(kanaColor);
droidEdictEntry.setDefinitionColor(definitionColor);
droidEdictEntry.setIndexColor(indexColor);
droidEdictEntry.setHeisig6(this.heisig6);
return droidEdictEntry;
}
@Override
public String toString() {
return this.getName();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

+ 100
- 0
src/main/java/org/zorgblub/rikai/DroidKanjiEntry.java View File

@ -0,0 +1,100 @@
package org.zorgblub.rikai;
import android.graphics.Color;
import android.text.Html;
import android.text.Spanned;
import org.rikai.dictionary.kanji.KanjiEntry;
import org.rikai.dictionary.kanji.KanjiTag;
import java.util.Map;
/**
* Created by Benjamin on 17/09/2015.
*/
public class DroidKanjiEntry extends KanjiEntry implements DroidEntry {
boolean heisig6 = true;
// default value are set to the rikaichan style
private int kanjiColor = -4724737;
private int kanaColor = -4128832;
private int definitionColor = -1;
private int indexColor = -8032;
public DroidKanjiEntry(Character kanji) {
super(kanji);
}
public int getKanjiColor() {
return kanjiColor;
}
public void setKanjiColor(int kanjiColor) {
this.kanjiColor = kanjiColor;
}
public int getKanaColor() {
return kanaColor;
}
public void setKanaColor(int kanaColor) {
this.kanaColor = kanaColor;
}
public int getDefinitionColor() {
return definitionColor