├── app
├── .gitignore
├── src
│ └── main
│ │ ├── ic_launcher-playstore.png
│ │ ├── res
│ │ ├── ic_launcher-playstore.png
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_round.png
│ │ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_round.png
│ │ │ └── ic_launcher_foreground.png
│ │ ├── drawable
│ │ │ ├── magnifiying_glass.png
│ │ │ ├── cursor_dark.xml
│ │ │ ├── ic_baseline_add_24.xml
│ │ │ ├── ic_keyboard_arrow_right_24px.xml
│ │ │ ├── ic_close_white_24dp.xml
│ │ │ ├── ic_baseline_list_24.xml
│ │ │ ├── ic_baseline_bookmark_border_24.xml
│ │ │ ├── ic_baseline_save_24.xml
│ │ │ ├── baseline_more_vert_24.xml
│ │ │ ├── baseline_content_copy_24.xml
│ │ │ ├── ic_baseline_bookmark_added_24.xml
│ │ │ ├── ic_search_black_24dp.xml
│ │ │ ├── baseline_menu_book_24.xml
│ │ │ └── ic_settings_24px.xml
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_round.png
│ │ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_round.png
│ │ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_round.png
│ │ │ └── ic_launcher_foreground.png
│ │ ├── values
│ │ │ ├── dimens.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── colors.xml
│ │ │ └── strings.xml
│ │ ├── color
│ │ │ └── bnv_tab_item_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── menu
│ │ │ ├── saved_words_action_bar.xml
│ │ │ ├── search_action_bar.xml
│ │ │ ├── dictionary_manage_actions.xml
│ │ │ └── bottom_navigation.xml
│ │ ├── values-night-v21
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ ├── layout
│ │ │ ├── dict_priority_dialog.xml
│ │ │ ├── layout_loading_dialog.xml
│ │ │ ├── saved_word.xml
│ │ │ ├── fragment_saved_words.xml
│ │ │ ├── fragment_manage_dicts.xml
│ │ │ ├── dictionary.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── fragment_lookup.xml
│ │ │ ├── lookup_card.xml
│ │ │ └── fragment_word_detail.xml
│ │ ├── values-v21
│ │ │ └── styles.xml
│ │ ├── xml
│ │ │ └── preferences.xml
│ │ └── navigation
│ │ │ └── nav_graph.xml
│ │ ├── java
│ │ └── com
│ │ │ └── kamui
│ │ │ └── rin
│ │ │ ├── db
│ │ │ ├── model
│ │ │ │ ├── SavedWord.kt
│ │ │ │ ├── PitchAccent.kt
│ │ │ │ ├── Frequency.kt
│ │ │ │ ├── Dictionary.kt
│ │ │ │ ├── Tag.kt
│ │ │ │ └── DictEntry.kt
│ │ │ ├── dao
│ │ │ │ ├── FrequencyDao.kt
│ │ │ │ ├── PitchAccentDao.kt
│ │ │ │ ├── DictionaryDao.kt
│ │ │ │ ├── TagDao.kt
│ │ │ │ ├── SavedWordDao.kt
│ │ │ │ └── DictEntryDao.kt
│ │ │ └── AppDatabase.kt
│ │ │ ├── ui
│ │ │ ├── Theme.kt
│ │ │ └── fragment
│ │ │ │ ├── SettingsFragment.kt
│ │ │ │ ├── LookupFragment.kt
│ │ │ │ ├── WordDetailFragment.kt
│ │ │ │ ├── SavedWordsFragment.kt
│ │ │ │ └── ManageDictsFragment.kt
│ │ │ ├── dict
│ │ │ ├── worker
│ │ │ │ ├── IoUtils.kt
│ │ │ │ ├── DeleteDictionaryWorker.kt
│ │ │ │ ├── ImportFrequencyWorker.kt
│ │ │ │ ├── ImportPitchWorker.kt
│ │ │ │ ├── BaseDictionaryWorker.kt
│ │ │ │ └── ImportDictionaryWorker.kt
│ │ │ ├── Japanese.kt
│ │ │ ├── Lookup.kt
│ │ │ ├── YomichanDecoder.kt
│ │ │ └── Deinflector.kt
│ │ │ ├── CopyToClipboardReceiver.kt
│ │ │ ├── Settings.kt
│ │ │ ├── adapter
│ │ │ └── DictEntryAdapter.kt
│ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
├── build.gradle
└── schemas
│ └── com.kamui.rin.db.AppDatabase
│ ├── 2.json
│ ├── 5.json
│ ├── 3.json
│ └── 4.json
├── .idea
├── .name
├── .gitignore
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── vcs.xml
├── compiler.xml
├── kotlinc.xml
├── misc.xml
├── gradle.xml
└── jarRepositories.xml
├── settings.gradle
├── screenshots
├── saved.png
├── search.png
├── settings.png
├── light_mode.png
├── word_detail.png
└── manage_dicts.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── README.md
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | My Application
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "My Application"
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/screenshots/saved.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/screenshots/saved.png
--------------------------------------------------------------------------------
/screenshots/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/screenshots/search.png
--------------------------------------------------------------------------------
/screenshots/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/screenshots/settings.png
--------------------------------------------------------------------------------
/screenshots/light_mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/screenshots/light_mode.png
--------------------------------------------------------------------------------
/screenshots/word_detail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/screenshots/word_detail.png
--------------------------------------------------------------------------------
/screenshots/manage_dicts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/screenshots/manage_dicts.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/magnifiying_glass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/drawable/magnifiying_glass.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamui-fin/rin/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #C2EBEB
4 |
5 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cursor_dark.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Feb 26 17:16:07 CST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/model/SavedWord.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(
7 | )
8 | data class SavedWord(
9 | @PrimaryKey(autoGenerate = true) var savedWordId: Long = 0,
10 | var kanji: String,
11 | )
--------------------------------------------------------------------------------
/app/src/main/res/color/bnv_tab_item_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/model/PitchAccent.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity
7 | data class PitchAccent(
8 | @PrimaryKey(autoGenerate = true) var pitchId: Long = 0,
9 | val kanji: String,
10 | val pitch: String,
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/model/Frequency.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity
7 | data class Frequency(
8 | @PrimaryKey(autoGenerate = true) var freqId: Long = 0,
9 | val kanji: String,
10 | val frequency: Long,
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/saved_words_action_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | app/src/main/assets/dict.db
17 | app/release
18 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -keepattributes *Annotation*, InnerClasses
2 | -dontnote kotlinx.serialization.AnnotationsKt
3 |
4 | -keep,includedescriptorclasses class com.kamui.rin.**$$serializer { *; }
5 | -keepclassmembers class com.kamui.rin.* {
6 | *** Companion;
7 | }
8 | -keepclasseswithmembers class com.kamui.rin.* {
9 | kotlinx.serialization.KSerializer serializer(...);
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/ui/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.ui
2 |
3 | import androidx.appcompat.app.AppCompatDelegate
4 |
5 | fun setupTheme(isDarkTheme: Boolean) {
6 | val theme = if (isDarkTheme) {
7 | AppCompatDelegate.MODE_NIGHT_YES
8 | } else {
9 | AppCompatDelegate.MODE_NIGHT_NO
10 | }
11 | AppCompatDelegate.setDefaultNightMode(theme)
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_add_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_keyboard_arrow_right_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/search_action_bar.xml:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/dictionary_manage_actions.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_list_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_bookmark_border_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_save_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/dao/FrequencyDao.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.Query
6 | import com.kamui.rin.db.model.Frequency
7 |
8 | @Dao
9 | interface FrequencyDao {
10 | @Query("DELETE FROM Frequency")
11 | fun clear()
12 |
13 | @Insert
14 | fun insertFrequencies(freq: List)
15 |
16 | @Query("SELECT frequency FROM Frequency WHERE kanji = :kanji")
17 | fun getFrequencyForWord(kanji: String): Long?
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_more_vert_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/dao/PitchAccentDao.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.Query
6 | import com.kamui.rin.db.model.PitchAccent
7 |
8 | @Dao
9 | interface PitchAccentDao {
10 | @Query("DELETE FROM PitchAccent")
11 | fun clear()
12 |
13 | @Insert
14 | fun insertPitchAccents(freq: List)
15 |
16 | @Query("SELECT pitch FROM PitchAccent WHERE kanji = :kanji")
17 | fun getPitchForWord(kanji: String): String?
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_content_copy_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/dao/DictionaryDao.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.dao
2 |
3 | import androidx.room.*
4 | import com.kamui.rin.db.model.Dictionary
5 |
6 | @Dao
7 | interface DictionaryDao {
8 | @Insert
9 | fun insertDictionary(dictionary: Dictionary): Long
10 |
11 | @Query("DELETE FROM Dictionary WHERE dictId = :dictId")
12 | fun deleteDictionary(dictId: Long)
13 |
14 | @Update
15 | fun updateDictionary(dictionary: Dictionary)
16 |
17 | @Query("SELECT * FROM Dictionary")
18 | fun getAllDictionaries(): List
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/dict/worker/IoUtils.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.dict.worker
2 |
3 | import java.io.InputStream
4 | import java.util.zip.ZipInputStream
5 |
6 | fun mapFilenameToBytes(input: InputStream): Map {
7 | return ZipInputStream(input).use { stream ->
8 | generateSequence { stream.nextEntry }
9 | .filterNot { it.isDirectory }
10 | .map { entry ->
11 | val pair = Pair(entry.name, stream.readBytes())
12 | pair
13 | }.toMap()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_bookmark_added_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_search_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/model/Dictionary.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.model
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity
8 | data class Dictionary(
9 | @PrimaryKey(autoGenerate = true) var dictId: Long = 0,
10 | @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT) var name: String,
11 | @ColumnInfo(name = "order", typeAffinity = ColumnInfo.INTEGER) var order: Int = 0,
12 | ) : Comparable {
13 |
14 | override operator fun compareTo(other: Dictionary): Int {
15 | return order.compareTo(other.order)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night-v21/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #121212
4 | #050505
5 |
6 | #74CBF2
7 | @color/colorAccent
8 | #4598BD
9 |
10 | @color/colorSurface
11 |
12 | #ababab
13 | #ababab
14 | #fff
15 |
16 | #fff
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/dao/TagDao.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.Query
6 | import androidx.room.Transaction
7 | import com.kamui.rin.db.model.Tag
8 |
9 | @Dao
10 | interface TagDao {
11 | @Insert
12 | fun insertTags(tags: List): List
13 |
14 | @Query("SELECT * FROM Tag WHERE tagId IN (:ids)")
15 | fun getTagsFromIds(ids: List): List
16 |
17 | @Transaction
18 | fun insertTagsAndRetrieve(tags: List): List {
19 | val insertedIds = insertTags(tags)
20 | return getTagsFromIds(insertedIds)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #fff
4 | #fff
5 |
6 | #26a69a
7 | #00766c
8 |
9 | #64d8cb
10 |
11 | #C8E9E6
12 | #64d8cb
13 | #000
14 |
15 | #26a69a
16 |
17 | #fff
18 |
19 | #000
20 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_navigation.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Rin
3 | Lookup
4 | Search
5 | Settings
6 | Start by looking up a word
7 | Not found! Try refining your search.
8 | Close
9 | RIN_NOTIFICATION_CHANNEL
10 | Cancel
11 | Dictionary Manager
12 | Background Activity
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dict_priority_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/CopyToClipboardReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.ClipData
5 | import android.content.ClipboardManager
6 | import android.content.Context
7 | import android.content.Intent
8 |
9 | class CopyToClipboardReceiver : BroadcastReceiver() {
10 | override fun onReceive(context: Context?, intent: Intent?) {
11 | if (intent?.action == "COPY_TO_CLIPBOARD") {
12 | val textToCopy = intent.getStringExtra("textToCopy")
13 | val clipboard =
14 | context?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
15 | val clip = ClipData.newPlainText("Copied Text", textToCopy)
16 | clipboard.setPrimaryClip(clip)
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/model/Tag.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.ForeignKey
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 |
8 | @Entity(
9 | indices = [
10 | Index(
11 | value = ["dictionaryId"],
12 | name = "idx_dictionary_tag_ref",
13 | unique = false
14 | )
15 | ],
16 | foreignKeys = [ForeignKey(
17 | entity = Dictionary::class,
18 | parentColumns = arrayOf("dictId"),
19 | childColumns = arrayOf("dictionaryId"),
20 | onDelete = ForeignKey.CASCADE
21 | )]
22 | )
23 | data class Tag(
24 | @PrimaryKey(autoGenerate = true) var tagId: Long = 0,
25 | val dictionaryId: Long,
26 | val name: String,
27 | val notes: String,
28 | )
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/dao/SavedWordDao.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.dao
2 |
3 |
4 | import androidx.room.Dao
5 | import androidx.room.Delete
6 | import androidx.room.Insert
7 | import androidx.room.Query
8 | import com.kamui.rin.db.model.SavedWord
9 | import kotlinx.coroutines.flow.Flow
10 |
11 | @Dao
12 | interface SavedWordDao {
13 | @Query("SELECT * FROM SavedWord")
14 | fun getAllSaved(): Flow>
15 |
16 | @Query("SELECT EXISTS(SELECT * FROM SavedWord WHERE kanji = :kanji)")
17 | fun existsWord(kanji: String): Boolean
18 |
19 | @Query("DELETE FROM SavedWord")
20 | fun deleteAllWords()
21 |
22 | @Delete
23 | fun deleteWord(word: SavedWord)
24 |
25 | @Query("DELETE FROM SavedWord WHERE kanji = :kanji")
26 | fun deleteWordByKanji(kanji: String)
27 |
28 | @Insert
29 | fun insertWord(word: SavedWord)
30 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/dict/Japanese.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.dict
2 |
3 | fun toHiragana(c: Char): Char {
4 | if (isFullWidthKatakana(c)) {
5 | return (c.code - 0x60).toChar()
6 | } else if (isHalfWidthKatakana(c)) {
7 | return (c.code - 0xcf25).toChar()
8 | }
9 | return c
10 | }
11 |
12 | fun isHalfWidthKatakana(c: Char): Boolean {
13 | return c in '\uff66'..'\uff9d'
14 | }
15 |
16 | fun isFullWidthKatakana(c: Char): Boolean {
17 | return c in '\u30a1'..'\u30fe'
18 | }
19 |
20 | fun isHiragana(c: Char): Boolean {
21 | return c in '\u3041'..'\u309e'
22 | }
23 |
24 | fun allHiragana(word: String): Boolean {
25 | for (x in word.toCharArray()) {
26 | if (!isHiragana(x)) {
27 | return false
28 | }
29 | }
30 | return true
31 | }
32 |
33 | fun isAllKana(word: String): Boolean {
34 | for (element in word) {
35 | if (!(element in 'ぁ'..'ゞ' || element in 'ァ'..'ヾ')) {
36 | return false
37 | }
38 | }
39 | return true
40 | }
41 |
42 | fun katakanaToHiragana(katakanaWord: String): String {
43 | return katakanaWord.map { c -> toHiragana(c) }.toString()
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/model/DictEntry.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.model
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.ForeignKey
6 | import androidx.room.Index
7 | import androidx.room.PrimaryKey
8 |
9 | @Entity(
10 | foreignKeys = [
11 | ForeignKey(
12 | entity = Dictionary::class,
13 | parentColumns = ["dictId"],
14 | childColumns = ["dictionaryId"],
15 | onDelete = ForeignKey.CASCADE
16 | )
17 | ],
18 | indices = [Index(
19 | value = ["kanji", "reading"],
20 | name = "idx_word_reading",
21 | unique = false
22 | ), Index(
23 | value = ["dictionaryId"],
24 | name = "idx_dictionary_entry_ref",
25 | unique = false
26 | )]
27 | )
28 | data class DictEntry(
29 | @PrimaryKey(autoGenerate = true) var entryId: Long = 0,
30 | var kanji: String,
31 | var meaning: String,
32 | var reading: String,
33 | var dictionaryId: Long,
34 | )
35 |
36 | @Entity(primaryKeys = ["entryId", "tagId"])
37 | data class DictEntryTagCrossRef(
38 | val entryId: Long,
39 | @ColumnInfo(index = true)
40 | val tagId: Long,
41 | )
--------------------------------------------------------------------------------
/app/src/main/res/values-night-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
16 |
17 |
20 |
21 |
26 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_loading_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
17 |
18 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/saved_word.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
29 |
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rin
2 |
3 | Rin is a minimal and lightning fast Japanese popup dictionary app for Android, inspired from Apple's
4 | highlight and lookup [feature](https://www.macrumors.com/how-to/look-up-word-definitions-ios-11/).
5 | The installable APK can be found in the
6 | latest [releases](https://github.com/kamui-fin/rin/releases/latest).
7 |
8 | Features include:
9 |
10 | - Blazing fast lookup times
11 | - Light/Dark mode
12 | - Automatic word de-inflection
13 | - Recursive lookups within definitions
14 | - Importing Yomichan dictionaries
15 | - Frequency and pitch accent dictionaries supported
16 | - Set dictionary priorities for search result ordering
17 | - Save words into a list
18 | - View word tags
19 |
20 | [](https://opensource.org/licenses/)
21 |
22 | ## Screenshots
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## Contributing
32 |
33 | Rin is actively looking for new contributors!
34 | Feel free to submit any feature requests, bug fixes, or enhancements through Github.
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/dict/worker/DeleteDictionaryWorker.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.dict.worker
2 |
3 | import android.content.Context
4 | import androidx.work.WorkerParameters
5 | import com.kamui.rin.db.AppDatabase
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.withContext
8 |
9 | class DeleteDictionaryWorker(context: Context, parameters: WorkerParameters) :
10 | BaseDictionaryWorker(context, parameters) {
11 | override suspend fun doWork(): Result {
12 | return withContext(Dispatchers.IO) {
13 | return@withContext try {
14 | val dictId = inputData.getLong("DICT_ID", 0) // FIXME: find out optional default
15 | val progress = "Deleting dictionary"
16 | setForeground(createForegroundInfo(progress))
17 | deleteDictionary(dictId)
18 | return@withContext Result.success()
19 | } catch (throwable: Throwable) {
20 | onException(throwable)
21 | throwable.printStackTrace()
22 | Result.failure()
23 | }
24 | }
25 | }
26 |
27 | private fun deleteDictionary(dictId: Long) {
28 | AppDatabase.buildDatabase(applicationContext).dictionaryDao().deleteDictionary(dictId)
29 | }
30 |
31 | override fun getNotificationId(): Int {
32 | return 0; }
33 | }
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/Settings.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin
2 |
3 | import android.content.SharedPreferences
4 | import android.net.Uri
5 |
6 | class Settings(private val sharedPreferences: SharedPreferences) {
7 | fun disabledDicts(): List {
8 | return sharedPreferences.getStringSet("disabledDicts", setOf())!!.toList()
9 | .map { it.toLong() }
10 | }
11 |
12 | fun disabledDictSet(): Set {
13 | return sharedPreferences.getStringSet("disabledDicts", setOf())!!
14 | }
15 |
16 | fun updateDisabledDicts(newSet: Set) {
17 | sharedPreferences.edit().putStringSet("disabledDicts", newSet).apply()
18 | }
19 |
20 | fun isDictActive(id: Long): Boolean {
21 | return !sharedPreferences.getStringSet("disabledDicts", setOf())!!.contains(id.toString())
22 | }
23 |
24 | fun shouldDeconjugate(): Boolean {
25 | return sharedPreferences.getBoolean("shouldDeconjugate", true)
26 | }
27 |
28 | fun darkTheme(): Boolean {
29 | return sharedPreferences.getBoolean("darkTheme", false)
30 | }
31 |
32 | fun savedWordsPath(): Uri? {
33 | val uri = sharedPreferences.getString("savedWordsPath", null)
34 | return if (uri != null) {
35 | Uri.parse(uri)
36 | } else {
37 | null
38 | }
39 | }
40 |
41 | fun setSavedWordsPath(path: String) {
42 | sharedPreferences.edit().putString("savedWordsPath", path).apply()
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_menu_book_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
13 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_saved_words.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
17 |
18 |
19 |
20 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_manage_dicts.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
27 |
28 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_settings_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
13 |
17 |
21 |
22 |
23 |
26 |
31 |
32 |
33 |
36 |
41 |
42 |
43 |
44 |
47 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/dao/DictEntryDao.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db.dao
2 |
3 | import androidx.room.*
4 | import com.kamui.rin.db.model.DictEntry
5 | import com.kamui.rin.db.model.DictEntryTagCrossRef
6 | import com.kamui.rin.db.model.Dictionary
7 | import com.kamui.rin.db.model.Tag
8 |
9 | @Dao
10 | interface DictEntryDao {
11 | @Transaction
12 | @Query("SELECT * FROM DictEntry WHERE entryId = :id")
13 | fun searchEntryById(
14 | id: Long
15 | ): DictEntry
16 |
17 | @Transaction
18 | @Query(
19 | "SELECT * FROM DictEntry " +
20 | "JOIN Dictionary ON Dictionary.dictId = DictEntry.dictionaryId " +
21 | "WHERE kanji = :query AND Dictionary.dictId NOT IN (:disabled)"
22 | )
23 | fun searchEntryByKanji(
24 | query: String,
25 | disabled: List
26 | ): Map
27 |
28 | @Transaction
29 | @Query(
30 | "SELECT * FROM DictEntry " +
31 | "JOIN Dictionary ON Dictionary.dictId = DictEntry.dictionaryId " +
32 | "WHERE reading = :query AND Dictionary.dictId NOT IN (:disabled)"
33 | )
34 | fun searchEntryByReading(
35 | query: String,
36 | disabled: List
37 | ): Map
38 |
39 | @Query(
40 | "SELECT Tag.* from DictEntryTagCrossRef " +
41 | "JOIN Tag ON Tag.tagId = DictEntryTagCrossRef.tagId " +
42 | "WHERE DictEntryTagCrossRef.entryId = :entryId"
43 | )
44 | fun getTagsForEntry(entryId: Long): List
45 |
46 | @Insert
47 | fun insertEntry(entry: DictEntry): Long
48 |
49 | @Insert
50 | fun insertTagForEntry(entryTagCrossRef: DictEntryTagCrossRef)
51 |
52 | @Transaction
53 | fun insertEntriesWithTags(entryTags: List>>) {
54 | for ((entry, tags) in entryTags) {
55 | val entryId = insertEntry(entry)
56 | for (tag in tags) {
57 | val tagId = tag.tagId
58 | insertTagForEntry(DictEntryTagCrossRef(entryId, tagId))
59 | }
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/dictionary.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
27 |
28 |
40 |
41 |
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/dict/worker/ImportFrequencyWorker.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.dict.worker
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import androidx.work.WorkerParameters
6 | import com.kamui.rin.db.AppDatabase
7 | import com.kamui.rin.dict.decodeFrequencyEntries
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.withContext
10 | import java.io.FileNotFoundException
11 |
12 | class ImportFrequencyWorker(context: Context, parameters: WorkerParameters) :
13 | BaseDictionaryWorker(context, parameters) {
14 | override suspend fun doWork(): Result {
15 | return withContext(Dispatchers.IO) {
16 | val uri = Uri.parse(inputData.getString("URI"))
17 | val progress = "Importing frequency list"
18 | setForeground(createForegroundInfo(progress))
19 | return@withContext try {
20 | importFrequencyList(uri)
21 | Result.success()
22 | } catch (throwable: Throwable) {
23 | onException(throwable)
24 | throwable.printStackTrace()
25 | Result.failure()
26 | }
27 | }
28 | }
29 |
30 | private fun importFrequencyList(uri: Uri) {
31 | return applicationContext.contentResolver.openInputStream(uri).use f@{ input ->
32 | if (input == null) {
33 | throw FileNotFoundException("could not open frequency list")
34 | }
35 | updateNotification("Scanning frequency list files")
36 | val zipMap = mapFilenameToBytes(input)
37 | updateNotification("Importing frequency data")
38 | val frequencyList = zipMap.filter { (key, _) ->
39 | key.startsWith("term_meta_bank_") && key.endsWith(
40 | ".json"
41 | )
42 | }
43 | .map { (_, termBank) -> decodeFrequencyEntries(termBank.decodeToString()) }
44 | .flatten()
45 | updateNotification("Inserting into database")
46 | val dao = AppDatabase.buildDatabase(applicationContext).frequencyDao()
47 | dao.clear()
48 | dao.insertFrequencies(frequencyList)
49 | }
50 | }
51 |
52 | override fun getNotificationId(): Int {
53 | return 2; }
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/dict/Lookup.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.dict
2 |
3 | import android.content.Context
4 | import com.kamui.rin.Settings
5 | import com.kamui.rin.db.AppDatabase
6 | import com.kamui.rin.db.dao.DictEntryDao
7 | import com.kamui.rin.db.model.DictEntry
8 | import com.kamui.rin.db.model.Dictionary
9 |
10 | class Lookup(
11 | context: Context,
12 | deinflectionText: String,
13 | private val settings: Settings
14 | ) {
15 | private val db: AppDatabase = AppDatabase.buildDatabase(context)
16 | private val dao: DictEntryDao = db.dictEntryDao()
17 | private var deinflector: Deinflector = Deinflector(deinflectionText)
18 |
19 | fun lookup(query: String): List> {
20 | val possibleVariations = normalizeWord(query).toMutableList()
21 | val entries: MutableList> = ArrayList()
22 |
23 | if (possibleVariations.isEmpty()) {
24 | possibleVariations.add(query)
25 | }
26 |
27 | for (variation in possibleVariations) {
28 | var results: Map
29 | if (isAllKana(variation)) {
30 | val convertedToHiragana: String = if (!allHiragana(variation)) {
31 | katakanaToHiragana(variation)
32 | } else {
33 | variation
34 | }
35 | results = dao.searchEntryByReading(convertedToHiragana, settings.disabledDicts())
36 | if (results.isEmpty()) {
37 | results = dao.searchEntryByKanji(variation, settings.disabledDicts())
38 | }
39 | } else {
40 | results = dao.searchEntryByKanji(variation, settings.disabledDicts())
41 | }
42 | entries.addAll(results.toList())
43 | }
44 |
45 | entries.sortWith(compareBy { it.second })
46 | return entries
47 | }
48 |
49 | private fun normalizeWord(word: String): List {
50 | return if (settings.shouldDeconjugate()) {
51 | deconjugateWord(word.trim { it <= ' ' })
52 | } else {
53 | mutableListOf(word)
54 | }
55 | }
56 |
57 | private fun deconjugateWord(word: String): List {
58 | return deinflector.deinflect(word).map { d -> d.term }
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/dict/YomichanDecoder.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.dict
2 |
3 | import com.kamui.rin.db.model.Frequency
4 | import com.kamui.rin.db.model.Tag
5 | import kotlinx.serialization.json.Json
6 | import kotlinx.serialization.json.JsonArray
7 | import kotlinx.serialization.json.intOrNull
8 | import kotlinx.serialization.json.jsonArray
9 | import kotlinx.serialization.json.jsonPrimitive
10 | import kotlinx.serialization.json.long
11 |
12 | val format = Json { ignoreUnknownKeys = true }
13 |
14 | @kotlinx.serialization.Serializable
15 | data class YomichanMeta(
16 | val title: String
17 | )
18 |
19 | @kotlinx.serialization.Serializable
20 | data class YomichanDictionaryEntry(
21 | val expression: String,
22 | val reading: String,
23 | val definitionTags: String,
24 | val ruleIdentifiers: String,
25 | val popularity: Int,
26 | val meanings: List,
27 | val sequence: Int,
28 | val termTags: List,
29 | )
30 |
31 | fun decodeDictionaryEntries(stringData: String): List {
32 | val root: JsonArray = format.parseToJsonElement(stringData).jsonArray
33 | return root.map {
34 | YomichanDictionaryEntry(
35 | it.jsonArray[0].jsonPrimitive.content,
36 | it.jsonArray[1].jsonPrimitive.content,
37 | it.jsonArray[2].jsonPrimitive.content,
38 | it.jsonArray[3].jsonPrimitive.content,
39 | it.jsonArray[4].jsonPrimitive.intOrNull!!,
40 | it.jsonArray[5].jsonArray.toList().map { meaning -> meaning.jsonPrimitive.content },
41 | it.jsonArray[6].jsonPrimitive.intOrNull!!,
42 | it.jsonArray[7].jsonPrimitive.content.split(" "),
43 | )
44 | }
45 | }
46 |
47 | fun decodeFrequencyEntries(stringData: String): List {
48 | val root: JsonArray = format.parseToJsonElement(stringData).jsonArray
49 | return root.map {
50 | Frequency(
51 | kanji = it.jsonArray[0].jsonPrimitive.content,
52 | frequency = it.jsonArray[2].jsonPrimitive.long,
53 | )
54 | }
55 | }
56 |
57 |
58 | fun decodeTags(stringData: String, dictId: Long): List {
59 | val root: JsonArray = format.parseToJsonElement(stringData).jsonArray
60 | return root.map {
61 | Tag(
62 | dictionaryId = dictId,
63 | name = it.jsonArray[0].jsonPrimitive.content,
64 | notes = it.jsonArray[3].jsonPrimitive.content,
65 | )
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
14 |
15 |
16 |
19 |
20 |
25 |
28 |
33 |
34 |
35 |
36 |
40 |
43 |
46 |
47 |
48 |
53 |
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/adapter/DictEntryAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.adapter
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.view.animation.AnimationUtils
8 | import androidx.navigation.findNavController
9 | import androidx.recyclerview.widget.RecyclerView
10 | import com.kamui.rin.databinding.LookupCardBinding
11 | import com.kamui.rin.db.model.DictEntry
12 | import com.kamui.rin.db.model.Dictionary
13 | import com.kamui.rin.ui.fragment.LookupFragmentDirections
14 |
15 | class DictEntryAdapter(
16 | private val context: Context,
17 | private val entries: List>
18 | ) :
19 | RecyclerView.Adapter() {
20 | private var lastPosition = -1
21 |
22 | override fun getItemCount(): Int {
23 | return entries.size
24 | }
25 |
26 | private fun trimDefinition(meaning: String): String {
27 | if (meaning.length >= 100) {
28 | return meaning.substring(0, 100) + "..."
29 | }
30 | return meaning
31 | }
32 |
33 | private fun setAnimation(viewToAnimate: View, position: Int) {
34 | if (position > lastPosition) {
35 | val animation = AnimationUtils.loadAnimation(context, android.R.anim.fade_in)
36 | viewToAnimate.startAnimation(animation)
37 | lastPosition = position
38 | }
39 | }
40 |
41 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
42 | val binding = LookupCardBinding.inflate(LayoutInflater.from(parent.context), parent, false)
43 | return ViewHolder(binding)
44 | }
45 |
46 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
47 | val entry = entries[position].first
48 | val dictionary = entries[position].second
49 |
50 | holder.binding.wordTextView.text = entry.kanji
51 | holder.binding.secondaryTextCard.text = entry.reading
52 | holder.binding.meaningTextView.text = trimDefinition(entry.meaning)
53 | holder.binding.dictName.text = dictionary.name
54 | holder.binding.card.setOnClickListener {
55 | val action = LookupFragmentDirections.getDetails(entry.entryId, entry.kanji)
56 | it.findNavController().navigate(action)
57 | }
58 | setAnimation(holder.itemView, position)
59 | }
60 |
61 | inner class ViewHolder(val binding: LookupCardBinding) : RecyclerView.ViewHolder(binding.root)
62 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
28 |
29 |
40 |
41 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/dict/worker/ImportPitchWorker.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.dict.worker
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import androidx.work.WorkerParameters
6 | import com.kamui.rin.db.AppDatabase
7 | import com.kamui.rin.db.model.PitchAccent
8 | import com.kamui.rin.dict.decodeDictionaryEntries
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.withContext
11 | import java.io.FileNotFoundException
12 |
13 | class ImportPitchWorker(context: Context, parameters: WorkerParameters) :
14 | BaseDictionaryWorker(context, parameters) {
15 | override suspend fun doWork(): Result {
16 | return withContext(Dispatchers.IO) {
17 | val uri = Uri.parse(inputData.getString("URI"))
18 | val progress = "Importing pitch accent data"
19 | setForeground(createForegroundInfo(progress))
20 | return@withContext try {
21 | importPitchAccents(uri)
22 | Result.success()
23 | } catch (throwable: Throwable) {
24 | onException(throwable)
25 | throwable.printStackTrace()
26 | Result.failure()
27 | }
28 | }
29 | }
30 |
31 | private fun importPitchAccents(uri: Uri) {
32 | return applicationContext.contentResolver.openInputStream(uri).use f@{ input ->
33 | updateNotification("Scanning pitch accent files")
34 | if (input == null) {
35 | throw FileNotFoundException("could not open pitch accent dictionary")
36 | }
37 |
38 | val zipMap = mapFilenameToBytes(input)
39 | updateNotification("Importing pitch accent data")
40 | val pitchEntries =
41 | zipMap.filter { (key, _) -> key.startsWith("term_bank_") && key.endsWith(".json") }
42 | .map { (_, termBank) -> decodeDictionaryEntries(termBank.decodeToString()) }
43 | .flatten()
44 | .map { entry ->
45 | PitchAccent(
46 | kanji = entry.expression,
47 | pitch = entry.meanings.joinToString("\n") {
48 | it.trim { it <= ' ' }
49 | }
50 | )
51 | }
52 |
53 | if (pitchEntries.isEmpty()) {
54 | throw Exception("Error: Invalid dictionary format. Rin currently only supports the old pitch accent dictionary format.")
55 | } else {
56 | updateNotification("Inserting into database")
57 | val dao = AppDatabase.buildDatabase(applicationContext).pitchAccentDao()
58 | dao.clear()
59 | dao.insertPitchAccents(pitchEntries)
60 | }
61 | }
62 | }
63 |
64 | override fun getNotificationId(): Int {
65 | return 2; }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/db/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.db
2 |
3 | import android.content.Context
4 | import androidx.room.AutoMigration
5 | import androidx.room.Database
6 | import androidx.room.DeleteColumn
7 | import androidx.room.Room
8 | import androidx.room.RoomDatabase
9 | import androidx.room.migration.AutoMigrationSpec
10 | import androidx.room.migration.Migration
11 | import androidx.sqlite.db.SupportSQLiteDatabase
12 | import com.kamui.rin.db.dao.DictEntryDao
13 | import com.kamui.rin.db.dao.DictionaryDao
14 | import com.kamui.rin.db.dao.FrequencyDao
15 | import com.kamui.rin.db.dao.PitchAccentDao
16 | import com.kamui.rin.db.dao.SavedWordDao
17 | import com.kamui.rin.db.dao.TagDao
18 | import com.kamui.rin.db.model.DictEntry
19 | import com.kamui.rin.db.model.DictEntryTagCrossRef
20 | import com.kamui.rin.db.model.Dictionary
21 | import com.kamui.rin.db.model.Frequency
22 | import com.kamui.rin.db.model.PitchAccent
23 | import com.kamui.rin.db.model.SavedWord
24 | import com.kamui.rin.db.model.Tag
25 |
26 | @DeleteColumn(tableName = "DictEntry", columnName = "pitchAccent")
27 | @DeleteColumn(tableName = "DictEntry", columnName = "freq")
28 | class DeleteFreqPitchColumn : AutoMigrationSpec
29 |
30 | val MIGRATION_3_4 = object : Migration(3, 4) {
31 | override fun migrate(database: SupportSQLiteDatabase) {
32 | database.execSQL(
33 | """
34 | CREATE TABLE PitchAccent(
35 | pitchId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
36 | kanji TEXT NOT NULL,
37 | pitch TEXT NOT NULL
38 | )
39 | """.trimIndent()
40 | )
41 | }
42 | }
43 |
44 | @Database(
45 | version = 5,
46 | entities = [PitchAccent::class, Frequency::class, DictEntry::class, DictEntryTagCrossRef::class, SavedWord::class, Tag::class, Dictionary::class],
47 | exportSchema = true,
48 | autoMigrations = [
49 | AutoMigration(
50 | from = 2,
51 | to = 3
52 | ),
53 | AutoMigration(
54 | from = 4,
55 | to = 5,
56 | DeleteFreqPitchColumn::class
57 | ),
58 | ]
59 | )
60 | abstract class AppDatabase : RoomDatabase() {
61 | abstract fun dictEntryDao(): DictEntryDao
62 | abstract fun savedDao(): SavedWordDao
63 | abstract fun tagDao(): TagDao
64 | abstract fun dictionaryDao(): DictionaryDao
65 | abstract fun frequencyDao(): FrequencyDao
66 | abstract fun pitchAccentDao(): PitchAccentDao
67 |
68 | companion object {
69 | @Volatile
70 | private lateinit var INSTANCE: AppDatabase
71 |
72 | fun buildDatabase(context: Context): AppDatabase {
73 | if (!this::INSTANCE.isInitialized) {
74 | INSTANCE = Room.databaseBuilder(
75 | context.applicationContext,
76 | AppDatabase::class.java,
77 | "dict.db"
78 | )
79 | .addMigrations(MIGRATION_3_4)
80 | .build()
81 | }
82 | return INSTANCE
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_lookup.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
20 |
21 |
30 |
31 |
41 |
42 |
51 |
52 |
61 |
62 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/dict/Deinflector.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.dict
2 |
3 | import kotlinx.serialization.Serializable
4 | import kotlinx.serialization.decodeFromString
5 | import kotlinx.serialization.json.Json
6 |
7 | @Serializable
8 | data class ReasonInfo(
9 | val kanaIn: String,
10 | val kanaOut: String,
11 | val rulesIn: T,
12 | val rulesOut: T
13 | )
14 |
15 | @Serializable
16 | data class ReasonEntry(
17 | val reason: String,
18 | val information: List>
19 | )
20 |
21 | data class Deinflection(
22 | val term: String,
23 | val rules: Int,
24 | val reasons: List>
25 | )
26 |
27 | class Deinflector(private val deinflectionText: String) {
28 | private val reasons = normalizeReasons()
29 |
30 | fun deinflect(source: String): MutableList {
31 | val results = mutableListOf(Deinflection(source, 0, listOf()))
32 | for (result in results) {
33 | for (reason in this.reasons) {
34 | for (variant in reason.information) {
35 | if (
36 | (result.rules != 0 && (result.rules and variant.rulesIn) == 0) ||
37 | !result.term.endsWith(variant.kanaIn) ||
38 | (result.term.length - variant.kanaIn.length + variant.kanaOut.length) <= 0
39 | ) {
40 | continue
41 | }
42 |
43 | results.add(
44 | Deinflection(
45 | result.term.substring(
46 | 0,
47 | result.term.length - variant.kanaIn.length
48 | ) + variant.kanaOut, variant.rulesOut, result.reasons + reason
49 | )
50 | )
51 | }
52 | }
53 | }
54 | return results
55 | }
56 |
57 | private fun normalizeReasons(): List> {
58 | val entries = Json.decodeFromString>>>(deinflectionText)
59 | val normalized: MutableList> = ArrayList()
60 | for (reason in entries) {
61 | val variants: List> = reason.information.map { i ->
62 | ReasonInfo(
63 | i.kanaIn,
64 | i.kanaOut,
65 | rulesToRuleFlags(i.rulesIn),
66 | rulesToRuleFlags(i.rulesOut)
67 | )
68 | }
69 | normalized.add(ReasonEntry(reason.reason, variants))
70 | }
71 | return normalized
72 | }
73 |
74 | private fun rulesToRuleFlags(rules: List): Int {
75 | val ruleTypes = mapOf(
76 | "v1" to 0b00000001,
77 | "v5" to 0b00000010,
78 | "vs" to 0b00000100,
79 | "vk" to 0b00001000,
80 | "adj-i" to 0b00010000,
81 | "iru" to 0b00100000
82 | )
83 | var value = 0
84 | for (rule in rules) {
85 | val bits = ruleTypes[rule] ?: continue
86 | value = value or bits
87 | }
88 | return value
89 | }
90 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | id 'kotlin-parcelize'
6 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.5.0'
7 | id 'androidx.navigation.safeargs.kotlin'
8 | }
9 |
10 | android {
11 | compileSdkVersion 33
12 | buildToolsVersion "30.0.3"
13 |
14 | defaultConfig {
15 | applicationId "com.kamui.rin"
16 | minSdkVersion 30
17 | targetSdkVersion 33
18 | versionCode 1
19 | versionName "1.0"
20 | vectorDrawables {
21 | useSupportLibrary true
22 | }
23 | javaCompileOptions {
24 | annotationProcessorOptions {
25 | arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
26 | }
27 | }
28 | }
29 |
30 | buildTypes {
31 | release {
32 | minifyEnabled false
33 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
34 | }
35 | }
36 |
37 | buildFeatures {
38 | viewBinding true
39 | }
40 |
41 | compileOptions {
42 | sourceCompatibility JavaVersion.VERSION_1_8
43 | targetCompatibility JavaVersion.VERSION_1_8
44 | }
45 |
46 | kotlinOptions {
47 | jvmTarget = '1.8'
48 | }
49 | }
50 |
51 | dependencies {
52 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1"
53 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.7.20"
54 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
55 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
56 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
57 | implementation 'androidx.core:core-ktx:1.9.0'
58 | implementation 'androidx.appcompat:appcompat:1.6.1'
59 | implementation 'com.google.android.material:material:1.8.0'
60 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
61 | implementation 'androidx.preference:preference-ktx:1.2.0'
62 | implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
63 | implementation "androidx.room:room-runtime:2.5.0"
64 | implementation 'androidx.room:room-ktx:2.5.0'
65 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
66 | implementation 'androidx.recyclerview:recyclerview:1.2.1'
67 | kapt "androidx.room:room-compiler:2.5.0"
68 |
69 | def lifecycle_version = "2.5.1"
70 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
71 | implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
72 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
73 | kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
74 |
75 | def nav_version = "2.5.3"
76 | implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
77 | implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
78 | implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
79 |
80 | def work_version = "2.8.1"
81 | implementation "androidx.work:work-runtime:$work_version"
82 | implementation "androidx.work:work-runtime-ktx:$work_version"
83 |
84 | implementation "com.google.guava:guava:31.0.1-android"
85 | implementation "androidx.concurrent:concurrent-futures:1.1.0"
86 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/dict/worker/BaseDictionaryWorker.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.dict.worker
2 |
3 | import android.app.Notification
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.app.PendingIntent
7 | import android.content.Context
8 | import android.content.Intent
9 | import androidx.core.app.NotificationCompat
10 | import androidx.work.CoroutineWorker
11 | import androidx.work.ForegroundInfo
12 | import androidx.work.WorkerParameters
13 | import com.kamui.rin.CopyToClipboardReceiver
14 | import com.kamui.rin.R
15 |
16 | abstract class BaseDictionaryWorker(context: Context, parameters: WorkerParameters) :
17 | CoroutineWorker(context, parameters) {
18 | private val notificationManager =
19 | context.getSystemService(Context.NOTIFICATION_SERVICE) as
20 | NotificationManager
21 |
22 | abstract fun getNotificationId(): Int
23 |
24 | fun onException(error: Throwable) {
25 | val copyIntent = Intent(applicationContext, CopyToClipboardReceiver::class.java).apply {
26 | action = "COPY_TO_CLIPBOARD"
27 | putExtra("textToCopy", error.localizedMessage)
28 | }
29 | val copyPendingIntent = PendingIntent.getBroadcast(
30 | applicationContext,
31 | 0,
32 | copyIntent,
33 | PendingIntent.FLAG_UPDATE_CURRENT
34 | )
35 |
36 |
37 | val id = applicationContext.getString(R.string.notification_channel_id)
38 | val title = "Application Error"
39 |
40 | val notification = NotificationCompat.Builder(applicationContext, id)
41 | .setOnlyAlertOnce(true)
42 | .setTicker(title)
43 | .setContentText(error.localizedMessage)
44 | .setStyle(NotificationCompat.BigTextStyle().bigText(error.localizedMessage))
45 | .addAction(R.drawable.baseline_content_copy_24, "Copy Error", copyPendingIntent)
46 | .setSmallIcon(R.drawable.baseline_menu_book_24)
47 | .build()
48 |
49 | notificationManager.notify(400, notification)
50 | }
51 |
52 | private fun getNotification(text: String): Notification {
53 | val id = applicationContext.getString(R.string.notification_channel_id)
54 | val title = applicationContext.getString(R.string.notification_title)
55 | return NotificationCompat.Builder(applicationContext, id)
56 | .setOnlyAlertOnce(true)
57 | .setContentTitle(title)
58 | .setTicker(title)
59 | .setContentText(text)
60 | .setSmallIcon(R.drawable.baseline_menu_book_24)
61 | .setProgress(0, 0, true)
62 | .setOngoing(true)
63 | .build()
64 | }
65 |
66 | fun createForegroundInfo(progress: String): ForegroundInfo {
67 | createChannel()
68 | return ForegroundInfo(getNotificationId(), getNotification(progress))
69 | }
70 |
71 | fun updateNotification(status: String) {
72 | notificationManager.notify(getNotificationId(), getNotification(status))
73 | }
74 |
75 | private fun createChannel() {
76 | val channelId = applicationContext.getString(R.string.notification_channel_id)
77 | val descriptionText = applicationContext.getString(R.string.channel_description)
78 | val importance = NotificationManager.IMPORTANCE_DEFAULT
79 | val channel = NotificationChannel(channelId, "Rin notification channel", importance).apply {
80 | description = descriptionText
81 | }
82 | notificationManager.createNotificationChannel(channel)
83 | }
84 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin
2 |
3 | import android.app.AlertDialog
4 | import android.content.pm.PackageManager
5 | import android.os.Build
6 | import android.os.Bundle
7 | import android.view.MenuItem
8 | import androidx.activity.result.contract.ActivityResultContracts
9 | import androidx.annotation.RequiresApi
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.core.content.ContextCompat
12 | import androidx.navigation.NavController
13 | import androidx.navigation.findNavController
14 | import androidx.navigation.fragment.NavHostFragment
15 | import androidx.navigation.ui.*
16 | import androidx.preference.PreferenceManager
17 | import com.kamui.rin.databinding.ActivityMainBinding
18 | import com.kamui.rin.ui.setupTheme
19 |
20 | class MainActivity : AppCompatActivity() {
21 | private lateinit var appBarConfiguration: AppBarConfiguration
22 | private lateinit var binding: ActivityMainBinding
23 |
24 | private val requestPermissionLauncher =
25 | registerForActivityResult(
26 | ActivityResultContracts.RequestPermission()
27 | ) { isGranted: Boolean ->
28 | if (!isGranted) {
29 | val builder = AlertDialog.Builder(applicationContext)
30 | builder.setTitle("Rin Notifications")
31 | builder.setMessage("Rin optionally uses notifications to display dictionary management progress")
32 | builder.setPositiveButton("OK") { dialog, _ ->
33 | dialog.dismiss()
34 | }
35 | val alertDialog: AlertDialog = builder.create()
36 | alertDialog.show()
37 | }
38 | }
39 |
40 |
41 | override fun onCreate(savedInstanceState: Bundle?) {
42 | super.onCreate(savedInstanceState)
43 | binding = ActivityMainBinding.inflate(layoutInflater)
44 | val view = binding.root
45 | setContentView(view)
46 |
47 | val navHostFragment =
48 | supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
49 | val navController = navHostFragment.navController
50 | binding.bottomNavigation.setupWithNavController(navController)
51 |
52 | setupTheme(Settings(PreferenceManager.getDefaultSharedPreferences(applicationContext)).darkTheme())
53 |
54 | appBarConfiguration = AppBarConfiguration(navController.graph)
55 | configureActionBar(navController)
56 |
57 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
58 | getNotificationPermission()
59 | }
60 | }
61 |
62 | private fun configureActionBar(navController: NavController) {
63 | binding.searchToolbar.bringToFront()
64 | setSupportActionBar(binding.searchToolbar)
65 | setupActionBarWithNavController(navController, appBarConfiguration)
66 | }
67 |
68 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
69 | val navController = findNavController(R.id.nav_host_fragment)
70 | return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
71 | }
72 |
73 | override fun onSupportNavigateUp(): Boolean {
74 | val navController = findNavController(R.id.nav_host_fragment)
75 | if (!navController.navigateUp(appBarConfiguration)) {
76 | finish()
77 | }
78 | return true
79 | }
80 |
81 | @RequiresApi(Build.VERSION_CODES.TIRAMISU)
82 | private fun getNotificationPermission() {
83 | if (ContextCompat.checkSelfPermission(
84 | this, android.Manifest.permission.POST_NOTIFICATIONS
85 | ) != PackageManager.PERMISSION_GRANTED
86 | ) {
87 | shouldShowRequestPermissionRationale(android.Manifest.permission.POST_NOTIFICATIONS)
88 | requestPermissionLauncher.launch(
89 | android.Manifest.permission.POST_NOTIFICATIONS
90 | )
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/dict/worker/ImportDictionaryWorker.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.dict.worker
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.util.Log
6 | import androidx.work.WorkerParameters
7 | import androidx.work.workDataOf
8 | import com.kamui.rin.db.AppDatabase
9 | import com.kamui.rin.db.model.DictEntry
10 | import com.kamui.rin.db.model.Dictionary
11 | import com.kamui.rin.dict.YomichanMeta
12 | import com.kamui.rin.dict.decodeDictionaryEntries
13 | import com.kamui.rin.dict.decodeTags
14 | import com.kamui.rin.dict.format
15 | import kotlinx.coroutines.Dispatchers
16 | import kotlinx.coroutines.withContext
17 | import kotlinx.serialization.decodeFromString
18 | import java.io.FileNotFoundException
19 |
20 | class ImportDictionaryWorker(context: Context, parameters: WorkerParameters) :
21 | BaseDictionaryWorker(context, parameters) {
22 | override suspend fun doWork(): Result {
23 | return withContext(Dispatchers.IO) {
24 | val uri = Uri.parse(inputData.getString("URI"))
25 | val progress = "Importing dictionary"
26 | setForeground(createForegroundInfo(progress))
27 | return@withContext try {
28 | val dict = importDictionary(uri)
29 | val data = workDataOf("DICT_ID" to dict.dictId, "DICT_TITLE" to dict.name)
30 | Result.success(data)
31 | } catch (throwable: Throwable) {
32 | onException(throwable)
33 | throwable.printStackTrace()
34 | Result.failure()
35 | }
36 | }
37 | }
38 |
39 | private fun importDictionary(uri: Uri): Dictionary {
40 | return applicationContext.contentResolver.openInputStream(uri).use f@{ input ->
41 | updateNotification("Scanning dictionary files")
42 | if (input == null) {
43 | throw FileNotFoundException("could not open dictionary")
44 | }
45 |
46 | val zipMap = mapFilenameToBytes(input)
47 | if (!zipMap.containsKey("index.json")) {
48 | throw FileNotFoundException("index.json could not be found")
49 | }
50 | val index =
51 | format.decodeFromString(zipMap["index.json"]!!.decodeToString())
52 | updateNotification("Creating dictionary ${index.title}")
53 | val dictId = AppDatabase.buildDatabase(applicationContext).dictionaryDao()
54 | .insertDictionary(Dictionary(name = index.title))
55 | Log.d("RIN", dictId.toString())
56 |
57 | updateNotification("Importing tags from ${index.title}")
58 | val tags =
59 | zipMap.filter { (key, _) -> key.startsWith("tag_bank") && key.endsWith(".json") }
60 | .map { (_, value) ->
61 | decodeTags(value.decodeToString(), dictId)
62 | }.flatten()
63 | val tagMap =
64 | AppDatabase.buildDatabase(applicationContext).tagDao().insertTagsAndRetrieve(tags)
65 | .associateBy { it.name }
66 |
67 | updateNotification("Importing entries from ${index.title}")
68 | val termEntries =
69 | zipMap.filter { (key, _) -> key.startsWith("term_bank_") && key.endsWith(".json") }
70 | .map { (_, termBank) ->
71 | decodeDictionaryEntries(termBank.decodeToString())
72 | }.flatten()
73 |
74 | val dbEntries = termEntries.map { entry ->
75 | entry.meanings.map { meaning ->
76 | meaning.replace("\n", "\n\n").trim { it <= ' ' }
77 | }.map { meaning ->
78 | Pair(DictEntry(
79 | kanji = entry.expression,
80 | meaning = meaning,
81 | reading = entry.reading,
82 | dictionaryId = dictId,
83 | ), entry.termTags.mapNotNull { tagMap[it] })
84 | }
85 | }.flatten()
86 |
87 | updateNotification("Inserting into database")
88 | AppDatabase.buildDatabase(applicationContext).dictEntryDao()
89 | .insertEntriesWithTags(dbEntries)
90 |
91 | return@f Dictionary(dictId, index.title)
92 | }
93 | }
94 |
95 | override fun getNotificationId(): Int {
96 | return 1; }
97 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/lookup_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
25 |
26 |
31 |
32 |
33 |
37 |
38 |
41 |
42 |
51 |
52 |
65 |
66 |
75 |
76 |
77 |
78 |
79 |
80 |
87 |
88 |
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/ui/fragment/SettingsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.ui.fragment
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import androidx.activity.result.ActivityResultLauncher
8 | import androidx.activity.result.contract.ActivityResultContracts
9 | import androidx.appcompat.app.AlertDialog
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.fragment.app.viewModels
12 | import androidx.lifecycle.LifecycleOwner
13 | import androidx.lifecycle.ViewModel
14 | import androidx.lifecycle.ViewModelProvider
15 | import androidx.navigation.Navigation
16 | import androidx.preference.Preference
17 | import androidx.preference.PreferenceFragmentCompat
18 | import androidx.preference.PreferenceManager
19 | import androidx.preference.SwitchPreference
20 | import androidx.work.OneTimeWorkRequestBuilder
21 | import androidx.work.WorkManager
22 | import androidx.work.WorkRequest
23 | import androidx.work.workDataOf
24 | import com.kamui.rin.R
25 | import com.kamui.rin.Settings
26 | import com.kamui.rin.dict.worker.ImportFrequencyWorker
27 | import com.kamui.rin.dict.worker.ImportPitchWorker
28 | import com.kamui.rin.ui.setupTheme
29 |
30 | class SettingsViewModel(private val context: Context) : ViewModel() {
31 | fun importFrequencyList(uri: Uri, lifecycleOwner: LifecycleOwner) {
32 | val importWork: WorkRequest =
33 | OneTimeWorkRequestBuilder().setInputData(
34 | workDataOf(
35 | "URI" to uri.toString()
36 | )
37 | ).build()
38 | WorkManager.getInstance(context).enqueue(importWork)
39 | }
40 |
41 | fun importPitchAccents(uri: Uri, lifecycleOwner: LifecycleOwner) {
42 | val importWork: WorkRequest =
43 | OneTimeWorkRequestBuilder().setInputData(
44 | workDataOf(
45 | "URI" to uri.toString()
46 | )
47 | ).build()
48 | WorkManager.getInstance(context).enqueue(importWork)
49 | }
50 | }
51 |
52 | class SettingsViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
53 | override fun create(modelClass: Class): T {
54 | return SettingsViewModel(context) as T
55 | }
56 | }
57 |
58 | class SettingsFragment : PreferenceFragmentCompat() {
59 | private lateinit var settings: Settings
60 | private var dialog: AlertDialog? = null
61 |
62 | private lateinit var pickFreq: ActivityResultLauncher
63 | private lateinit var pickPitch: ActivityResultLauncher
64 | private lateinit var pickSavedWords: ActivityResultLauncher
65 |
66 | private val viewModel: SettingsViewModel by viewModels {
67 | SettingsViewModelFactory(requireContext())
68 | }
69 |
70 | private fun pickFile(callback: (Uri) -> Unit): ActivityResultLauncher {
71 | return registerForActivityResult(
72 | ActivityResultContracts.StartActivityForResult()
73 | ) {
74 | if (it.resultCode == AppCompatActivity.RESULT_OK) {
75 | val uri = Uri.parse(it.data?.dataString)
76 | if (uri != null) {
77 | callback(uri)
78 | }
79 | }
80 | }
81 | }
82 |
83 | override fun onCreate(savedInstanceState: Bundle?) {
84 | super.onCreate(savedInstanceState)
85 | settings = Settings(PreferenceManager.getDefaultSharedPreferences(requireContext()))
86 | pickFreq = pickFile { viewModel.importFrequencyList(it, viewLifecycleOwner) }
87 | pickPitch = pickFile { viewModel.importPitchAccents(it, viewLifecycleOwner) }
88 | pickSavedWords = pickFile {
89 | settings.setSavedWordsPath(it.path!!)
90 | updateSavedWordsPathLabel()
91 | }
92 | }
93 |
94 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
95 | setPreferencesFromResource(R.xml.preferences, rootKey)
96 |
97 | findPreference("darkTheme")?.setOnPreferenceChangeListener { _, theme ->
98 | setupTheme(theme as Boolean)
99 | true
100 | }
101 |
102 | findPreference("manageDicts")?.setOnPreferenceClickListener {
103 | Navigation.findNavController(requireView()).navigate(R.id.manage_dict_settings_fragment)
104 | true
105 | }
106 |
107 | findPreference("importFreq")?.setOnPreferenceClickListener {
108 | pickFreq.launch(chooseYomichanFile())
109 | true
110 | }
111 |
112 | findPreference("importPitch")?.setOnPreferenceClickListener {
113 | pickPitch.launch(chooseYomichanFile())
114 | true
115 | }
116 |
117 | findPreference("savedWordsPath")?.setOnPreferenceClickListener {
118 | pickSavedWords.launch(chooseSavedWordsFile())
119 | true
120 | }
121 |
122 | updateSavedWordsPathLabel()
123 | }
124 |
125 | private fun updateSavedWordsPathLabel() {
126 | if (PreferenceManager.getDefaultSharedPreferences(requireContext())
127 | .getString("savedWordsPath", null) != null
128 | ) {
129 | findPreference("savedWordsPath")?.summary = "Change path"
130 | }
131 | }
132 |
133 | private fun chooseSavedWordsFile(): Intent {
134 | return Intent(Intent.ACTION_CREATE_DOCUMENT)
135 | .addCategory(Intent.CATEGORY_OPENABLE)
136 | .setType("text/plain")
137 | .putExtra(Intent.EXTRA_TITLE, "words.txt")
138 | .setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
139 | }
140 |
141 | private fun chooseYomichanFile(): Intent {
142 | return Intent(Intent.ACTION_OPEN_DOCUMENT)
143 | .addCategory(Intent.CATEGORY_OPENABLE)
144 | .setType("application/zip")
145 | .setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
146 | }
147 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/ui/fragment/LookupFragment.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.ui.fragment
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.Menu
7 | import android.view.MenuInflater
8 | import android.view.MenuItem
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import androidx.appcompat.app.ActionBar
12 | import androidx.appcompat.app.AppCompatActivity
13 | import androidx.appcompat.widget.SearchView
14 | import androidx.fragment.app.Fragment
15 | import androidx.fragment.app.viewModels
16 | import androidx.lifecycle.Lifecycle
17 | import androidx.lifecycle.ViewModel
18 | import androidx.lifecycle.lifecycleScope
19 | import androidx.lifecycle.repeatOnLifecycle
20 | import androidx.lifecycle.viewModelScope
21 | import androidx.navigation.fragment.navArgs
22 | import androidx.preference.PreferenceManager
23 | import androidx.recyclerview.widget.LinearLayoutManager
24 | import com.kamui.rin.R
25 | import com.kamui.rin.Settings
26 | import com.kamui.rin.adapter.DictEntryAdapter
27 | import com.kamui.rin.databinding.FragmentLookupBinding
28 | import com.kamui.rin.db.model.DictEntry
29 | import com.kamui.rin.db.model.Dictionary
30 | import com.kamui.rin.dict.Lookup
31 | import kotlinx.coroutines.Dispatchers
32 | import kotlinx.coroutines.flow.MutableStateFlow
33 | import kotlinx.coroutines.flow.StateFlow
34 | import kotlinx.coroutines.flow.asStateFlow
35 | import kotlinx.coroutines.flow.update
36 | import kotlinx.coroutines.launch
37 | import java.io.IOException
38 |
39 | data class LookupState(
40 | val results: List> = listOf(),
41 | val currentlySearching: Boolean = false,
42 | val showStartPrompt: Boolean = true,
43 | val noResultsFound: Boolean = false,
44 | val lastQuery: String? = null
45 | )
46 |
47 | class LookupViewModel : ViewModel() {
48 | private val _uiState = MutableStateFlow(LookupState())
49 | val uiState: StateFlow = _uiState.asStateFlow()
50 |
51 | fun hideHome() {
52 | _uiState.update { curr ->
53 | curr.copy(showStartPrompt = false, noResultsFound = false)
54 | }
55 | }
56 |
57 |
58 | fun displayHome() {
59 | if (!uiState.value.noResultsFound) {
60 | _uiState.update { curr ->
61 | curr.copy(showStartPrompt = true)
62 | }
63 | }
64 | }
65 |
66 | fun lookupWord(query: String, helper: Lookup) {
67 | _uiState.update { state ->
68 | state.copy(
69 | currentlySearching = true,
70 | noResultsFound = false,
71 | showStartPrompt = false,
72 | lastQuery = query
73 | )
74 | }
75 | viewModelScope.launch(Dispatchers.IO) {
76 | _uiState.update { state ->
77 | val dictEntries = helper.lookup(query)
78 | state.copy(
79 | currentlySearching = false,
80 | noResultsFound = dictEntries.isEmpty(),
81 | results = dictEntries
82 | )
83 | }
84 | }
85 | }
86 | }
87 |
88 |
89 | class LookupFragment : Fragment() {
90 | val state: LookupViewModel by viewModels()
91 | private var _binding: FragmentLookupBinding? = null
92 | private val binding get() = _binding!!
93 |
94 | private val args: LookupFragmentArgs by navArgs()
95 |
96 | lateinit var helper: Lookup
97 | lateinit var adapter: DictEntryAdapter
98 |
99 | // for customizing search bar
100 | lateinit var actionBar: ActionBar
101 |
102 | override fun onCreate(savedInstanceState: Bundle?) {
103 | super.onCreate(savedInstanceState)
104 | setHasOptionsMenu(true)
105 | }
106 |
107 | override fun onCreateView(
108 | inflater: LayoutInflater, container: ViewGroup?,
109 | savedInstanceState: Bundle?
110 | ): View {
111 | viewLifecycleOwner.lifecycleScope.launch {
112 | repeatOnLifecycle(Lifecycle.State.STARTED) {
113 | state.uiState.collect {
114 | // new/changed entries
115 | adapter = DictEntryAdapter(requireContext(), it.results)
116 | binding.resultRecyclerView.layoutManager = LinearLayoutManager(requireContext())
117 | binding.resultRecyclerView.adapter = adapter
118 | adapter.notifyDataSetChanged()
119 |
120 | if (it.lastQuery != null) actionBar.title = "Results for ${it.lastQuery}"
121 |
122 | binding.homeGroup.visibility =
123 | if (it.showStartPrompt) View.VISIBLE else View.GONE
124 | binding.progressBar.visibility =
125 | if (it.currentlySearching) View.VISIBLE else View.GONE
126 | binding.noResultsFound.visibility =
127 | if (it.noResultsFound) View.VISIBLE else View.GONE
128 | }
129 | }
130 | }
131 |
132 | val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
133 | helper = Lookup(requireContext(), readDeinflectJsonFile(), Settings(sharedPreferences))
134 | _binding = FragmentLookupBinding.inflate(inflater, container, false)
135 | actionBar = (requireActivity() as AppCompatActivity).supportActionBar!!
136 | return binding.root
137 | }
138 |
139 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
140 | inflater.inflate(R.menu.search_action_bar, menu)
141 | val searchAction = menu.findItem(R.id.action_search)
142 | searchAction.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
143 | override fun onMenuItemActionExpand(item: MenuItem): Boolean {
144 | state.hideHome()
145 | return true
146 | }
147 |
148 | override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
149 | state.displayHome()
150 | return true
151 | }
152 | })
153 | val searchView = searchAction.actionView as SearchView
154 | searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
155 | override fun onQueryTextSubmit(query: String): Boolean {
156 | state.lookupWord(query, helper)
157 | return false
158 | }
159 |
160 | override fun onQueryTextChange(s: String): Boolean {
161 | return false
162 | }
163 | })
164 |
165 | // query intent
166 | handleIntent(requireActivity().intent)?.let {
167 | actionBar.setDisplayHomeAsUpEnabled(true)
168 | actionBar.setDisplayShowHomeEnabled(true)
169 | searchView.setQuery(it, true)
170 | }
171 |
172 | if (args.query != null) {
173 | searchView.setQuery(args.query, true)
174 | }
175 | }
176 |
177 | private fun handleIntent(intent: Intent): String? {
178 | return if (Intent.ACTION_PROCESS_TEXT == intent.action) {
179 | val keyword = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)
180 | keyword.toString()
181 | } else {
182 | null
183 | }
184 | }
185 |
186 | private fun readDeinflectJsonFile(): String {
187 | return try {
188 | val stream = requireContext().assets.open("deinflect.json")
189 | val size = stream.available()
190 | val buffer = ByteArray(size)
191 | stream.read(buffer)
192 | stream.close()
193 | String(buffer)
194 | } catch (e: IOException) {
195 | ""
196 | }
197 | }
198 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/ui/fragment/WordDetailFragment.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.ui.fragment
2 |
3 | import android.app.AlertDialog
4 | import android.content.ClipData
5 | import android.content.ClipboardManager
6 | import android.content.Context
7 | import android.content.DialogInterface
8 | import android.os.Bundle
9 | import android.view.LayoutInflater
10 | import android.view.View
11 | import android.view.ViewGroup
12 | import android.widget.Toast
13 | import androidx.appcompat.app.AppCompatActivity
14 | import androidx.fragment.app.Fragment
15 | import androidx.fragment.app.viewModels
16 | import androidx.lifecycle.Lifecycle
17 | import androidx.lifecycle.ViewModel
18 | import androidx.lifecycle.ViewModelProvider
19 | import androidx.lifecycle.lifecycleScope
20 | import androidx.lifecycle.repeatOnLifecycle
21 | import androidx.lifecycle.viewModelScope
22 | import androidx.navigation.fragment.navArgs
23 | import com.google.android.material.chip.Chip
24 | import com.kamui.rin.R
25 | import com.kamui.rin.databinding.FragmentWordDetailBinding
26 | import com.kamui.rin.db.AppDatabase
27 | import com.kamui.rin.db.model.DictEntry
28 | import com.kamui.rin.db.model.SavedWord
29 | import com.kamui.rin.db.model.Tag
30 | import kotlinx.coroutines.Dispatchers
31 | import kotlinx.coroutines.flow.MutableStateFlow
32 | import kotlinx.coroutines.flow.StateFlow
33 | import kotlinx.coroutines.flow.asStateFlow
34 | import kotlinx.coroutines.flow.update
35 | import kotlinx.coroutines.launch
36 | import java.text.DecimalFormat
37 |
38 | data class WordDetailState(
39 | val entry: DictEntry? = null,
40 | val frequency: Long? = null,
41 | val pitch: String? = null,
42 | val tags: List = listOf(),
43 | val saved: Boolean = false
44 | )
45 |
46 | class WordDetailViewModel(private val database: AppDatabase, entryId: Long) : ViewModel() {
47 | private val _uiState = MutableStateFlow(WordDetailState())
48 | val uiState: StateFlow = _uiState.asStateFlow()
49 |
50 | init {
51 | viewModelScope.launch(Dispatchers.IO) {
52 | _uiState.update { currentState ->
53 | val entry = database.dictEntryDao().searchEntryById(entryId)
54 | val tags = database.dictEntryDao().getTagsForEntry(entryId)
55 | val freq = database.frequencyDao().getFrequencyForWord(entry.kanji)
56 | val pitch = database.pitchAccentDao().getPitchForWord(entry.kanji)
57 | currentState.copy(
58 | entry = entry,
59 | tags = tags,
60 | saved = database.savedDao().existsWord(entry.kanji),
61 | frequency = freq,
62 | pitch = pitch
63 | )
64 | }
65 | }
66 | }
67 |
68 | fun removeWord(kanji: String) {
69 | viewModelScope.launch(Dispatchers.IO) {
70 | _uiState.update { currentState ->
71 | database.savedDao().deleteWordByKanji(kanji)
72 | currentState.copy(saved = false)
73 | }
74 | }
75 | }
76 |
77 | fun saveWord(kanji: String) {
78 | viewModelScope.launch(Dispatchers.IO) {
79 | _uiState.update { currentState ->
80 | database.savedDao().insertWord(SavedWord(kanji = kanji))
81 | currentState.copy(saved = true)
82 | }
83 | }
84 | }
85 | }
86 |
87 | class WordDetailViewModelFactory(private val database: AppDatabase, private val wordId: Long) :
88 | ViewModelProvider.Factory {
89 | override fun create(modelClass: Class): T {
90 | return WordDetailViewModel(database, wordId) as T
91 | }
92 | }
93 |
94 | class WordDetailFragment : Fragment() {
95 | private var _binding: FragmentWordDetailBinding? = null
96 | private val binding get() = _binding!!
97 | private val args: WordDetailFragmentArgs by navArgs()
98 |
99 | override fun onCreateView(
100 | inflater: LayoutInflater, container: ViewGroup?,
101 | savedInstanceState: Bundle?
102 | ): View {
103 | _binding = FragmentWordDetailBinding.inflate(inflater, container, false)
104 |
105 | val viewModel: WordDetailViewModel by viewModels {
106 | WordDetailViewModelFactory(AppDatabase.buildDatabase(binding.root.context), args.wordId)
107 | }
108 |
109 | viewLifecycleOwner.lifecycleScope.launch {
110 | repeatOnLifecycle(Lifecycle.State.STARTED) {
111 | viewModel.uiState.collect {
112 | if (it.entry != null) {
113 | binding.loadingProgress.visibility = View.GONE
114 | binding.cardsHolder.visibility = View.VISIBLE
115 | // set title bar to word
116 | val entry = it.entry
117 | val tags = it.tags
118 | (requireActivity() as AppCompatActivity).supportActionBar?.title =
119 | entry.kanji
120 |
121 | // fill in UI elements
122 | binding.secondaryTextCard.text = entry.reading
123 | binding.wordTextView.text = entry.kanji
124 | binding.meaningTextView.text = entry.meaning
125 | binding.pitchText.text = it.pitch
126 | binding.freqChip.text = formatFrequency(it.frequency)
127 |
128 | if (binding.chipLayout.childCount == 0) {
129 | tags.forEach { tag -> configureChip(tag) }
130 | }
131 |
132 | if (tags.isEmpty()) {
133 | binding.chipLayout.visibility = View.GONE
134 | }
135 | if (it.pitch == null) {
136 | binding.pitchCard.visibility = View.GONE
137 | }
138 | if (it.frequency == null) {
139 | binding.freqChip.visibility = View.GONE
140 | }
141 |
142 | binding.copyButton.setOnClickListener {
143 | val clipboard =
144 | activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
145 | val clip: ClipData =
146 | ClipData.newPlainText("${entry.kanji} definition", entry.meaning)
147 | clipboard.setPrimaryClip(clip)
148 | Toast.makeText(
149 | context,
150 | "Copied definition to clipboard",
151 | Toast.LENGTH_SHORT
152 | ).show()
153 | }
154 |
155 | binding.saveWordButton.setOnClickListener { _ ->
156 | if (it.saved) {
157 | viewModel.removeWord(entry.kanji)
158 | } else {
159 | viewModel.saveWord(entry.kanji)
160 | }
161 | }
162 |
163 | if (it.saved) {
164 | binding.saveWordButton.setImageResource(R.drawable.ic_baseline_bookmark_added_24)
165 | } else {
166 | binding.saveWordButton.setImageResource(R.drawable.ic_baseline_bookmark_border_24)
167 | }
168 | }
169 | }
170 | }
171 | }
172 |
173 | return binding.root
174 | }
175 |
176 | private fun configureChip(tag: Tag) {
177 | val chip = Chip(context)
178 | chip.text = tag.name
179 | chip.setOnClickListener { showTagAlert(tag.notes) }
180 | binding.chipLayout.addView(chip)
181 | }
182 |
183 | private fun showTagAlert(tagNotes: String) {
184 | val alertDialog = AlertDialog.Builder(activity)
185 | .setMessage(tagNotes)
186 | .setPositiveButton("OK") { dialog: DialogInterface, _: Int -> dialog.dismiss() }
187 | .create()
188 | alertDialog.show()
189 | }
190 |
191 | private fun formatFrequency(frequency: Long?): String? {
192 | return frequency?.let {
193 | val formatter = DecimalFormat("#,###")
194 | return "Freq: ${formatter.format(frequency)}"
195 | }
196 | }
197 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/ui/fragment/SavedWordsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.ui.fragment
2 |
3 | import android.app.AlertDialog
4 | import android.content.ContentResolver
5 | import android.content.DialogInterface
6 | import android.content.Intent
7 | import android.net.Uri
8 | import android.os.Bundle
9 | import android.view.LayoutInflater
10 | import android.view.Menu
11 | import android.view.MenuInflater
12 | import android.view.MenuItem
13 | import android.view.View
14 | import android.view.ViewGroup
15 | import androidx.fragment.app.Fragment
16 | import androidx.fragment.app.viewModels
17 | import androidx.lifecycle.Lifecycle
18 | import androidx.lifecycle.ViewModel
19 | import androidx.lifecycle.ViewModelProvider
20 | import androidx.lifecycle.lifecycleScope
21 | import androidx.lifecycle.repeatOnLifecycle
22 | import androidx.lifecycle.viewModelScope
23 | import androidx.navigation.findNavController
24 | import androidx.preference.PreferenceManager
25 | import androidx.recyclerview.widget.DividerItemDecoration
26 | import androidx.recyclerview.widget.ItemTouchHelper
27 | import androidx.recyclerview.widget.LinearLayoutManager
28 | import androidx.recyclerview.widget.RecyclerView
29 | import com.kamui.rin.R
30 | import com.kamui.rin.Settings
31 | import com.kamui.rin.databinding.FragmentSavedWordsBinding
32 | import com.kamui.rin.databinding.SavedWordBinding
33 | import com.kamui.rin.db.AppDatabase
34 | import com.kamui.rin.db.model.SavedWord
35 | import kotlinx.coroutines.Dispatchers
36 | import kotlinx.coroutines.flow.MutableStateFlow
37 | import kotlinx.coroutines.flow.StateFlow
38 | import kotlinx.coroutines.flow.asStateFlow
39 | import kotlinx.coroutines.flow.update
40 | import kotlinx.coroutines.launch
41 |
42 | data class SavedWordsState(
43 | val words: List = listOf()
44 | )
45 |
46 | class SavedWordsViewModel(private val database: AppDatabase) : ViewModel() {
47 | private val _uiState = MutableStateFlow(SavedWordsState())
48 | val uiState: StateFlow = _uiState.asStateFlow()
49 |
50 | init {
51 | viewModelScope.launch(Dispatchers.IO) {
52 | database.savedDao().getAllSaved().collect {
53 | _uiState.update { currentState ->
54 | currentState.copy(
55 | words = it
56 | )
57 | }
58 | }
59 | }
60 | }
61 |
62 | fun deleteItem(index: Int) {
63 | _uiState.update { state ->
64 | viewModelScope.launch(Dispatchers.IO) {
65 | database.savedDao().deleteWord(state.words[index])
66 | }
67 | state.copy(
68 | words = state.words.subList(0, index) + state.words.subList(
69 | index + 1,
70 | state.words.size
71 | )
72 | )
73 | }
74 | }
75 |
76 | fun deleteAll() {
77 | _uiState.update { state ->
78 | viewModelScope.launch(Dispatchers.IO) {
79 | database.savedDao().deleteAllWords()
80 | }
81 | state.copy(words = listOf())
82 | }
83 | }
84 |
85 | fun saveWordsToFile(fileUri: Uri, resolver: ContentResolver?) {
86 | viewModelScope.launch(Dispatchers.IO) {
87 | val wordListString = uiState.value.words.joinToString("\n")
88 | resolver?.takePersistableUriPermission(fileUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
89 | val outputStream = resolver?.openOutputStream(fileUri, "wt")
90 | outputStream?.write(wordListString.toByteArray())
91 | outputStream?.close()
92 | }
93 | }
94 | }
95 |
96 | class SavedWordsViewModelFactory(private val database: AppDatabase) : ViewModelProvider.Factory {
97 | override fun create(modelClass: Class): T {
98 | return SavedWordsViewModel(database) as T
99 | }
100 | }
101 |
102 |
103 | class SavedWordsFragment : Fragment() {
104 | private var _binding: FragmentSavedWordsBinding? = null
105 | private val binding get() = _binding!!
106 |
107 | val wordsViewModel: SavedWordsViewModel by viewModels {
108 | SavedWordsViewModelFactory(AppDatabase.buildDatabase(binding.root.context))
109 | }
110 |
111 | private lateinit var settings: Settings
112 |
113 | override fun onCreate(savedInstanceState: Bundle?) {
114 | super.onCreate(savedInstanceState)
115 | settings = Settings(PreferenceManager.getDefaultSharedPreferences(requireContext()))
116 | setHasOptionsMenu(true)
117 | }
118 |
119 | override fun onCreateView(
120 | inflater: LayoutInflater, container: ViewGroup?,
121 | savedInstanceState: Bundle?
122 | ): View {
123 | _binding = FragmentSavedWordsBinding.inflate(inflater, container, false)
124 |
125 | binding.wordListRecycler.addItemDecoration(
126 | DividerItemDecoration(
127 | context,
128 | DividerItemDecoration.VERTICAL
129 | )
130 | )
131 | binding.wordListRecycler.layoutManager = LinearLayoutManager(context)
132 | val itemTouchHelper = ItemTouchHelper(ItemTouchCallback())
133 | itemTouchHelper.attachToRecyclerView(binding.wordListRecycler)
134 |
135 | binding.saveButton.setOnClickListener { saveToFile() }
136 |
137 | viewLifecycleOwner.lifecycleScope.launch {
138 | repeatOnLifecycle(Lifecycle.State.STARTED) {
139 | wordsViewModel.uiState.collect {
140 | binding.wordListRecycler.adapter = Adapter()
141 | }
142 | }
143 | }
144 |
145 | return binding.root
146 | }
147 |
148 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
149 | inflater.inflate(R.menu.saved_words_action_bar, menu)
150 | }
151 |
152 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
153 | return when (item.itemId) {
154 | R.id.clearListButton -> {
155 | wordsViewModel.deleteAll()
156 | return true
157 | }
158 |
159 | else -> super.onOptionsItemSelected(item)
160 | }
161 | }
162 |
163 |
164 | private fun saveToFile() {
165 | val fileUri = settings.savedWordsPath()
166 | if (fileUri != null) {
167 | wordsViewModel.saveWordsToFile(fileUri, activity?.contentResolver)
168 | } else {
169 | val alertDialog = AlertDialog.Builder(activity)
170 | .setMessage("Specify a file in settings")
171 | .setPositiveButton("OK") { dialog: DialogInterface, _: Int -> dialog.dismiss() }
172 | .create()
173 | alertDialog.show()
174 | }
175 | }
176 |
177 | inner class ItemTouchCallback : ItemTouchHelper.Callback() {
178 | override fun getMovementFlags(
179 | recyclerView: RecyclerView,
180 | viewHolder: RecyclerView.ViewHolder
181 | ): Int {
182 | return makeMovementFlags(0, ItemTouchHelper.RIGHT)
183 | }
184 |
185 | override fun onMove(
186 | recyclerView: RecyclerView,
187 | viewHolder: RecyclerView.ViewHolder,
188 | target: RecyclerView.ViewHolder
189 | ): Boolean {
190 | return false
191 | }
192 |
193 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
194 | val pos = viewHolder.absoluteAdapterPosition
195 | wordsViewModel.deleteItem(pos)
196 | }
197 | }
198 |
199 | inner class ViewHolder(val binding: SavedWordBinding) :
200 | RecyclerView.ViewHolder(binding.root)
201 |
202 | inner class Adapter : RecyclerView.Adapter() {
203 | override fun onCreateViewHolder(
204 | parent: ViewGroup,
205 | viewType: Int
206 | ): SavedWordsFragment.ViewHolder {
207 | val binding =
208 | SavedWordBinding.inflate(LayoutInflater.from(parent.context), parent, false)
209 | return ViewHolder(binding)
210 | }
211 |
212 | override fun getItemCount(): Int {
213 | return wordsViewModel.uiState.value.words.size
214 | }
215 |
216 | override fun onBindViewHolder(holder: SavedWordsFragment.ViewHolder, position: Int) {
217 | val kanji = wordsViewModel.uiState.value.words[position].kanji
218 | holder.binding.word.text = kanji
219 | holder.binding.root.setOnClickListener {
220 | val searchLink = Uri.parse("rin://search/${kanji}")
221 | it.findNavController().navigate(searchLink)
222 | }
223 | }
224 | }
225 | }
--------------------------------------------------------------------------------
/app/schemas/com.kamui.rin.db.AppDatabase/2.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 2,
5 | "identityHash": "447616638c64885e33433861a5594291",
6 | "entities": [
7 | {
8 | "tableName": "DictEntry",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`entryId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL, `meaning` TEXT NOT NULL, `reading` TEXT NOT NULL, `dictionaryId` INTEGER NOT NULL, `pitchAccent` TEXT, `freq` INTEGER, FOREIGN KEY(`dictionaryId`) REFERENCES `Dictionary`(`dictId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
10 | "fields": [
11 | {
12 | "fieldPath": "entryId",
13 | "columnName": "entryId",
14 | "affinity": "INTEGER",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "kanji",
19 | "columnName": "kanji",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "meaning",
25 | "columnName": "meaning",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | },
29 | {
30 | "fieldPath": "reading",
31 | "columnName": "reading",
32 | "affinity": "TEXT",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "dictionaryId",
37 | "columnName": "dictionaryId",
38 | "affinity": "INTEGER",
39 | "notNull": true
40 | },
41 | {
42 | "fieldPath": "pitchAccent",
43 | "columnName": "pitchAccent",
44 | "affinity": "TEXT",
45 | "notNull": false
46 | },
47 | {
48 | "fieldPath": "freq",
49 | "columnName": "freq",
50 | "affinity": "INTEGER",
51 | "notNull": false
52 | }
53 | ],
54 | "primaryKey": {
55 | "autoGenerate": true,
56 | "columnNames": [
57 | "entryId"
58 | ]
59 | },
60 | "indices": [
61 | {
62 | "name": "idx_word_reading",
63 | "unique": false,
64 | "columnNames": [
65 | "kanji",
66 | "reading"
67 | ],
68 | "orders": [],
69 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_word_reading` ON `${TABLE_NAME}` (`kanji`, `reading`)"
70 | },
71 | {
72 | "name": "idx_dictionary_entry_ref",
73 | "unique": false,
74 | "columnNames": [
75 | "dictionaryId"
76 | ],
77 | "orders": [],
78 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_dictionary_entry_ref` ON `${TABLE_NAME}` (`dictionaryId`)"
79 | }
80 | ],
81 | "foreignKeys": [
82 | {
83 | "table": "Dictionary",
84 | "onDelete": "CASCADE",
85 | "onUpdate": "NO ACTION",
86 | "columns": [
87 | "dictionaryId"
88 | ],
89 | "referencedColumns": [
90 | "dictId"
91 | ]
92 | }
93 | ]
94 | },
95 | {
96 | "tableName": "DictEntryTagCrossRef",
97 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`entryId` INTEGER NOT NULL, `tagId` INTEGER NOT NULL, PRIMARY KEY(`entryId`, `tagId`))",
98 | "fields": [
99 | {
100 | "fieldPath": "entryId",
101 | "columnName": "entryId",
102 | "affinity": "INTEGER",
103 | "notNull": true
104 | },
105 | {
106 | "fieldPath": "tagId",
107 | "columnName": "tagId",
108 | "affinity": "INTEGER",
109 | "notNull": true
110 | }
111 | ],
112 | "primaryKey": {
113 | "autoGenerate": false,
114 | "columnNames": [
115 | "entryId",
116 | "tagId"
117 | ]
118 | },
119 | "indices": [
120 | {
121 | "name": "index_DictEntryTagCrossRef_tagId",
122 | "unique": false,
123 | "columnNames": [
124 | "tagId"
125 | ],
126 | "orders": [],
127 | "createSql": "CREATE INDEX IF NOT EXISTS `index_DictEntryTagCrossRef_tagId` ON `${TABLE_NAME}` (`tagId`)"
128 | }
129 | ],
130 | "foreignKeys": []
131 | },
132 | {
133 | "tableName": "SavedWord",
134 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`savedWordId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL)",
135 | "fields": [
136 | {
137 | "fieldPath": "savedWordId",
138 | "columnName": "savedWordId",
139 | "affinity": "INTEGER",
140 | "notNull": true
141 | },
142 | {
143 | "fieldPath": "kanji",
144 | "columnName": "kanji",
145 | "affinity": "TEXT",
146 | "notNull": true
147 | }
148 | ],
149 | "primaryKey": {
150 | "autoGenerate": true,
151 | "columnNames": [
152 | "savedWordId"
153 | ]
154 | },
155 | "indices": [],
156 | "foreignKeys": []
157 | },
158 | {
159 | "tableName": "Tag",
160 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dictionaryId` INTEGER NOT NULL, `name` TEXT NOT NULL, `notes` TEXT NOT NULL, FOREIGN KEY(`dictionaryId`) REFERENCES `Dictionary`(`dictId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
161 | "fields": [
162 | {
163 | "fieldPath": "tagId",
164 | "columnName": "tagId",
165 | "affinity": "INTEGER",
166 | "notNull": true
167 | },
168 | {
169 | "fieldPath": "dictionaryId",
170 | "columnName": "dictionaryId",
171 | "affinity": "INTEGER",
172 | "notNull": true
173 | },
174 | {
175 | "fieldPath": "name",
176 | "columnName": "name",
177 | "affinity": "TEXT",
178 | "notNull": true
179 | },
180 | {
181 | "fieldPath": "notes",
182 | "columnName": "notes",
183 | "affinity": "TEXT",
184 | "notNull": true
185 | }
186 | ],
187 | "primaryKey": {
188 | "autoGenerate": true,
189 | "columnNames": [
190 | "tagId"
191 | ]
192 | },
193 | "indices": [
194 | {
195 | "name": "idx_dictionary_tag_ref",
196 | "unique": false,
197 | "columnNames": [
198 | "dictionaryId"
199 | ],
200 | "orders": [],
201 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_dictionary_tag_ref` ON `${TABLE_NAME}` (`dictionaryId`)"
202 | }
203 | ],
204 | "foreignKeys": [
205 | {
206 | "table": "Dictionary",
207 | "onDelete": "CASCADE",
208 | "onUpdate": "NO ACTION",
209 | "columns": [
210 | "dictionaryId"
211 | ],
212 | "referencedColumns": [
213 | "dictId"
214 | ]
215 | }
216 | ]
217 | },
218 | {
219 | "tableName": "Dictionary",
220 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dictId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL)",
221 | "fields": [
222 | {
223 | "fieldPath": "dictId",
224 | "columnName": "dictId",
225 | "affinity": "INTEGER",
226 | "notNull": true
227 | },
228 | {
229 | "fieldPath": "name",
230 | "columnName": "name",
231 | "affinity": "TEXT",
232 | "notNull": true
233 | },
234 | {
235 | "fieldPath": "order",
236 | "columnName": "order",
237 | "affinity": "INTEGER",
238 | "notNull": true
239 | }
240 | ],
241 | "primaryKey": {
242 | "autoGenerate": true,
243 | "columnNames": [
244 | "dictId"
245 | ]
246 | },
247 | "indices": [],
248 | "foreignKeys": []
249 | }
250 | ],
251 | "views": [],
252 | "setupQueries": [
253 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
254 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '447616638c64885e33433861a5594291')"
255 | ]
256 | }
257 | }
--------------------------------------------------------------------------------
/app/schemas/com.kamui.rin.db.AppDatabase/5.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 5,
5 | "identityHash": "d830f3ecbc866bf868547d745a73458d",
6 | "entities": [
7 | {
8 | "tableName": "PitchAccent",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pitchId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL, `pitch` TEXT NOT NULL)",
10 | "fields": [
11 | {
12 | "fieldPath": "pitchId",
13 | "columnName": "pitchId",
14 | "affinity": "INTEGER",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "kanji",
19 | "columnName": "kanji",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "pitch",
25 | "columnName": "pitch",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | }
29 | ],
30 | "primaryKey": {
31 | "autoGenerate": true,
32 | "columnNames": [
33 | "pitchId"
34 | ]
35 | },
36 | "indices": [],
37 | "foreignKeys": []
38 | },
39 | {
40 | "tableName": "Frequency",
41 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`freqId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL, `frequency` INTEGER NOT NULL)",
42 | "fields": [
43 | {
44 | "fieldPath": "freqId",
45 | "columnName": "freqId",
46 | "affinity": "INTEGER",
47 | "notNull": true
48 | },
49 | {
50 | "fieldPath": "kanji",
51 | "columnName": "kanji",
52 | "affinity": "TEXT",
53 | "notNull": true
54 | },
55 | {
56 | "fieldPath": "frequency",
57 | "columnName": "frequency",
58 | "affinity": "INTEGER",
59 | "notNull": true
60 | }
61 | ],
62 | "primaryKey": {
63 | "autoGenerate": true,
64 | "columnNames": [
65 | "freqId"
66 | ]
67 | },
68 | "indices": [],
69 | "foreignKeys": []
70 | },
71 | {
72 | "tableName": "DictEntry",
73 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`entryId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL, `meaning` TEXT NOT NULL, `reading` TEXT NOT NULL, `dictionaryId` INTEGER NOT NULL, FOREIGN KEY(`dictionaryId`) REFERENCES `Dictionary`(`dictId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
74 | "fields": [
75 | {
76 | "fieldPath": "entryId",
77 | "columnName": "entryId",
78 | "affinity": "INTEGER",
79 | "notNull": true
80 | },
81 | {
82 | "fieldPath": "kanji",
83 | "columnName": "kanji",
84 | "affinity": "TEXT",
85 | "notNull": true
86 | },
87 | {
88 | "fieldPath": "meaning",
89 | "columnName": "meaning",
90 | "affinity": "TEXT",
91 | "notNull": true
92 | },
93 | {
94 | "fieldPath": "reading",
95 | "columnName": "reading",
96 | "affinity": "TEXT",
97 | "notNull": true
98 | },
99 | {
100 | "fieldPath": "dictionaryId",
101 | "columnName": "dictionaryId",
102 | "affinity": "INTEGER",
103 | "notNull": true
104 | }
105 | ],
106 | "primaryKey": {
107 | "autoGenerate": true,
108 | "columnNames": [
109 | "entryId"
110 | ]
111 | },
112 | "indices": [
113 | {
114 | "name": "idx_word_reading",
115 | "unique": false,
116 | "columnNames": [
117 | "kanji",
118 | "reading"
119 | ],
120 | "orders": [],
121 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_word_reading` ON `${TABLE_NAME}` (`kanji`, `reading`)"
122 | },
123 | {
124 | "name": "idx_dictionary_entry_ref",
125 | "unique": false,
126 | "columnNames": [
127 | "dictionaryId"
128 | ],
129 | "orders": [],
130 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_dictionary_entry_ref` ON `${TABLE_NAME}` (`dictionaryId`)"
131 | }
132 | ],
133 | "foreignKeys": [
134 | {
135 | "table": "Dictionary",
136 | "onDelete": "CASCADE",
137 | "onUpdate": "NO ACTION",
138 | "columns": [
139 | "dictionaryId"
140 | ],
141 | "referencedColumns": [
142 | "dictId"
143 | ]
144 | }
145 | ]
146 | },
147 | {
148 | "tableName": "DictEntryTagCrossRef",
149 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`entryId` INTEGER NOT NULL, `tagId` INTEGER NOT NULL, PRIMARY KEY(`entryId`, `tagId`))",
150 | "fields": [
151 | {
152 | "fieldPath": "entryId",
153 | "columnName": "entryId",
154 | "affinity": "INTEGER",
155 | "notNull": true
156 | },
157 | {
158 | "fieldPath": "tagId",
159 | "columnName": "tagId",
160 | "affinity": "INTEGER",
161 | "notNull": true
162 | }
163 | ],
164 | "primaryKey": {
165 | "autoGenerate": false,
166 | "columnNames": [
167 | "entryId",
168 | "tagId"
169 | ]
170 | },
171 | "indices": [
172 | {
173 | "name": "index_DictEntryTagCrossRef_tagId",
174 | "unique": false,
175 | "columnNames": [
176 | "tagId"
177 | ],
178 | "orders": [],
179 | "createSql": "CREATE INDEX IF NOT EXISTS `index_DictEntryTagCrossRef_tagId` ON `${TABLE_NAME}` (`tagId`)"
180 | }
181 | ],
182 | "foreignKeys": []
183 | },
184 | {
185 | "tableName": "SavedWord",
186 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`savedWordId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL)",
187 | "fields": [
188 | {
189 | "fieldPath": "savedWordId",
190 | "columnName": "savedWordId",
191 | "affinity": "INTEGER",
192 | "notNull": true
193 | },
194 | {
195 | "fieldPath": "kanji",
196 | "columnName": "kanji",
197 | "affinity": "TEXT",
198 | "notNull": true
199 | }
200 | ],
201 | "primaryKey": {
202 | "autoGenerate": true,
203 | "columnNames": [
204 | "savedWordId"
205 | ]
206 | },
207 | "indices": [],
208 | "foreignKeys": []
209 | },
210 | {
211 | "tableName": "Tag",
212 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dictionaryId` INTEGER NOT NULL, `name` TEXT NOT NULL, `notes` TEXT NOT NULL, FOREIGN KEY(`dictionaryId`) REFERENCES `Dictionary`(`dictId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
213 | "fields": [
214 | {
215 | "fieldPath": "tagId",
216 | "columnName": "tagId",
217 | "affinity": "INTEGER",
218 | "notNull": true
219 | },
220 | {
221 | "fieldPath": "dictionaryId",
222 | "columnName": "dictionaryId",
223 | "affinity": "INTEGER",
224 | "notNull": true
225 | },
226 | {
227 | "fieldPath": "name",
228 | "columnName": "name",
229 | "affinity": "TEXT",
230 | "notNull": true
231 | },
232 | {
233 | "fieldPath": "notes",
234 | "columnName": "notes",
235 | "affinity": "TEXT",
236 | "notNull": true
237 | }
238 | ],
239 | "primaryKey": {
240 | "autoGenerate": true,
241 | "columnNames": [
242 | "tagId"
243 | ]
244 | },
245 | "indices": [
246 | {
247 | "name": "idx_dictionary_tag_ref",
248 | "unique": false,
249 | "columnNames": [
250 | "dictionaryId"
251 | ],
252 | "orders": [],
253 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_dictionary_tag_ref` ON `${TABLE_NAME}` (`dictionaryId`)"
254 | }
255 | ],
256 | "foreignKeys": [
257 | {
258 | "table": "Dictionary",
259 | "onDelete": "CASCADE",
260 | "onUpdate": "NO ACTION",
261 | "columns": [
262 | "dictionaryId"
263 | ],
264 | "referencedColumns": [
265 | "dictId"
266 | ]
267 | }
268 | ]
269 | },
270 | {
271 | "tableName": "Dictionary",
272 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dictId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL)",
273 | "fields": [
274 | {
275 | "fieldPath": "dictId",
276 | "columnName": "dictId",
277 | "affinity": "INTEGER",
278 | "notNull": true
279 | },
280 | {
281 | "fieldPath": "name",
282 | "columnName": "name",
283 | "affinity": "TEXT",
284 | "notNull": true
285 | },
286 | {
287 | "fieldPath": "order",
288 | "columnName": "order",
289 | "affinity": "INTEGER",
290 | "notNull": true
291 | }
292 | ],
293 | "primaryKey": {
294 | "autoGenerate": true,
295 | "columnNames": [
296 | "dictId"
297 | ]
298 | },
299 | "indices": [],
300 | "foreignKeys": []
301 | }
302 | ],
303 | "views": [],
304 | "setupQueries": [
305 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
306 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd830f3ecbc866bf868547d745a73458d')"
307 | ]
308 | }
309 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_word_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
17 |
18 |
23 |
24 |
30 |
31 |
32 |
36 |
37 |
44 |
45 |
46 |
47 |
48 |
54 |
55 |
64 |
65 |
69 |
70 |
75 |
76 |
79 |
80 |
90 |
91 |
104 |
105 |
106 |
107 |
114 |
115 |
124 |
125 |
128 |
129 |
141 |
142 |
156 |
157 |
158 |
159 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
179 |
180 |
184 |
185 |
193 |
194 |
195 |
196 |
197 |
198 |
--------------------------------------------------------------------------------
/app/schemas/com.kamui.rin.db.AppDatabase/3.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 3,
5 | "identityHash": "b50e605b20426500c9b65e6a5fddc34c",
6 | "entities": [
7 | {
8 | "tableName": "PitchAccent",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pitchId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL, `pitch` TEXT NOT NULL)",
10 | "fields": [
11 | {
12 | "fieldPath": "pitchId",
13 | "columnName": "pitchId",
14 | "affinity": "INTEGER",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "kanji",
19 | "columnName": "kanji",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "pitch",
25 | "columnName": "pitch",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | }
29 | ],
30 | "primaryKey": {
31 | "autoGenerate": true,
32 | "columnNames": [
33 | "pitchId"
34 | ]
35 | },
36 | "indices": [],
37 | "foreignKeys": []
38 | },
39 | {
40 | "tableName": "Frequency",
41 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`freqId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL, `frequency` INTEGER NOT NULL)",
42 | "fields": [
43 | {
44 | "fieldPath": "freqId",
45 | "columnName": "freqId",
46 | "affinity": "INTEGER",
47 | "notNull": true
48 | },
49 | {
50 | "fieldPath": "kanji",
51 | "columnName": "kanji",
52 | "affinity": "TEXT",
53 | "notNull": true
54 | },
55 | {
56 | "fieldPath": "frequency",
57 | "columnName": "frequency",
58 | "affinity": "INTEGER",
59 | "notNull": true
60 | }
61 | ],
62 | "primaryKey": {
63 | "autoGenerate": true,
64 | "columnNames": [
65 | "freqId"
66 | ]
67 | },
68 | "indices": [],
69 | "foreignKeys": []
70 | },
71 | {
72 | "tableName": "DictEntry",
73 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`entryId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL, `meaning` TEXT NOT NULL, `reading` TEXT NOT NULL, `dictionaryId` INTEGER NOT NULL, `pitchAccent` TEXT, `freq` INTEGER, FOREIGN KEY(`dictionaryId`) REFERENCES `Dictionary`(`dictId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
74 | "fields": [
75 | {
76 | "fieldPath": "entryId",
77 | "columnName": "entryId",
78 | "affinity": "INTEGER",
79 | "notNull": true
80 | },
81 | {
82 | "fieldPath": "kanji",
83 | "columnName": "kanji",
84 | "affinity": "TEXT",
85 | "notNull": true
86 | },
87 | {
88 | "fieldPath": "meaning",
89 | "columnName": "meaning",
90 | "affinity": "TEXT",
91 | "notNull": true
92 | },
93 | {
94 | "fieldPath": "reading",
95 | "columnName": "reading",
96 | "affinity": "TEXT",
97 | "notNull": true
98 | },
99 | {
100 | "fieldPath": "dictionaryId",
101 | "columnName": "dictionaryId",
102 | "affinity": "INTEGER",
103 | "notNull": true
104 | },
105 | {
106 | "fieldPath": "pitchAccent",
107 | "columnName": "pitchAccent",
108 | "affinity": "TEXT",
109 | "notNull": false
110 | },
111 | {
112 | "fieldPath": "freq",
113 | "columnName": "freq",
114 | "affinity": "INTEGER",
115 | "notNull": false
116 | }
117 | ],
118 | "primaryKey": {
119 | "autoGenerate": true,
120 | "columnNames": [
121 | "entryId"
122 | ]
123 | },
124 | "indices": [
125 | {
126 | "name": "idx_word_reading",
127 | "unique": false,
128 | "columnNames": [
129 | "kanji",
130 | "reading"
131 | ],
132 | "orders": [],
133 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_word_reading` ON `${TABLE_NAME}` (`kanji`, `reading`)"
134 | },
135 | {
136 | "name": "idx_dictionary_entry_ref",
137 | "unique": false,
138 | "columnNames": [
139 | "dictionaryId"
140 | ],
141 | "orders": [],
142 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_dictionary_entry_ref` ON `${TABLE_NAME}` (`dictionaryId`)"
143 | }
144 | ],
145 | "foreignKeys": [
146 | {
147 | "table": "Dictionary",
148 | "onDelete": "CASCADE",
149 | "onUpdate": "NO ACTION",
150 | "columns": [
151 | "dictionaryId"
152 | ],
153 | "referencedColumns": [
154 | "dictId"
155 | ]
156 | }
157 | ]
158 | },
159 | {
160 | "tableName": "DictEntryTagCrossRef",
161 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`entryId` INTEGER NOT NULL, `tagId` INTEGER NOT NULL, PRIMARY KEY(`entryId`, `tagId`))",
162 | "fields": [
163 | {
164 | "fieldPath": "entryId",
165 | "columnName": "entryId",
166 | "affinity": "INTEGER",
167 | "notNull": true
168 | },
169 | {
170 | "fieldPath": "tagId",
171 | "columnName": "tagId",
172 | "affinity": "INTEGER",
173 | "notNull": true
174 | }
175 | ],
176 | "primaryKey": {
177 | "autoGenerate": false,
178 | "columnNames": [
179 | "entryId",
180 | "tagId"
181 | ]
182 | },
183 | "indices": [
184 | {
185 | "name": "index_DictEntryTagCrossRef_tagId",
186 | "unique": false,
187 | "columnNames": [
188 | "tagId"
189 | ],
190 | "orders": [],
191 | "createSql": "CREATE INDEX IF NOT EXISTS `index_DictEntryTagCrossRef_tagId` ON `${TABLE_NAME}` (`tagId`)"
192 | }
193 | ],
194 | "foreignKeys": []
195 | },
196 | {
197 | "tableName": "SavedWord",
198 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`savedWordId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL)",
199 | "fields": [
200 | {
201 | "fieldPath": "savedWordId",
202 | "columnName": "savedWordId",
203 | "affinity": "INTEGER",
204 | "notNull": true
205 | },
206 | {
207 | "fieldPath": "kanji",
208 | "columnName": "kanji",
209 | "affinity": "TEXT",
210 | "notNull": true
211 | }
212 | ],
213 | "primaryKey": {
214 | "autoGenerate": true,
215 | "columnNames": [
216 | "savedWordId"
217 | ]
218 | },
219 | "indices": [],
220 | "foreignKeys": []
221 | },
222 | {
223 | "tableName": "Tag",
224 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dictionaryId` INTEGER NOT NULL, `name` TEXT NOT NULL, `notes` TEXT NOT NULL, FOREIGN KEY(`dictionaryId`) REFERENCES `Dictionary`(`dictId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
225 | "fields": [
226 | {
227 | "fieldPath": "tagId",
228 | "columnName": "tagId",
229 | "affinity": "INTEGER",
230 | "notNull": true
231 | },
232 | {
233 | "fieldPath": "dictionaryId",
234 | "columnName": "dictionaryId",
235 | "affinity": "INTEGER",
236 | "notNull": true
237 | },
238 | {
239 | "fieldPath": "name",
240 | "columnName": "name",
241 | "affinity": "TEXT",
242 | "notNull": true
243 | },
244 | {
245 | "fieldPath": "notes",
246 | "columnName": "notes",
247 | "affinity": "TEXT",
248 | "notNull": true
249 | }
250 | ],
251 | "primaryKey": {
252 | "autoGenerate": true,
253 | "columnNames": [
254 | "tagId"
255 | ]
256 | },
257 | "indices": [
258 | {
259 | "name": "idx_dictionary_tag_ref",
260 | "unique": false,
261 | "columnNames": [
262 | "dictionaryId"
263 | ],
264 | "orders": [],
265 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_dictionary_tag_ref` ON `${TABLE_NAME}` (`dictionaryId`)"
266 | }
267 | ],
268 | "foreignKeys": [
269 | {
270 | "table": "Dictionary",
271 | "onDelete": "CASCADE",
272 | "onUpdate": "NO ACTION",
273 | "columns": [
274 | "dictionaryId"
275 | ],
276 | "referencedColumns": [
277 | "dictId"
278 | ]
279 | }
280 | ]
281 | },
282 | {
283 | "tableName": "Dictionary",
284 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dictId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL)",
285 | "fields": [
286 | {
287 | "fieldPath": "dictId",
288 | "columnName": "dictId",
289 | "affinity": "INTEGER",
290 | "notNull": true
291 | },
292 | {
293 | "fieldPath": "name",
294 | "columnName": "name",
295 | "affinity": "TEXT",
296 | "notNull": true
297 | },
298 | {
299 | "fieldPath": "order",
300 | "columnName": "order",
301 | "affinity": "INTEGER",
302 | "notNull": true
303 | }
304 | ],
305 | "primaryKey": {
306 | "autoGenerate": true,
307 | "columnNames": [
308 | "dictId"
309 | ]
310 | },
311 | "indices": [],
312 | "foreignKeys": []
313 | }
314 | ],
315 | "views": [],
316 | "setupQueries": [
317 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
318 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b50e605b20426500c9b65e6a5fddc34c')"
319 | ]
320 | }
321 | }
--------------------------------------------------------------------------------
/app/schemas/com.kamui.rin.db.AppDatabase/4.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 4,
5 | "identityHash": "b50e605b20426500c9b65e6a5fddc34c",
6 | "entities": [
7 | {
8 | "tableName": "PitchAccent",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pitchId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL, `pitch` TEXT NOT NULL)",
10 | "fields": [
11 | {
12 | "fieldPath": "pitchId",
13 | "columnName": "pitchId",
14 | "affinity": "INTEGER",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "kanji",
19 | "columnName": "kanji",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "pitch",
25 | "columnName": "pitch",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | }
29 | ],
30 | "primaryKey": {
31 | "autoGenerate": true,
32 | "columnNames": [
33 | "pitchId"
34 | ]
35 | },
36 | "indices": [],
37 | "foreignKeys": []
38 | },
39 | {
40 | "tableName": "Frequency",
41 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`freqId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL, `frequency` INTEGER NOT NULL)",
42 | "fields": [
43 | {
44 | "fieldPath": "freqId",
45 | "columnName": "freqId",
46 | "affinity": "INTEGER",
47 | "notNull": true
48 | },
49 | {
50 | "fieldPath": "kanji",
51 | "columnName": "kanji",
52 | "affinity": "TEXT",
53 | "notNull": true
54 | },
55 | {
56 | "fieldPath": "frequency",
57 | "columnName": "frequency",
58 | "affinity": "INTEGER",
59 | "notNull": true
60 | }
61 | ],
62 | "primaryKey": {
63 | "autoGenerate": true,
64 | "columnNames": [
65 | "freqId"
66 | ]
67 | },
68 | "indices": [],
69 | "foreignKeys": []
70 | },
71 | {
72 | "tableName": "DictEntry",
73 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`entryId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL, `meaning` TEXT NOT NULL, `reading` TEXT NOT NULL, `dictionaryId` INTEGER NOT NULL, `pitchAccent` TEXT, `freq` INTEGER, FOREIGN KEY(`dictionaryId`) REFERENCES `Dictionary`(`dictId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
74 | "fields": [
75 | {
76 | "fieldPath": "entryId",
77 | "columnName": "entryId",
78 | "affinity": "INTEGER",
79 | "notNull": true
80 | },
81 | {
82 | "fieldPath": "kanji",
83 | "columnName": "kanji",
84 | "affinity": "TEXT",
85 | "notNull": true
86 | },
87 | {
88 | "fieldPath": "meaning",
89 | "columnName": "meaning",
90 | "affinity": "TEXT",
91 | "notNull": true
92 | },
93 | {
94 | "fieldPath": "reading",
95 | "columnName": "reading",
96 | "affinity": "TEXT",
97 | "notNull": true
98 | },
99 | {
100 | "fieldPath": "dictionaryId",
101 | "columnName": "dictionaryId",
102 | "affinity": "INTEGER",
103 | "notNull": true
104 | },
105 | {
106 | "fieldPath": "pitchAccent",
107 | "columnName": "pitchAccent",
108 | "affinity": "TEXT",
109 | "notNull": false
110 | },
111 | {
112 | "fieldPath": "freq",
113 | "columnName": "freq",
114 | "affinity": "INTEGER",
115 | "notNull": false
116 | }
117 | ],
118 | "primaryKey": {
119 | "autoGenerate": true,
120 | "columnNames": [
121 | "entryId"
122 | ]
123 | },
124 | "indices": [
125 | {
126 | "name": "idx_word_reading",
127 | "unique": false,
128 | "columnNames": [
129 | "kanji",
130 | "reading"
131 | ],
132 | "orders": [],
133 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_word_reading` ON `${TABLE_NAME}` (`kanji`, `reading`)"
134 | },
135 | {
136 | "name": "idx_dictionary_entry_ref",
137 | "unique": false,
138 | "columnNames": [
139 | "dictionaryId"
140 | ],
141 | "orders": [],
142 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_dictionary_entry_ref` ON `${TABLE_NAME}` (`dictionaryId`)"
143 | }
144 | ],
145 | "foreignKeys": [
146 | {
147 | "table": "Dictionary",
148 | "onDelete": "CASCADE",
149 | "onUpdate": "NO ACTION",
150 | "columns": [
151 | "dictionaryId"
152 | ],
153 | "referencedColumns": [
154 | "dictId"
155 | ]
156 | }
157 | ]
158 | },
159 | {
160 | "tableName": "DictEntryTagCrossRef",
161 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`entryId` INTEGER NOT NULL, `tagId` INTEGER NOT NULL, PRIMARY KEY(`entryId`, `tagId`))",
162 | "fields": [
163 | {
164 | "fieldPath": "entryId",
165 | "columnName": "entryId",
166 | "affinity": "INTEGER",
167 | "notNull": true
168 | },
169 | {
170 | "fieldPath": "tagId",
171 | "columnName": "tagId",
172 | "affinity": "INTEGER",
173 | "notNull": true
174 | }
175 | ],
176 | "primaryKey": {
177 | "autoGenerate": false,
178 | "columnNames": [
179 | "entryId",
180 | "tagId"
181 | ]
182 | },
183 | "indices": [
184 | {
185 | "name": "index_DictEntryTagCrossRef_tagId",
186 | "unique": false,
187 | "columnNames": [
188 | "tagId"
189 | ],
190 | "orders": [],
191 | "createSql": "CREATE INDEX IF NOT EXISTS `index_DictEntryTagCrossRef_tagId` ON `${TABLE_NAME}` (`tagId`)"
192 | }
193 | ],
194 | "foreignKeys": []
195 | },
196 | {
197 | "tableName": "SavedWord",
198 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`savedWordId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `kanji` TEXT NOT NULL)",
199 | "fields": [
200 | {
201 | "fieldPath": "savedWordId",
202 | "columnName": "savedWordId",
203 | "affinity": "INTEGER",
204 | "notNull": true
205 | },
206 | {
207 | "fieldPath": "kanji",
208 | "columnName": "kanji",
209 | "affinity": "TEXT",
210 | "notNull": true
211 | }
212 | ],
213 | "primaryKey": {
214 | "autoGenerate": true,
215 | "columnNames": [
216 | "savedWordId"
217 | ]
218 | },
219 | "indices": [],
220 | "foreignKeys": []
221 | },
222 | {
223 | "tableName": "Tag",
224 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dictionaryId` INTEGER NOT NULL, `name` TEXT NOT NULL, `notes` TEXT NOT NULL, FOREIGN KEY(`dictionaryId`) REFERENCES `Dictionary`(`dictId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
225 | "fields": [
226 | {
227 | "fieldPath": "tagId",
228 | "columnName": "tagId",
229 | "affinity": "INTEGER",
230 | "notNull": true
231 | },
232 | {
233 | "fieldPath": "dictionaryId",
234 | "columnName": "dictionaryId",
235 | "affinity": "INTEGER",
236 | "notNull": true
237 | },
238 | {
239 | "fieldPath": "name",
240 | "columnName": "name",
241 | "affinity": "TEXT",
242 | "notNull": true
243 | },
244 | {
245 | "fieldPath": "notes",
246 | "columnName": "notes",
247 | "affinity": "TEXT",
248 | "notNull": true
249 | }
250 | ],
251 | "primaryKey": {
252 | "autoGenerate": true,
253 | "columnNames": [
254 | "tagId"
255 | ]
256 | },
257 | "indices": [
258 | {
259 | "name": "idx_dictionary_tag_ref",
260 | "unique": false,
261 | "columnNames": [
262 | "dictionaryId"
263 | ],
264 | "orders": [],
265 | "createSql": "CREATE INDEX IF NOT EXISTS `idx_dictionary_tag_ref` ON `${TABLE_NAME}` (`dictionaryId`)"
266 | }
267 | ],
268 | "foreignKeys": [
269 | {
270 | "table": "Dictionary",
271 | "onDelete": "CASCADE",
272 | "onUpdate": "NO ACTION",
273 | "columns": [
274 | "dictionaryId"
275 | ],
276 | "referencedColumns": [
277 | "dictId"
278 | ]
279 | }
280 | ]
281 | },
282 | {
283 | "tableName": "Dictionary",
284 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dictId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL)",
285 | "fields": [
286 | {
287 | "fieldPath": "dictId",
288 | "columnName": "dictId",
289 | "affinity": "INTEGER",
290 | "notNull": true
291 | },
292 | {
293 | "fieldPath": "name",
294 | "columnName": "name",
295 | "affinity": "TEXT",
296 | "notNull": true
297 | },
298 | {
299 | "fieldPath": "order",
300 | "columnName": "order",
301 | "affinity": "INTEGER",
302 | "notNull": true
303 | }
304 | ],
305 | "primaryKey": {
306 | "autoGenerate": true,
307 | "columnNames": [
308 | "dictId"
309 | ]
310 | },
311 | "indices": [],
312 | "foreignKeys": []
313 | }
314 | ],
315 | "views": [],
316 | "setupQueries": [
317 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
318 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b50e605b20426500c9b65e6a5fddc34c')"
319 | ]
320 | }
321 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/kamui/rin/ui/fragment/ManageDictsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.kamui.rin.ui.fragment
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import android.widget.EditText
11 | import android.widget.PopupMenu
12 | import android.widget.Toast
13 | import androidx.activity.result.ActivityResultLauncher
14 | import androidx.activity.result.contract.ActivityResultContracts
15 | import androidx.appcompat.app.AppCompatActivity
16 | import androidx.fragment.app.viewModels
17 | import androidx.lifecycle.Lifecycle
18 | import androidx.lifecycle.LifecycleOwner
19 | import androidx.lifecycle.Observer
20 | import androidx.lifecycle.ViewModel
21 | import androidx.lifecycle.ViewModelProvider
22 | import androidx.lifecycle.lifecycleScope
23 | import androidx.lifecycle.repeatOnLifecycle
24 | import androidx.lifecycle.viewModelScope
25 | import androidx.preference.PreferenceManager
26 | import androidx.recyclerview.widget.DividerItemDecoration
27 | import androidx.recyclerview.widget.LinearLayoutManager
28 | import androidx.recyclerview.widget.RecyclerView
29 | import androidx.work.OneTimeWorkRequestBuilder
30 | import androidx.work.WorkInfo
31 | import androidx.work.WorkManager
32 | import androidx.work.WorkRequest
33 | import androidx.work.workDataOf
34 | import com.kamui.rin.R
35 | import com.kamui.rin.Settings
36 | import com.kamui.rin.databinding.DictionaryBinding
37 | import com.kamui.rin.databinding.FragmentManageDictsBinding
38 | import com.kamui.rin.db.AppDatabase
39 | import com.kamui.rin.db.model.Dictionary
40 | import com.kamui.rin.dict.worker.DeleteDictionaryWorker
41 | import com.kamui.rin.dict.worker.ImportDictionaryWorker
42 | import kotlinx.coroutines.Dispatchers
43 | import kotlinx.coroutines.flow.MutableStateFlow
44 | import kotlinx.coroutines.flow.StateFlow
45 | import kotlinx.coroutines.flow.asStateFlow
46 | import kotlinx.coroutines.flow.update
47 | import kotlinx.coroutines.launch
48 |
49 | data class ManageDictSettingsState(
50 | val dictionaries: List = listOf()
51 | )
52 |
53 | class ManageDictSettingsViewModel(private val context: Context) : ViewModel() {
54 | private val _uiState = MutableStateFlow(ManageDictSettingsState())
55 | val uiState: StateFlow = _uiState.asStateFlow()
56 | private val dictDao = AppDatabase.buildDatabase(context).dictionaryDao()
57 |
58 | init {
59 | viewModelScope.launch(Dispatchers.IO) {
60 | _uiState.update { currentState ->
61 | currentState.copy(
62 | dictionaries = dictDao.getAllDictionaries().sortedDescending()
63 | )
64 | }
65 | }
66 | }
67 |
68 | fun delete(dict: Dictionary, lifecycleOwner: LifecycleOwner) {
69 | val deleteWork: WorkRequest =
70 | OneTimeWorkRequestBuilder().setInputData(
71 | workDataOf(
72 | "DICT_ID" to dict.dictId
73 | )
74 | ).build()
75 | WorkManager.getInstance(context).enqueue(deleteWork)
76 |
77 | WorkManager.getInstance(context).getWorkInfoByIdLiveData(deleteWork.id)
78 | .observe(lifecycleOwner) { result: WorkInfo ->
79 | if (result.state == WorkInfo.State.SUCCEEDED) {
80 | viewModelScope.launch(Dispatchers.IO) {
81 | _uiState.update { currentState ->
82 | val newDictionaries =
83 | currentState.dictionaries.filter { it.dictId != dict.dictId }
84 | currentState.copy(
85 | dictionaries = newDictionaries
86 | )
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | fun import(uri: Uri, lifecycleOwner: LifecycleOwner) {
94 | val importWork: WorkRequest =
95 | OneTimeWorkRequestBuilder().setInputData(
96 | workDataOf(
97 | "URI" to uri.toString()
98 | )
99 | ).build()
100 | WorkManager.getInstance(context).enqueue(importWork)
101 |
102 | WorkManager.getInstance(context).getWorkInfoByIdLiveData(importWork.id)
103 | .observe(lifecycleOwner, Observer { result: WorkInfo ->
104 | if (result.state == WorkInfo.State.SUCCEEDED) {
105 | val data = result.outputData
106 | val dictId = data.getLong("DICT_ID", -1)
107 | val dictTitle = data.getString("DICT_TITLE") ?: "Untitled Dictionary"
108 | if (dictId.toInt() != -1) {
109 | val dict = Dictionary(dictId, dictTitle)
110 | viewModelScope.launch(Dispatchers.IO) {
111 | _uiState.update { currentState ->
112 | currentState.copy(
113 | dictionaries = currentState.dictionaries + listOfNotNull(
114 | dict
115 | )
116 | )
117 | }
118 | }
119 | }
120 | }
121 | })
122 | }
123 |
124 | fun setOrder(dict: Dictionary, order: Int) {
125 | val newDictionary = dict.copy(order = order)
126 | viewModelScope.launch(Dispatchers.IO) {
127 | dictDao.updateDictionary(newDictionary)
128 | _uiState.update { currentState ->
129 | currentState.copy(
130 | dictionaries = currentState.dictionaries.map {
131 | if (it.dictId == dict.dictId) newDictionary
132 | else it
133 | }.sortedDescending()
134 | )
135 | }
136 | }
137 | }
138 |
139 | }
140 |
141 | class ManageDictSettingsViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
142 | override fun create(modelClass: Class): T {
143 | return ManageDictSettingsViewModel(context) as T
144 | }
145 | }
146 |
147 | class ManageDictsFragment : androidx.fragment.app.Fragment() {
148 | private var _binding: FragmentManageDictsBinding? = null
149 | private val binding get() = _binding!!
150 |
151 | private val viewModel: ManageDictSettingsViewModel by viewModels {
152 | ManageDictSettingsViewModelFactory(requireContext())
153 | }
154 |
155 | private val addDictionaryActivityResultLauncher: ActivityResultLauncher =
156 | registerForActivityResult(
157 | ActivityResultContracts.StartActivityForResult()
158 | ) {
159 | if (it.resultCode == AppCompatActivity.RESULT_OK) {
160 | val uri = it.data?.dataString
161 | if (uri != null) {
162 | Toast.makeText(activity, "Starting dictionary import", Toast.LENGTH_LONG).show()
163 | viewModel.import(Uri.parse(uri), viewLifecycleOwner)
164 | }
165 | }
166 | }
167 |
168 | override fun onCreateView(
169 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
170 | ): View {
171 | viewLifecycleOwner.lifecycleScope.launch {
172 | repeatOnLifecycle(Lifecycle.State.STARTED) {
173 | viewModel.uiState.collect {
174 | binding.dictRecyclerList.adapter = Adapter()
175 | }
176 | }
177 | }
178 |
179 | _binding = FragmentManageDictsBinding.inflate(inflater, container, false)
180 | binding.dictRecyclerList.addItemDecoration(
181 | DividerItemDecoration(
182 | context, DividerItemDecoration.VERTICAL
183 | )
184 | )
185 | binding.dictRecyclerList.layoutManager = LinearLayoutManager(context)
186 | binding.importButton.setOnClickListener { addDictionary() }
187 | return binding.root
188 | }
189 |
190 | private fun addDictionary() {
191 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).addCategory(Intent.CATEGORY_OPENABLE)
192 | .setType("application/zip")
193 | .setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
194 | addDictionaryActivityResultLauncher.launch(intent)
195 | }
196 |
197 | inner class ViewHolder(val binding: DictionaryBinding) : RecyclerView.ViewHolder(binding.root)
198 |
199 | inner class Adapter : RecyclerView.Adapter() {
200 | override fun onCreateViewHolder(
201 | parent: ViewGroup, viewType: Int
202 | ): ManageDictsFragment.ViewHolder {
203 | val binding =
204 | DictionaryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
205 | return ViewHolder(binding)
206 | }
207 |
208 | override fun getItemCount(): Int {
209 | return viewModel.uiState.value.dictionaries.size
210 | }
211 |
212 | private fun createPopupMenu(holder: ViewHolder, dict: Dictionary): PopupMenu {
213 | val popup = PopupMenu(requireContext(), holder.binding.moreActionsButton)
214 | popup.setOnMenuItemClickListener { item ->
215 | when (item.itemId) {
216 | R.id.delete -> {
217 | viewModel.delete(dict, viewLifecycleOwner)
218 | Toast.makeText(activity, "Deleting ${dict.name}", Toast.LENGTH_LONG).show()
219 | }
220 |
221 | R.id.setOrder -> {
222 | android.app.AlertDialog.Builder(requireContext()).apply {
223 | val view = layoutInflater.inflate(R.layout.dict_priority_dialog, null)
224 | setView(view)
225 | setTitle("Dictionary Priority")
226 | val input = view.findViewById(R.id.dictOrder)
227 | input.setText(dict.order.toString())
228 | setPositiveButton("OK") { _, _ ->
229 | val order = input.text.toString().toInt()
230 | viewModel.setOrder(dict, order)
231 | }
232 | setNegativeButton("Cancel") { _, _ -> }
233 | }.show()
234 | }
235 | }
236 | false
237 | }
238 | return popup
239 | }
240 |
241 | override fun onBindViewHolder(holder: ManageDictsFragment.ViewHolder, position: Int) {
242 | val settings = Settings(PreferenceManager.getDefaultSharedPreferences(requireContext()))
243 | val dict = viewModel.uiState.value.dictionaries[position]
244 | holder.binding.dictionaryName.text = dict.name
245 | holder.binding.order.text = dict.order.toString()
246 | holder.binding.toggleActive.isChecked = settings.isDictActive(dict.dictId)
247 |
248 | holder.binding.toggleActive.setOnCheckedChangeListener { _, isChecked ->
249 | val idString = dict.dictId.toString()
250 | val origSet = settings.disabledDictSet()
251 | val newSet = mutableSetOf()
252 | newSet.addAll(origSet)
253 | if (isChecked) {
254 | newSet.remove(idString)
255 | } else {
256 | newSet.add(idString)
257 | }
258 | settings.updateDisabledDicts(newSet)
259 | }
260 |
261 | val popup = createPopupMenu(holder, dict)
262 |
263 | val inflater = popup.menuInflater
264 | inflater.inflate(R.menu.dictionary_manage_actions, popup.menu)
265 | holder.binding.moreActionsButton.setOnClickListener {
266 | popup.show()
267 | }
268 |
269 | }
270 | }
271 | }
272 |
--------------------------------------------------------------------------------