├── app ├── .gitignore ├── src │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── java │ │ │ ├── data │ │ │ │ ├── architecture │ │ │ │ │ ├── IFactory.kt │ │ │ │ │ ├── ISearchProvider.kt │ │ │ │ │ ├── IRepository.kt │ │ │ │ │ └── SingletonHolder.kt │ │ │ │ ├── models │ │ │ │ │ ├── INote.kt │ │ │ │ │ ├── ITaggedItem.kt │ │ │ │ │ ├── TaggedItem.kt │ │ │ │ │ ├── Note.kt │ │ │ │ │ ├── DictionaryEntry.kt │ │ │ │ │ ├── IVocabulary.kt │ │ │ │ │ ├── IDefinition.kt │ │ │ │ │ ├── Definition.kt │ │ │ │ │ ├── IDictionaryEntry.kt │ │ │ │ │ └── Vocabulary.kt │ │ │ │ ├── anki │ │ │ │ │ ├── CardFormat.kt │ │ │ │ │ ├── WanicchouAnkiEntry.kt │ │ │ │ │ ├── IAnkiDroidConfigIdentifierStorage.kt │ │ │ │ │ ├── IAnkiDroidConfig.kt │ │ │ │ │ ├── IAnkiDroidApi.kt │ │ │ │ │ ├── AnkiDroidApi.kt │ │ │ │ │ └── AnkiDroidHelper.kt │ │ │ │ ├── search │ │ │ │ │ ├── IDictionarySearchProvider.kt │ │ │ │ │ ├── SearchRequest.kt │ │ │ │ │ ├── jsoup │ │ │ │ │ │ ├── IJsoupDictionaryEntryFactory.kt │ │ │ │ │ │ └── JsoupDictionarySearchProvider.kt │ │ │ │ │ ├── IDictionarySource.kt │ │ │ │ │ └── DictionarySearchManager.kt │ │ │ │ ├── web │ │ │ │ │ ├── DictionarySearchProviderFactory.kt │ │ │ │ │ └── sanseido │ │ │ │ │ │ └── SanseidoSource.kt │ │ │ │ ├── enums │ │ │ │ │ ├── Dictionary.kt │ │ │ │ │ ├── Language.kt │ │ │ │ │ └── MatchType.kt │ │ │ │ └── lang │ │ │ │ │ └── JapaneseVocabulary.kt │ │ │ ├── com │ │ │ │ └── limegrass │ │ │ │ │ └── wanicchou │ │ │ │ │ ├── enums │ │ │ │ │ └── AutoDelete.kt │ │ │ │ │ ├── viewmodel │ │ │ │ │ ├── RelatedVocabularyViewModel.kt │ │ │ │ │ ├── TagViewModel.kt │ │ │ │ │ ├── DefinitionNoteViewModel.kt │ │ │ │ │ ├── VocabularyNoteViewModel.kt │ │ │ │ │ ├── ObservableViewModel.kt │ │ │ │ │ ├── DatabaseViewModel.kt │ │ │ │ │ └── DictionaryEntryViewModel.kt │ │ │ │ │ ├── util │ │ │ │ │ ├── ViewPagerExtensions.kt │ │ │ │ │ ├── WanicchouToast.kt │ │ │ │ │ ├── InputAlertDialogBuilder.kt │ │ │ │ │ └── WanicchouSearchManager.kt │ │ │ │ │ ├── SettingsActivity.kt │ │ │ │ │ ├── ui │ │ │ │ │ ├── adapter │ │ │ │ │ │ ├── ListPagerAdapter.kt │ │ │ │ │ │ ├── ListViewAdapter.kt │ │ │ │ │ │ ├── TextBlockRecyclerViewAdapter.kt │ │ │ │ │ │ ├── TextSpanRecyclerViewAdapter.kt │ │ │ │ │ │ └── SelectableAdapterDecorator.kt │ │ │ │ │ └── fragments │ │ │ │ │ │ ├── TextBlockFragment.kt │ │ │ │ │ │ ├── WordFragment.kt │ │ │ │ │ │ └── RelatedFragment.kt │ │ │ │ │ └── DatabaseActivity.kt │ │ │ └── room │ │ │ │ ├── dao │ │ │ │ ├── entity │ │ │ │ │ ├── LanguageDao.kt │ │ │ │ │ ├── DictionaryDao.kt │ │ │ │ │ ├── TagDao.kt │ │ │ │ │ ├── VocabularyTagDao.kt │ │ │ │ │ ├── DefinitionNoteDao.kt │ │ │ │ │ ├── VocabularyDao.kt │ │ │ │ │ ├── VocabularyNoteDao.kt │ │ │ │ │ └── DefinitionDao.kt │ │ │ │ ├── BaseDao.kt │ │ │ │ └── composite │ │ │ │ │ ├── VocabularyAndTagDao.kt │ │ │ │ │ └── DictionaryEntryDao.kt │ │ │ │ ├── dbo │ │ │ │ ├── entity │ │ │ │ │ ├── Tag.kt │ │ │ │ │ ├── Language.kt │ │ │ │ │ ├── Dictionary.kt │ │ │ │ │ ├── DefinitionNote.kt │ │ │ │ │ ├── VocabularyNote.kt │ │ │ │ │ ├── VocabularyTag.kt │ │ │ │ │ ├── Vocabulary.kt │ │ │ │ │ └── Definition.kt │ │ │ │ └── composite │ │ │ │ │ ├── DictionaryEntry.kt │ │ │ │ │ └── VocabularyAndTag.kt │ │ │ │ ├── database │ │ │ │ ├── Converters.kt │ │ │ │ ├── WanicchouDatabase.kt │ │ │ │ └── EnumLikeValueInsertDatabaseCallback.kt │ │ │ │ └── repository │ │ │ │ ├── DefinitionNoteRepository.kt │ │ │ │ ├── VocabularyNoteRepository.kt │ │ │ │ ├── VocabularyTagRepository.kt │ │ │ │ └── DictionaryEntryRepository.kt │ │ ├── res │ │ │ ├── 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 │ │ │ ├── values-jp │ │ │ │ ├── .strings.xml.un~ │ │ │ │ ├── auto_delete.xml │ │ │ │ ├── match_type.xml │ │ │ │ └── strings.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 │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── auto_delete.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── match_type.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── arrays.xml │ │ │ │ └── shared_preferences.xml │ │ │ ├── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ ├── pref_search.xml │ │ │ │ └── searchable.xml │ │ │ ├── drawable │ │ │ │ ├── ic_circle_outline.xml │ │ │ │ ├── tab_selector.xml │ │ │ │ ├── oval.xml │ │ │ │ ├── tab_indicator_selected.xml │ │ │ │ ├── border.xml │ │ │ │ ├── tab_indicator_default.xml │ │ │ │ ├── ic_send_white_24dp.xml │ │ │ │ ├── ic_delete_white_24dp.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── ic_add_circle_filled_white_24dp.xml │ │ │ │ └── ic_search_white_24dp.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── activity_settings.xml │ │ │ │ ├── rv_item_text_span.xml │ │ │ │ ├── activity_database.xml │ │ │ │ ├── rv_item_text_block.xml │ │ │ │ ├── floating_action_button.xml │ │ │ │ ├── dialog_text_input.xml │ │ │ │ ├── fragment_definition.xml │ │ │ │ ├── fragment_related.xml │ │ │ │ ├── fragment_word.xml │ │ │ │ ├── fragment_text_list.xml │ │ │ │ ├── activity_wanicchou.xml │ │ │ │ └── fragment_tab_switch.xml │ │ │ ├── menu │ │ │ │ ├── word_list_menu.xml │ │ │ │ └── search_menu.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ └── AndroidManifest.xml │ ├── androidTest │ │ └── java │ │ │ ├── util │ │ │ └── LiveDataExtensions.kt │ │ │ └── room │ │ │ └── dao │ │ │ ├── entity │ │ │ ├── TagDaoTest.kt │ │ │ ├── VocabularyTagDaoTest.kt │ │ │ ├── VocabularyNoteDaoTest.kt │ │ │ ├── DefinitionNoteDaoTest.kt │ │ │ ├── DefinitionDaoTest.kt │ │ │ └── VocabularyDaoTest.kt │ │ │ ├── AbstractDaoTest.kt │ │ │ └── composite │ │ │ └── VocabularyAndTagDaoTest.kt │ ├── test │ │ └── java │ │ │ ├── data │ │ │ ├── architecture │ │ │ │ └── SingletonHolderTest.kt │ │ │ ├── enums │ │ │ │ ├── DictionaryTest.kt │ │ │ │ ├── MatchTypeTest.kt │ │ │ │ └── LanguageTest.kt │ │ │ ├── lang │ │ │ │ └── JapaneseVocabularyTest.kt │ │ │ ├── web │ │ │ │ └── sanseido │ │ │ │ │ ├── SanseidoDictionaryEntryFactoryTest.kt │ │ │ │ │ └── SanseidoDictionarySourceTest.kt │ │ │ └── anki │ │ │ │ └── AnkiDroidConfigTest.kt │ │ │ └── room │ │ │ └── database │ │ │ └── ConvertersTest.kt │ └── debug │ │ └── google-services.json ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── .gitattributes ├── docs ├── app-image.png └── settings.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── gradle.properties ├── README.jp.md ├── README.md ├── gradlew.bat └── WanicchouDB.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /docs/app-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/docs/app-image.png -------------------------------------------------------------------------------- /docs/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/docs/settings.png -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/java/data/architecture/IFactory.kt: -------------------------------------------------------------------------------- 1 | package data.architecture 2 | 3 | interface IFactory { 4 | fun get() : T 5 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-jp/.strings.xml.un~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/app/src/main/res/values-jp/.strings.xml.un~ -------------------------------------------------------------------------------- /app/src/main/java/data/models/INote.kt: -------------------------------------------------------------------------------- 1 | package data.models 2 | 3 | interface INote { 4 | val noteText : String 5 | val topic : T 6 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/java/data/models/ITaggedItem.kt: -------------------------------------------------------------------------------- 1 | package data.models 2 | 3 | interface ITaggedItem { 4 | val tag : String 5 | val item : T 6 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/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/Limegrass/Wanicchou/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/Limegrass/Wanicchou/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/Limegrass/Wanicchou/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/java/data/models/TaggedItem.kt: -------------------------------------------------------------------------------- 1 | package data.models 2 | 3 | class TaggedItem(override val item : T, override val tag: String) 4 | : ITaggedItem -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/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/Limegrass/Wanicchou/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/Limegrass/Wanicchou/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/java/data/architecture/ISearchProvider.kt: -------------------------------------------------------------------------------- 1 | package data.architecture 2 | 3 | interface ISearchProvider { 4 | suspend fun search(request : R) : T 5 | } -------------------------------------------------------------------------------- /app/src/main/java/data/models/Note.kt: -------------------------------------------------------------------------------- 1 | package data.models 2 | 3 | class Note(override val topic: T, 4 | override val noteText: String) 5 | : INote -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Limegrass/Wanicchou/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/Limegrass/Wanicchou/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #26A69A 4 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/enums/AutoDelete.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.enums 2 | 3 | enum class AutoDelete { 4 | NEVER, 5 | ON_ANKI_IMPORT, 6 | ON_SEARCH, 7 | } -------------------------------------------------------------------------------- /app/src/main/java/data/anki/CardFormat.kt: -------------------------------------------------------------------------------- 1 | package data.anki 2 | 3 | class CardFormat (val formatName : String, 4 | val questionFormat : String, 5 | val answerFormat : String) -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/room/dao/entity/LanguageDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import androidx.room.Dao 4 | import room.dao.BaseDao 5 | import room.dbo.entity.Language 6 | 7 | @Dao 8 | interface LanguageDao : BaseDao -------------------------------------------------------------------------------- /app/src/main/java/room/dao/entity/DictionaryDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import androidx.room.Dao 4 | import room.dao.BaseDao 5 | import room.dbo.entity.Dictionary 6 | 7 | @Dao 8 | interface DictionaryDao : BaseDao -------------------------------------------------------------------------------- /app/src/main/java/data/models/DictionaryEntry.kt: -------------------------------------------------------------------------------- 1 | package data.models 2 | 3 | class DictionaryEntry (override val vocabulary: IVocabulary, 4 | override val definitions: List = listOf()) : IDictionaryEntry -------------------------------------------------------------------------------- /app/src/main/java/data/models/IVocabulary.kt: -------------------------------------------------------------------------------- 1 | package data.models 2 | 3 | import data.enums.Language 4 | 5 | interface IVocabulary { 6 | val word : String 7 | val pronunciation : String 8 | val pitch : String 9 | val language : Language 10 | } -------------------------------------------------------------------------------- /app/src/main/java/data/architecture/IRepository.kt: -------------------------------------------------------------------------------- 1 | package data.architecture 2 | 3 | interface IRepository : ISearchProvider, R> { 4 | suspend fun insert(entity: T) 5 | suspend fun update(original: T, updated: T) 6 | suspend fun delete(entity: T) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/data/models/IDefinition.kt: -------------------------------------------------------------------------------- 1 | package data.models 2 | 3 | import data.enums.Dictionary 4 | import data.enums.Language 5 | 6 | interface IDefinition { 7 | val definitionText : String 8 | val dictionary : Dictionary 9 | val language : Language 10 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Aug 25 17:55:24 CDT 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/java/data/search/IDictionarySearchProvider.kt: -------------------------------------------------------------------------------- 1 | package data.search 2 | 3 | import data.models.IDictionaryEntry 4 | import data.architecture.ISearchProvider 5 | 6 | interface IDictionarySearchProvider 7 | : IDictionarySource, 8 | ISearchProvider, SearchRequest> 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.*/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | *.un 10 | *.swp 11 | *~ 12 | /app/release/* 13 | /app/.idea/* 14 | /app/local.properties 15 | /app/gradle/* 16 | /app/gradlew 17 | /app/gradlew.bat 18 | *.vim 19 | /app/src/release/* -------------------------------------------------------------------------------- /app/src/main/java/data/search/SearchRequest.kt: -------------------------------------------------------------------------------- 1 | package data.search 2 | 3 | import data.enums.* 4 | 5 | class SearchRequest(val searchTerm: String, 6 | val vocabularyLanguage: Language, 7 | val definitionLanguage: Language, 8 | val matchType : MatchType) -------------------------------------------------------------------------------- /app/src/main/res/xml/pref_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/data/anki/WanicchouAnkiEntry.kt: -------------------------------------------------------------------------------- 1 | package data.anki 2 | 3 | import data.models.IDefinition 4 | import data.models.IVocabulary 5 | 6 | class WanicchouAnkiEntry(val vocabulary : IVocabulary, 7 | val definition : IDefinition, 8 | val notes : List) -------------------------------------------------------------------------------- /app/src/main/java/data/models/Definition.kt: -------------------------------------------------------------------------------- 1 | package data.models 2 | 3 | import data.enums.Dictionary 4 | import data.enums.Language 5 | 6 | class Definition (override val definitionText: String, 7 | override val language: Language, 8 | override val dictionary: Dictionary) : IDefinition -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/viewmodel/RelatedVocabularyViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.viewmodel 2 | 3 | import data.models.IVocabulary 4 | 5 | class RelatedVocabularyViewModel 6 | : ObservableViewModel>(){ 7 | init{ 8 | value = listOf() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_circle_outline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/viewmodel/TagViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.viewmodel 2 | 3 | import data.models.ITaggedItem 4 | import data.models.IVocabulary 5 | 6 | class TagViewModel 7 | : ObservableViewModel>>(){ 8 | init { 9 | value = listOf() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/viewmodel/DefinitionNoteViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.viewmodel 2 | 3 | import data.models.IDefinition 4 | import data.models.INote 5 | 6 | class DefinitionNoteViewModel 7 | : ObservableViewModel>>(){ 8 | init{ 9 | value = listOf() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/viewmodel/VocabularyNoteViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.viewmodel 2 | 3 | import data.models.INote 4 | import data.models.IVocabulary 5 | 6 | class VocabularyNoteViewModel 7 | : ObservableViewModel>>(){ 8 | init { 9 | value = listOf() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-jp/auto_delete.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | しない 4 | 暗記ドロイドに送ると同時に 5 | 次の検索が成功すると同時に 6 | -------------------------------------------------------------------------------- /app/src/main/res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/oval.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_indicator_selected.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/border.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/auto_delete.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Never 4 | On Anki Import 5 | On Next Successful Search 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_indicator_default.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/data/anki/IAnkiDroidConfigIdentifierStorage.kt: -------------------------------------------------------------------------------- 1 | package data.anki 2 | 3 | interface IAnkiDroidConfigIdentifierStorage { 4 | fun getDeckID(deckName : String) : Long? 5 | fun addDeckID(deckName : String, deckID : Long) 6 | 7 | fun getModelID(modelName : String, minimumFieldCount : Int = 1) : Long? 8 | fun addModelID(modelName : String, modelID : Long) 9 | } -------------------------------------------------------------------------------- /app/src/main/java/room/dao/BaseDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao 2 | 3 | import androidx.room.* 4 | 5 | interface BaseDao { 6 | @Insert(onConflict = OnConflictStrategy.IGNORE) 7 | suspend fun insert(obj: T): Long 8 | 9 | @Update(onConflict = OnConflictStrategy.IGNORE) 10 | suspend fun update(obj: T) 11 | 12 | @Delete 13 | suspend fun delete(obj: T) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/room/dao/entity/TagDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import room.dao.BaseDao 6 | import room.dbo.entity.Tag 7 | 8 | @Dao 9 | interface TagDao : BaseDao { 10 | @Query(""" 11 | SELECT t.TagID 12 | FROM Tag t 13 | WHERE t.TagText = :tag""") 14 | fun getExistingTagID(tag: String) : Long? 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_send_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/room/dbo/entity/Tag.kt: -------------------------------------------------------------------------------- 1 | package room.dbo.entity 2 | import androidx.room.* 3 | 4 | @Entity(tableName = "Tag") 5 | data class Tag ( 6 | @ColumnInfo(name = "TagText") 7 | var tagText: String, 8 | 9 | @PrimaryKey(autoGenerate = true) 10 | @ColumnInfo(name = "TagID") 11 | var tagID: Long = 0){ 12 | override fun toString(): String { 13 | return tagText 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_circle_filled_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/data/search/jsoup/IJsoupDictionaryEntryFactory.kt: -------------------------------------------------------------------------------- 1 | package data.search.jsoup 2 | 3 | import data.models.IDictionaryEntry 4 | import data.enums.Language 5 | import org.jsoup.nodes.Element 6 | 7 | interface IJsoupDictionaryEntryFactory { 8 | fun getDictionaryEntries(element : Element, 9 | vocabularyLanguage: Language, 10 | definitionLanguage: Language) : List 11 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/rv_item_text_span.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_database.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/data/anki/IAnkiDroidConfig.kt: -------------------------------------------------------------------------------- 1 | package data.anki 2 | 3 | interface IAnkiDroidConfig { 4 | val deckName : String 5 | val modelName : String 6 | val fields : Array 7 | val cardFormats : List 8 | val frontSideKey : String 9 | val backSideKey : String 10 | val css : String 11 | val sortField : Int? 12 | fun mapToNoteFields(noteEntry : T) : Array 13 | fun mapFromNoteFields(fields : Array) : T 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/util/ViewPagerExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.util 2 | 3 | import androidx.viewpager.widget.ViewPager 4 | import com.limegrass.wanicchou.ui.adapter.ListPagerAdapter 5 | 6 | fun ViewPager.replaceListPagerAdapter(replacementAdapter : ListPagerAdapter){ 7 | val existingAdapter = this.adapter as ListPagerAdapter 8 | existingAdapter.clearFragments() 9 | this.removeAllViews() 10 | this.adapter = replacementAdapter 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/data/architecture/SingletonHolder.kt: -------------------------------------------------------------------------------- 1 | package data.architecture 2 | 3 | open class SingletonHolder (private val creator: (A) -> T) { 4 | @Volatile private var instance: T? = null 5 | // Double checked locking on the creator 6 | protected fun getInstance(arg: A) : T { 7 | instance = instance ?: synchronized(creator){ 8 | instance = instance ?: creator(arg) 9 | instance 10 | } 11 | return instance!! 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/data/search/IDictionarySource.kt: -------------------------------------------------------------------------------- 1 | package data.search 2 | 3 | import data.enums.Language 4 | import data.enums.MatchType 5 | import java.net.URL 6 | 7 | interface IDictionarySource { 8 | fun buildSearchQueryURL(searchRequest: SearchRequest) : URL 9 | val supportedMatchTypes : Set 10 | /** 11 | * A map of support languages from its source word language to a target definition language 12 | */ 13 | val supportedTranslations : Map> 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/room/dao/composite/VocabularyAndTagDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao.composite 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import androidx.room.Transaction 6 | import room.dbo.composite.VocabularyAndTag 7 | 8 | @Dao 9 | interface VocabularyAndTagDao { 10 | @Transaction 11 | @Query(""" 12 | SELECT * 13 | FROM VocabularyAndTag 14 | WHERE VocabularyID = :vocabularyID """) 15 | suspend fun getVocabularyAndTag(vocabularyID: Long) : List 16 | } -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/rv_item_text_block.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/util/WanicchouToast.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.util 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | 6 | var WanicchouToast : Toast? = null 7 | 8 | fun cancelSetAndShowWanicchouToast(context : Context, 9 | toastText : String, 10 | duration: Int) { 11 | WanicchouToast?.cancel() 12 | WanicchouToast = Toast.makeText(context, 13 | toastText, 14 | duration) 15 | WanicchouToast!!.show() 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/floating_action_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/room/dbo/composite/DictionaryEntry.kt: -------------------------------------------------------------------------------- 1 | package room.dbo.composite 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | import data.models.IDictionaryEntry 6 | import room.dbo.entity.Definition 7 | import room.dbo.entity.Vocabulary 8 | 9 | data class DictionaryEntry ( 10 | @Embedded 11 | override var vocabulary : Vocabulary, 12 | @Relation(entity = Definition::class, 13 | parentColumn = "VocabularyID", 14 | entityColumn = "VocabularyID") 15 | override var definitions : List) : IDictionaryEntry 16 | -------------------------------------------------------------------------------- /app/src/main/java/data/models/IDictionaryEntry.kt: -------------------------------------------------------------------------------- 1 | package data.models 2 | 3 | interface IDictionaryEntry { 4 | val vocabulary : IVocabulary 5 | val definitions : List 6 | } 7 | // Can change to List<>? if performance becomes of concern. 8 | // Benefits of list: I don't have to allow null definitions, it makes sense to return empty lists 9 | // No repeated vocabulary words, don't need to order by anything weird 10 | // makes more sense in general when UI controls change. 11 | // Benefits of flat: I can target an individual definition easier, which makes shared preferences simpler -------------------------------------------------------------------------------- /app/src/main/java/room/dbo/entity/Language.kt: -------------------------------------------------------------------------------- 1 | package room.dbo.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "Language") 8 | data class Language ( 9 | @ColumnInfo(name = "LanguageName") 10 | var languageName : String, 11 | 12 | @ColumnInfo(name = "LanguageCode") 13 | var languageCode : String, 14 | 15 | @PrimaryKey(autoGenerate = true) 16 | @ColumnInfo(name = "LanguageID") 17 | var languageID : Long = 0) { 18 | override fun toString(): String { 19 | return languageName 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/util/LiveDataExtensions.kt: -------------------------------------------------------------------------------- 1 | package util 2 | import androidx.lifecycle.LiveData 3 | import androidx.lifecycle.Observer 4 | import java.util.concurrent.CountDownLatch 5 | import java.util.concurrent.TimeUnit 6 | 7 | 8 | @Throws(InterruptedException::class) 9 | fun LiveData.awaitValue(): T { 10 | var data : T? = null 11 | val latch = CountDownLatch(1) 12 | val observer = object : Observer { 13 | override fun onChanged(o: T) { 14 | data = o 15 | latch.countDown() 16 | removeObserver(this) 17 | } 18 | } 19 | observeForever(observer) 20 | latch.await(2, TimeUnit.SECONDS) 21 | return data!! 22 | } -------------------------------------------------------------------------------- /app/src/main/res/values-jp/match_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 完全一致 4 | 前方一致 5 | 後方一致 6 | 部分一致 7 | ワイルドカード付き完全一致〈 _, % 〉 8 | 定義部分一致 9 | 全文検索 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/word_list_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement, new 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /app/src/main/java/room/dao/entity/VocabularyTagDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import room.dao.BaseDao 6 | import room.dbo.entity.VocabularyTag 7 | 8 | @Dao 9 | interface VocabularyTagDao : BaseDao { 10 | @Query(""" 11 | DELETE 12 | FROM VocabularyTag 13 | WHERE VocabularyTagID = ( 14 | SELECT vt.VocabularyTagID 15 | FROM VocabularyTag vt 16 | JOIN Tag t 17 | ON t.TagID = vt.TagID 18 | WHERE t.TagText = :tagText 19 | AND vt.VocabularyID = :vocabularyID ) 20 | """) 21 | suspend fun deleteVocabularyTag(tagText: String, vocabularyID: Long) : Int 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou 2 | 3 | import androidx.core.app.NavUtils 4 | import androidx.appcompat.app.AppCompatActivity 5 | import android.os.Bundle 6 | import android.view.MenuItem 7 | 8 | class SettingsActivity : AppCompatActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.activity_settings) 12 | } 13 | 14 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 15 | when (item.itemId){ 16 | android.R.id.home -> NavUtils.navigateUpFromSameTask(this) 17 | } 18 | return super.onOptionsItemSelected(item) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/test/java/data/architecture/SingletonHolderTest.kt: -------------------------------------------------------------------------------- 1 | package data.architecture 2 | 3 | import org.junit.Test 4 | import kotlin.test.* 5 | 6 | class SingletonHolderTest { 7 | @Test 8 | fun `getInstance returns the same object`() { 9 | val firstInstance = TestSingleton("Test") 10 | val secondInstance = TestSingleton("NotATest") 11 | assertSame(firstInstance, secondInstance) 12 | } 13 | 14 | 15 | private class TestSingleton { 16 | companion object : SingletonHolder({ 17 | TestSingleton() 18 | }) { 19 | operator fun invoke(value : String) : TestSingleton { 20 | return getInstance(value) 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/viewmodel/ObservableViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.viewmodel 2 | 3 | import androidx.lifecycle.* 4 | 5 | abstract class ObservableViewModel : ViewModel(){ 6 | //TODO: MediatorLiveData with source setting. 7 | private val liveData : MutableLiveData = MutableLiveData() 8 | 9 | open var value : T? 10 | get() { 11 | return liveData.value 12 | } 13 | set(value){ 14 | liveData.value = value 15 | } 16 | 17 | fun setObserver(lifecycleOwner: LifecycleOwner, 18 | action : () -> Unit){ 19 | val observer = Observer{ 20 | action() 21 | } 22 | liveData.observe(lifecycleOwner, observer) 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_text_input.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/match_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exact Word Match 4 | Word Starts With 5 | Word Ends With 6 | Word Contains 7 | Word Match with Wildcard characters (_, %) 8 | Definition Contains 9 | Word or Definition Contains 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/viewmodel/DatabaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.viewmodel 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.LiveData 6 | import room.database.WanicchouDatabase 7 | import room.dbo.entity.Vocabulary 8 | import kotlinx.coroutines.runBlocking 9 | 10 | class DatabaseViewModel(application: Application) 11 | : AndroidViewModel(application){ 12 | val vocabularyList : LiveData> = runBlocking { 13 | // This is hot garbage but I don't care enough for this screen right now 14 | // Need to redo the entire Database Activity Screen 15 | val database = WanicchouDatabase(application) 16 | database.vocabularyDao().getAllWithDefinition() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/room/dbo/entity/Dictionary.kt: -------------------------------------------------------------------------------- 1 | package room.dbo.entity 2 | 3 | import android.os.Parcelable 4 | import androidx.room.* 5 | import kotlinx.android.parcel.Parcelize 6 | 7 | @Parcelize 8 | @Entity(tableName = "Dictionary") 9 | data class Dictionary ( 10 | @ColumnInfo(name = "DictionaryName") 11 | var dictionaryName: String, 12 | 13 | @ColumnInfo(name = "DefaultVocabularyLanguageID") 14 | var defaultVocabularyLanguage: data.enums.Language, 15 | 16 | @ColumnInfo(name = "DefaultDefinitionLanguageID") 17 | var defaultDefinitionLanguage: data.enums.Language, 18 | 19 | @PrimaryKey(autoGenerate = true) 20 | @ColumnInfo(name = "DictionaryID") 21 | var dictionaryID: Long = 0 22 | ) : Parcelable { 23 | override fun toString(): String { 24 | return dictionaryName 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/debug/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "86828245787", 4 | "firebase_url": "https://wanicchou.firebaseio.com", 5 | "project_id": "wanicchou", 6 | "storage_bucket": "wanicchou.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:86828245787:android:b20635909a5b84cb", 12 | "android_client_info": { 13 | "package_name": "com.limegrass.wanicchou" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "nottherealclientid.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "notarealkey" 25 | } 26 | ] 27 | } 28 | ], 29 | "configuration_version": "1" 30 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/java/data/web/DictionarySearchProviderFactory.kt: -------------------------------------------------------------------------------- 1 | package data.web 2 | 3 | import data.search.IDictionarySearchProvider 4 | import data.search.jsoup.JsoupDictionarySearchProvider 5 | import data.architecture.IFactory 6 | import data.enums.Dictionary 7 | import data.web.sanseido.SanseidoDictionaryEntryFactory 8 | import data.web.sanseido.SanseidoSource 9 | 10 | class DictionarySearchProviderFactory(private val dictionary : Dictionary) 11 | : IFactory { 12 | override fun get(): IDictionarySearchProvider { 13 | return when (dictionary) { 14 | Dictionary.SANSEIDO -> { 15 | val factory = SanseidoDictionaryEntryFactory() 16 | val source = SanseidoSource() 17 | JsoupDictionarySearchProvider(source, factory) 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3E50B4 4 | #000000 5 | #000000 6 | #FFFFFF 7 | #247E80 8 | #209999FF 9 | 10 | 11 | #101010 12 | #202020 13 | 14 | #3A3A3A 15 | 16 | #FFFFFF 17 | #D8D8D8CC 18 | #530879 19 | #247E80 20 | 21 | 22 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.enableJetifier=true 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, new 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - OS: [e.g. iOS8.1] 29 | - Browser [e.g. stock browser, safari] 30 | - Version [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /app/src/main/java/room/dbo/entity/DefinitionNote.kt: -------------------------------------------------------------------------------- 1 | package room.dbo.entity 2 | 3 | import androidx.room.* 4 | import androidx.room.ForeignKey.CASCADE 5 | 6 | @Entity(tableName = "DefinitionNote", 7 | foreignKeys = [ 8 | ForeignKey( 9 | entity = Definition::class, 10 | parentColumns = ["DefinitionID"], 11 | childColumns = ["DefinitionID"], 12 | onDelete = CASCADE)] 13 | ) 14 | data class DefinitionNote ( 15 | @ColumnInfo(name = "NoteText") 16 | var noteText: String = "", 17 | 18 | @ColumnInfo(name = "DefinitionID", index = true) 19 | var definitionID: Long, 20 | 21 | @PrimaryKey(autoGenerate = true) 22 | @ColumnInfo(name = "DefinitionNoteID") 23 | var definitionNoteID: Long = 0 ) { 24 | override fun toString(): String { 25 | return noteText 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_definition.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 16 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_related.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 18 | 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/room/dao/entity/TagDaoTest.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import org.junit.Test 5 | import room.dao.AbstractDaoTest 6 | import room.dbo.entity.Tag 7 | import kotlin.test.assertNotNull 8 | import kotlin.test.assertNull 9 | 10 | class TagDaoTest : AbstractDaoTest() { 11 | @Test 12 | fun getExistingTagID_Exists_ReturnsID(){ 13 | val tagText = "Test" 14 | runBlocking { 15 | db.tagDao().insert(Tag(tagText)) 16 | } 17 | val tagID = db.tagDao().getExistingTagID(tagText) 18 | assertNotNull(tagID) 19 | } 20 | 21 | @Test 22 | fun getExistingTagID_TagTextMismatch_ReturnsNull(){ 23 | val tagText = "Test" 24 | runBlocking { 25 | db.tagDao().insert(Tag(tagText)) 26 | } 27 | val tagID = db.tagDao().getExistingTagID(tagText+"Whatever") 28 | assertNull(tagID) 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/room/database/Converters.kt: -------------------------------------------------------------------------------- 1 | package room.database 2 | 3 | import androidx.room.TypeConverter 4 | 5 | import room.dbo.entity.Vocabulary 6 | 7 | class Converters { 8 | @TypeConverter 9 | fun fromVocabularyEntity(vocabulary: Vocabulary?): Long? { 10 | return vocabulary?.vocabularyID 11 | } 12 | 13 | @TypeConverter 14 | fun fromLanguage(language: data.enums.Language): Long { 15 | return language.languageID 16 | } 17 | @TypeConverter 18 | fun toLanguage(languageID : Long): data.enums.Language { 19 | return data.enums.Language.getLanguage(languageID) 20 | } 21 | 22 | @TypeConverter 23 | fun fromDictionary(dictionary: data.enums.Dictionary): Long { 24 | return dictionary.dictionaryID 25 | } 26 | @TypeConverter 27 | fun toDictionary(dictionaryID : Long): data.enums.Dictionary { 28 | return data.enums.Dictionary.getDictionary(dictionaryID) 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/viewmodel/DictionaryEntryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.viewmodel 2 | 3 | import data.models.IDictionaryEntry 4 | 5 | class DictionaryEntryViewModel 6 | : ObservableViewModel() { 7 | 8 | private var dictionaryEntries : List? = null 9 | var availableDictionaryEntries : List 10 | get() { 11 | return dictionaryEntries ?: listOf() 12 | } 13 | set(dictionaryEntries) { 14 | this.dictionaryEntries = dictionaryEntries 15 | dictionaryEntryIndex = 0 16 | value = dictionaryEntries.first{ 17 | dictionaryEntryIndex++ 18 | it.definitions.isNotEmpty() 19 | } 20 | } 21 | 22 | private var dictionaryEntryIndex = 0 23 | 24 | companion object { 25 | private val TAG : String = DictionaryEntryViewModel::class.java.simpleName 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/data/models/Vocabulary.kt: -------------------------------------------------------------------------------- 1 | package data.models 2 | 3 | import data.enums.Language 4 | 5 | class Vocabulary (override val word: String, 6 | override val pronunciation: String, 7 | override val pitch: String, 8 | override val language: Language) : IVocabulary { 9 | override fun toString(): String { 10 | return word 11 | } 12 | 13 | override fun equals(other: Any?): Boolean { 14 | if (other == null || other !is IVocabulary){ 15 | return false 16 | } 17 | return this.word == other.word 18 | && this.pronunciation == other.pronunciation 19 | && this.language == other.language 20 | && this.pitch == other.pitch 21 | } 22 | 23 | override fun hashCode(): Int { 24 | return word.hashCode() xor 25 | pronunciation.hashCode() xor 26 | language.hashCode() xor 27 | pitch.hashCode() 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/room/dao/entity/DefinitionNoteDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import room.dao.BaseDao 6 | import room.dbo.entity.DefinitionNote 7 | 8 | @Dao 9 | interface DefinitionNoteDao : BaseDao { 10 | @Query(""" 11 | SELECT dn.NoteText 12 | FROM DefinitionNote dn 13 | WHERE dn.DefinitionID = :definitionID """) 14 | suspend fun getNotesForDefinition(definitionID: Long) : List 15 | 16 | @Query(""" 17 | UPDATE DefinitionNote 18 | SET NoteText = :updatedText 19 | WHERE DefinitionID = :definitionID 20 | AND NoteText = :originalText """) 21 | suspend fun updateNote(updatedText: String, originalText : String, definitionID: Long) 22 | 23 | @Query(""" 24 | DELETE FROM DefinitionNote 25 | WHERE DefinitionID = :definitionID 26 | AND NoteText = :noteText """) 27 | suspend fun deleteNote(noteText : String, definitionID: Long) 28 | } -------------------------------------------------------------------------------- /app/src/main/java/room/dbo/entity/VocabularyNote.kt: -------------------------------------------------------------------------------- 1 | package room.dbo.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.ForeignKey 6 | import androidx.room.ForeignKey.CASCADE 7 | import androidx.room.PrimaryKey 8 | 9 | @Entity(tableName = "VocabularyNote", 10 | foreignKeys = [ 11 | ForeignKey( 12 | entity = Vocabulary::class, 13 | parentColumns = ["VocabularyID"], 14 | childColumns = ["VocabularyID"], 15 | onDelete = CASCADE) 16 | ] 17 | ) 18 | 19 | data class VocabularyNote ( 20 | @ColumnInfo(name = "NoteText") 21 | var noteText: String = "", 22 | 23 | @ColumnInfo(name = "VocabularyID", index = true) 24 | var vocabularyID: Long, 25 | 26 | @PrimaryKey(autoGenerate = true) 27 | @ColumnInfo(name = "VocabularyNoteID") 28 | var vocabularyNoteID: Long = 0) { 29 | override fun toString(): String { 30 | return noteText 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/data/enums/Dictionary.kt: -------------------------------------------------------------------------------- 1 | package data.enums 2 | 3 | enum class Dictionary { 4 | SANSEIDO; 5 | 6 | val dictionaryID : Long 7 | get() { 8 | return when (this) { 9 | SANSEIDO -> 1L 10 | } 11 | } 12 | val dictionaryName : String 13 | get() { 14 | return when (this) { 15 | SANSEIDO -> "三省堂" 16 | } 17 | } 18 | 19 | val defaultVocabularyLanguage : Language 20 | get() { 21 | return when (this) { 22 | SANSEIDO -> Language.JAPANESE 23 | } 24 | } 25 | 26 | val defaultDefinitionLanguage : Language 27 | get() { 28 | return when (this) { 29 | SANSEIDO -> Language.JAPANESE 30 | } 31 | } 32 | 33 | companion object { 34 | fun getDictionary(dictionaryID : Long) : Dictionary { 35 | return values().single{ it.dictionaryID == dictionaryID } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/room/dbo/composite/VocabularyAndTag.kt: -------------------------------------------------------------------------------- 1 | package room.dbo.composite 2 | 3 | import androidx.room.DatabaseView 4 | import androidx.room.Embedded 5 | import data.models.ITaggedItem 6 | import data.models.IVocabulary 7 | import room.dbo.entity.Tag 8 | import room.dbo.entity.Vocabulary 9 | 10 | // Probably a view here would be sufficient : ITaggedItem 11 | 12 | @DatabaseView(""" 13 | SELECT v.VocabularyID, 14 | v.Word, 15 | v.Pitch, 16 | v.Pronunciation, 17 | v.LanguageID, 18 | t.TagID, 19 | t.TagText 20 | FROM Vocabulary v 21 | JOIN VocabularyTag vt 22 | ON v.VocabularyID = vt.VocabularyID 23 | JOIN Tag t 24 | ON vt.TagID = t.TagID """) 25 | data class VocabularyAndTag( 26 | @Embedded 27 | val vocabulary : Vocabulary, 28 | @Embedded 29 | val tagEntity : Tag) 30 | : ITaggedItem { 31 | override val tag: String 32 | get() = tagEntity.tagText 33 | override val item: IVocabulary 34 | get() = vocabulary 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/ui/adapter/ListPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.ui.adapter 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.fragment.app.FragmentPagerAdapter 6 | 7 | class ListPagerAdapter(private val fragmentManager: FragmentManager, 8 | private var fragments : List, 9 | val id : Int) 10 | : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { 11 | 12 | @Throws(IndexOutOfBoundsException::class) 13 | override fun getItem(position: Int): Fragment { 14 | return fragments[position] 15 | } 16 | 17 | override fun getCount(): Int { 18 | return fragments.size 19 | } 20 | 21 | fun clearFragments() { 22 | val transaction = fragmentManager.beginTransaction() 23 | for(fragment in fragments){ 24 | transaction.remove(fragment) 25 | } 26 | transaction.commitNow() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/room/dao/entity/VocabularyDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.room.Dao 5 | import androidx.room.Query 6 | import data.enums.Language 7 | import room.dao.BaseDao 8 | import room.dbo.entity.Vocabulary 9 | 10 | @Dao 11 | interface VocabularyDao : BaseDao { 12 | 13 | @Query(""" 14 | SELECT v.* 15 | FROM Vocabulary v 16 | INNER JOIN Definition d 17 | on v.VocabularyID = d.VocabularyID 18 | """) 19 | fun getAllWithDefinition(): LiveData> 20 | 21 | @Query(""" 22 | SELECT v.VocabularyID 23 | FROM Vocabulary v 24 | WHERE v.Word = :word 25 | AND v.Pronunciation = :pronunciation 26 | AND v.Pitch = :pitch 27 | AND v.LanguageID = :vocabularyLanguage 28 | """) 29 | suspend fun getVocabularyID(word: String, 30 | pronunciation: String, 31 | pitch: String, 32 | vocabularyLanguage: Language): Long? 33 | } -------------------------------------------------------------------------------- /app/src/main/java/room/dao/entity/VocabularyNoteDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import androidx.room.Transaction 6 | import room.dao.BaseDao 7 | import room.dbo.entity.VocabularyNote 8 | 9 | @Dao 10 | interface VocabularyNoteDao : BaseDao { 11 | @Transaction 12 | @Query(""" 13 | SELECT vn.NoteText 14 | FROM VocabularyNote vn 15 | WHERE vn.VocabularyID = :vocabularyID""") 16 | suspend fun getNotesForVocabulary(vocabularyID : Long): List 17 | 18 | @Query(""" 19 | UPDATE VocabularyNote 20 | SET NoteText = :updatedText 21 | WHERE VocabularyID = :vocabularyID 22 | AND NoteText = :originalText """) 23 | suspend fun updateNote(updatedText: String, originalText : String, vocabularyID: Long) 24 | 25 | @Query(""" 26 | DELETE FROM VocabularyNote 27 | WHERE VocabularyID = :vocabularyID 28 | AND NoteText = :noteText """) 29 | suspend fun deleteNote(noteText : String, vocabularyID: Long) 30 | } -------------------------------------------------------------------------------- /app/src/main/res/menu/search_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/room/dao/entity/DefinitionDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import data.enums.Dictionary 6 | import data.enums.Language 7 | import room.dao.BaseDao 8 | import room.dbo.entity.Definition 9 | 10 | @Dao 11 | interface DefinitionDao : BaseDao { 12 | @Query(""" 13 | SELECT d.DefinitionID 14 | FROM Definition d 15 | WHERE d.VocabularyID = :vocabularyID 16 | AND d.DictionaryID = :dictionary 17 | AND d.LanguageID = :definitionLanguage """) 18 | suspend fun getDefinitionIDByVocabularyID(vocabularyID : Long, definitionLanguage: Language, dictionary: Dictionary) : Long? 19 | 20 | @Query(""" 21 | SELECT d.DefinitionID 22 | FROM Definition d 23 | WHERE d.DefinitionText = :definitionText 24 | AND d.DictionaryID = :dictionary 25 | AND d.LanguageID = :definitionLanguage """) 26 | suspend fun getDefinitionIDByDefinitionText(definitionText: String, definitionLanguage: Language, dictionary: Dictionary) : Long? 27 | } -------------------------------------------------------------------------------- /app/src/main/java/data/enums/Language.kt: -------------------------------------------------------------------------------- 1 | package data.enums 2 | 3 | import java.util.Locale 4 | 5 | enum class Language { 6 | JAPANESE, 7 | ENGLISH; 8 | 9 | val languageID : Long 10 | get() { 11 | return when (this) { 12 | JAPANESE -> 1L 13 | ENGLISH -> 2L 14 | } 15 | } 16 | 17 | val languageCode : String 18 | get() { 19 | return when (this) { 20 | JAPANESE -> Locale.JAPANESE.isO3Language 21 | ENGLISH -> Locale.ENGLISH.isO3Language 22 | } 23 | } 24 | 25 | val displayName : String 26 | get(){ 27 | return when (this){ 28 | JAPANESE -> Locale.JAPANESE.getDisplayLanguage(Locale.JAPANESE) 29 | ENGLISH -> Locale.ENGLISH.getDisplayLanguage(Locale.ENGLISH) 30 | } 31 | } 32 | 33 | companion object { 34 | fun getLanguage(languageID : Long) : Language { 35 | return values().single { 36 | it.languageID == languageID 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/ui/fragments/TextBlockFragment.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.ui.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import androidx.fragment.app.Fragment 9 | import com.limegrass.wanicchou.R 10 | import kotlin.properties.Delegates 11 | 12 | abstract class TextBlockFragment : Fragment() { 13 | protected var title by Delegates.observable(""){ 14 | _, _, newValue -> 15 | fragmentView.findViewById(R.id.tv_text_block_label).text = newValue 16 | } 17 | private lateinit var fragmentView : View 18 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 19 | val attachToRoot = false 20 | fragmentView = inflater.inflate(R.layout.fragment_text_list, 21 | container, 22 | attachToRoot) 23 | fragmentView.findViewById(R.id.tv_text_block_label).text = title 24 | return fragmentView 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/util/InputAlertDialogBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.util 2 | 3 | import android.content.Context 4 | import android.text.InputType 5 | import android.view.LayoutInflater 6 | import android.view.ViewGroup 7 | import android.widget.EditText 8 | import androidx.appcompat.app.AlertDialog 9 | import com.limegrass.wanicchou.R 10 | 11 | class InputAlertDialogBuilder( 12 | context : Context, 13 | viewGroup : ViewGroup?, 14 | title : String?, 15 | message : String?) 16 | : AlertDialog.Builder(context) { 17 | val input : EditText 18 | init { 19 | val dialogBuilder = this 20 | val view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, 21 | viewGroup, 22 | false) 23 | input = view.findViewById(R.id.et_input) 24 | input.inputType = InputType.TYPE_CLASS_TEXT 25 | dialogBuilder.setView(view) 26 | if(!title.isNullOrBlank()){ 27 | dialogBuilder.setTitle(title) 28 | } 29 | if(!message.isNullOrBlank()){ 30 | dialogBuilder.setMessage(message) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/room/dbo/entity/VocabularyTag.kt: -------------------------------------------------------------------------------- 1 | package room.dbo.entity 2 | 3 | import androidx.room.* 4 | import androidx.room.ForeignKey.CASCADE 5 | 6 | @Entity(tableName = "VocabularyTag", 7 | foreignKeys = [ 8 | ForeignKey( 9 | entity = Vocabulary::class, 10 | parentColumns = ["VocabularyID"], 11 | childColumns = ["VocabularyID"], 12 | onDelete = CASCADE), 13 | ForeignKey( 14 | entity = Tag::class, 15 | parentColumns = ["TagID"], 16 | childColumns = ["TagID"], 17 | onDelete = CASCADE)], 18 | indices = [Index( 19 | value = ["VocabularyID", 20 | "TagID"], 21 | unique = true 22 | )] 23 | ) 24 | 25 | //Note: room requires a PK on an entity. 26 | data class VocabularyTag ( 27 | @ColumnInfo(name = "TagID") 28 | var tagID: Long, 29 | 30 | @ColumnInfo(name = "VocabularyID") 31 | var vocabularyID: Long, 32 | 33 | @PrimaryKey(autoGenerate = true) 34 | @ColumnInfo(name = "VocabularyTagID") 35 | var vocabularyTagID: Long = 0) -------------------------------------------------------------------------------- /app/src/main/java/data/enums/MatchType.kt: -------------------------------------------------------------------------------- 1 | package data.enums 2 | 3 | enum class MatchType { 4 | WORD_EQUALS, 5 | WORD_STARTS_WITH, 6 | WORD_ENDS_WITH, 7 | WORD_CONTAINS, 8 | WORD_WILDCARDS, 9 | DEFINITION_CONTAINS, 10 | WORD_OR_DEFINITION_CONTAINS; 11 | 12 | val templateString : String 13 | get() { 14 | return when (this){ 15 | WORD_EQUALS -> NO_APPENDED_WILDCARDS_TEMPLATE_STRING 16 | WORD_STARTS_WITH -> TRAIL_WILDCARD_TEMPLATE_STRING 17 | WORD_ENDS_WITH -> LEAD_WILDCARD_TEMPLATE_STRING 18 | WORD_CONTAINS -> LEAD_AND_TRAIL_WILDCARD_TEMPLATE_STRING 19 | WORD_WILDCARDS -> NO_APPENDED_WILDCARDS_TEMPLATE_STRING 20 | DEFINITION_CONTAINS -> LEAD_AND_TRAIL_WILDCARD_TEMPLATE_STRING 21 | WORD_OR_DEFINITION_CONTAINS -> LEAD_AND_TRAIL_WILDCARD_TEMPLATE_STRING 22 | } 23 | } 24 | 25 | companion object { 26 | private const val LEAD_AND_TRAIL_WILDCARD_TEMPLATE_STRING = "%%%s%%" 27 | private const val TRAIL_WILDCARD_TEMPLATE_STRING = "%s%%" 28 | private const val LEAD_WILDCARD_TEMPLATE_STRING = "%%%s" 29 | private const val NO_APPENDED_WILDCARDS_TEMPLATE_STRING = "%s" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/androidTest/java/room/dao/entity/VocabularyTagDaoTest.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import data.enums.Language 4 | import kotlinx.coroutines.runBlocking 5 | import org.junit.Test 6 | import room.dao.AbstractDaoTest 7 | import room.dbo.entity.Tag 8 | import room.dbo.entity.Vocabulary 9 | import room.dbo.entity.VocabularyTag 10 | import kotlin.test.assertEquals 11 | 12 | class VocabularyTagDaoTest : AbstractDaoTest() { 13 | private fun insertVocabularyAndGetID() : Long { 14 | val vocabulary = Vocabulary("", "", "", Language.JAPANESE) 15 | return runBlocking { 16 | db.vocabularyDao().insert(vocabulary) 17 | } 18 | } 19 | @Test 20 | fun deleteVocabularyTagTest(){ 21 | val vocabularyID = insertVocabularyAndGetID() 22 | val tagText = "Test" 23 | val tag = Tag(tagText) 24 | val tagID = runBlocking { 25 | db.tagDao().insert(tag) 26 | } 27 | val vocabularyTag = VocabularyTag(tagID, vocabularyID) 28 | runBlocking { 29 | db.vocabularyTagDao().insert(vocabularyTag) 30 | } 31 | val deletedCount = runBlocking { 32 | db.vocabularyTagDao().deleteVocabularyTag(tagText, vocabularyID) 33 | } 34 | assertEquals(1, deletedCount) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/data/anki/IAnkiDroidApi.kt: -------------------------------------------------------------------------------- 1 | package data.anki 2 | 3 | import android.util.SparseArray 4 | import com.ichi2.anki.api.NoteInfo 5 | 6 | interface IAnkiDroidApi { 7 | val modelList : Map 8 | val deckList : Map 9 | val currentModelID : Long 10 | val apiHostSpecVersion : Int 11 | val selectedDeckName : String 12 | 13 | fun addNewDeck(deckName : String) : Long 14 | fun getDeckName(deckID : Long) : String? 15 | fun getModelName(modelID : Long) : String? 16 | fun getFieldList(modelID : Long) : Array? 17 | fun updateNoteFields(noteID : Long, fields : Array) : Boolean 18 | fun updateNoteTags(noteID : Long, tags : Set) : Boolean 19 | fun findDuplicateNotes(modelID : Long, firstFieldValue : String) : List 20 | fun findDuplicateNotes(modelID : Long, firstFieldValues : List) : SparseArray> 21 | fun addNewCustomModel(configuration : IAnkiDroidConfig, 22 | deckID : Long): Long 23 | fun addNote(modelID : Long, deckID: Long, fields : Array, tags : Set) : Long 24 | fun getModelList(minFieldCount : Int) : Map 25 | 26 | val hasAvailableApi : Boolean 27 | val hasAnkiReadWritePermission : Boolean 28 | val hasStoragePermission : Boolean 29 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_word.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 23 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/ui/adapter/ListViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.ui.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | 7 | abstract class ListViewAdapter>(private val list : MutableList, 8 | private val viewHolderConstructor: (View) -> VH, 9 | private val layoutID : Int) 10 | : androidx.recyclerview.widget.RecyclerView.Adapter(){ 11 | 12 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { 13 | val context = parent.context 14 | val inflater = LayoutInflater.from(context) 15 | val attachToRoot = false 16 | val view = inflater.inflate(layoutID, parent, attachToRoot) 17 | return viewHolderConstructor(view) 18 | } 19 | 20 | override fun getItemCount(): Int { 21 | return list.size 22 | } 23 | 24 | override fun onBindViewHolder(viewHolder: VH, position: Int){ 25 | viewHolder.bind(list[position]) 26 | } 27 | 28 | abstract class ViewHolder(itemView: View) 29 | : androidx.recyclerview.widget.RecyclerView.ViewHolder(itemView) { 30 | abstract fun bind(value: T) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_text_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 20 | 24 | 25 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/data/search/DictionarySearchManager.kt: -------------------------------------------------------------------------------- 1 | package data.search 2 | 3 | import data.models.IDictionaryEntry 4 | import data.architecture.ISearchProvider 5 | import java.util.* 6 | 7 | /** 8 | * Manages the order of search performed based on user preference. 9 | */ 10 | class DictionarySearchManager { 11 | private val searchCombinations : Queue = ArrayDeque() 12 | suspend fun executeSearches(): List { 13 | while (searchCombinations.isNotEmpty()){ 14 | val combination = searchCombinations.poll() 15 | val result = combination.searchProvider.search(combination.searchRequest) 16 | if (result.isSuccess()){ 17 | return result 18 | } 19 | } 20 | return listOf() 21 | } 22 | 23 | fun register(searchProvider: ISearchProvider, SearchRequest>, 24 | searchRequest: SearchRequest) { 25 | val combination = SearchCombination(searchProvider, searchRequest) 26 | searchCombinations.add(combination) 27 | } 28 | 29 | companion object { 30 | private fun List.isSuccess() : Boolean{ 31 | return this.any { it.definitions.isNotEmpty() } 32 | } 33 | } 34 | private class SearchCombination(val searchProvider: ISearchProvider, SearchRequest>, 35 | val searchRequest: SearchRequest) 36 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_wanicchou.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 13 | 14 | 21 | 22 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/test/java/data/enums/DictionaryTest.kt: -------------------------------------------------------------------------------- 1 | package data.enums 2 | 3 | import org.junit.Test 4 | import kotlin.test.asserter 5 | 6 | class DictionaryTest { 7 | 8 | @Test 9 | fun dictionaryID_Sanseido(){ 10 | asserter.assertEquals( 11 | "Sanseido DictionaryID changed", 12 | 1L, 13 | Dictionary.SANSEIDO.dictionaryID) 14 | } 15 | 16 | @Test 17 | fun dictionaryName_Sanseido(){ 18 | asserter.assertEquals( 19 | "Unexpected Sanseido Dictionary Name.", 20 | "三省堂", 21 | Dictionary.SANSEIDO.dictionaryName) 22 | } 23 | 24 | @Test 25 | fun defaultVocabularyLanguage_Sanseido(){ 26 | asserter.assertEquals( 27 | "Sanseido default vocabulary language no longer Japanese", 28 | Language.JAPANESE, 29 | Dictionary.SANSEIDO.defaultVocabularyLanguage) 30 | } 31 | 32 | @Test 33 | fun defaultDefinitionLanguage_Sanseido(){ 34 | asserter.assertEquals( 35 | "Sanseido default definition language no longer Japanese", 36 | Language.JAPANESE, 37 | Dictionary.SANSEIDO.defaultDefinitionLanguage) 38 | } 39 | 40 | @Test 41 | fun getDictionary_Sanseido(){ 42 | asserter.assertEquals( 43 | "Didn't get Sanseido enum type from it's dictionary ID", 44 | Dictionary.SANSEIDO, 45 | Dictionary.getDictionary(Dictionary.SANSEIDO.dictionaryID)) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/test/java/room/database/ConvertersTest.kt: -------------------------------------------------------------------------------- 1 | package room.database 2 | 3 | import data.enums.Dictionary 4 | import data.enums.Language 5 | import room.dbo.entity.Vocabulary 6 | import org.junit.Test 7 | import kotlin.test.assertEquals 8 | 9 | class ConvertersTest { 10 | @Test 11 | fun `fromVocabularyEntity returns vocabularyID`(){ 12 | val vocabularyID = 111L 13 | val vocabulary = Vocabulary("", "", "", Language.JAPANESE, vocabularyID) 14 | val converters = Converters() 15 | assertEquals(vocabularyID, converters.fromVocabularyEntity(vocabulary)) 16 | } 17 | 18 | @Test 19 | fun `fromLanguage returns languageID`(){ 20 | val language = Language.ENGLISH 21 | val converters = Converters() 22 | assertEquals(language.languageID, converters.fromLanguage(language)) 23 | } 24 | 25 | @Test 26 | fun `toLanguage returns Language enum`(){ 27 | val language = Language.ENGLISH 28 | val converters = Converters() 29 | assertEquals(language, converters.toLanguage(language.languageID)) 30 | } 31 | 32 | @Test 33 | fun `fromDictionary returns dictionaryID`(){ 34 | val dictionary = Dictionary.SANSEIDO 35 | val converters = Converters() 36 | assertEquals(dictionary.dictionaryID, converters.fromDictionary(dictionary)) 37 | } 38 | 39 | @Test 40 | fun `toDictionary returns Dictionary enum`(){ 41 | val dictionary = Dictionary.SANSEIDO 42 | val converters = Converters() 43 | assertEquals(dictionary, converters.toDictionary(dictionary.dictionaryID)) 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/data/search/jsoup/JsoupDictionarySearchProvider.kt: -------------------------------------------------------------------------------- 1 | package data.search.jsoup 2 | 3 | import androidx.annotation.WorkerThread 4 | import data.models.IDictionaryEntry 5 | import data.search.IDictionarySearchProvider 6 | import data.search.IDictionarySource 7 | import data.search.SearchRequest 8 | import data.enums.Language 9 | import data.enums.MatchType 10 | import org.jsoup.Jsoup 11 | import java.net.URL 12 | 13 | class JsoupDictionarySearchProvider(private val dictionarySource : IDictionarySource, 14 | private val dictionaryEntryFactory: IJsoupDictionaryEntryFactory) 15 | : IDictionarySearchProvider { 16 | 17 | override fun buildSearchQueryURL(searchRequest: SearchRequest): URL { 18 | return dictionarySource.buildSearchQueryURL(searchRequest) 19 | } 20 | override val supportedMatchTypes: Set 21 | get() = dictionarySource.supportedMatchTypes 22 | override val supportedTranslations: Map> 23 | get() = dictionarySource.supportedTranslations 24 | // ====================== PUBLIC ===================== 25 | 26 | @WorkerThread 27 | override suspend fun search(request: SearchRequest): List { 28 | val url = dictionarySource.buildSearchQueryURL(request).toString() 29 | val document = Jsoup.connect(url).data().get() 30 | return dictionaryEntryFactory.getDictionaryEntries(document, 31 | request.vocabularyLanguage, 32 | request.definitionLanguage) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.jp.md: -------------------------------------------------------------------------------- 1 | # 和日帳 (わにっちょう)Wanicchou 2 | [三省堂](https://www.sanseido.biz)から定義を検索して、暗記ドロイドへ送られるオンライン辞書アプリです。 3 | 4 | [Get it on F-Droid](https://f-droid.org/packages/com.waifusims.wanicchou/) 7 | [Get it on Google Play Store](https://play.google.com/store/apps/details?id=com.limegrass.wanicchou) 10 | 11 | 他の言語: [English](README.md) 12 | 13 | ## 目次 14 | - [遣い方](#遣い方) 15 | - [可能な開発](#可能な開発) 16 | - [参考したレポジトリ](#参考したレポジトリ) 17 | - [ライセンス](#ライセンス) 18 | 19 | 20 | ![Screenshot](/docs/app-image.png) 21 | 22 | ## 遣い方 23 | * 検索したい単語を検索箱に打ったら、Enterキーを打って、検索が始まります。 24 | * FAB(桃色のボタン)を押せば、暗記ドロイドへ新しいカードが作て送ります。 25 | * 設定には「国語」と「和英」の選択があります。英和は英語を打ったら、自動に英和の定義が出ます。 26 | * 定義が出来なかったら、設定で別の調べ方を変えてみればいいだと思います。 27 | * オートセーブとオートデリートは出来ますが、ディファインはしません。設定で変えられます。 28 | * 上からスワイプして、強制にオンライン辞書に検索出来ます。 29 | * セーブした検索はオプションメニューのボタンにボタンがあります。 30 | 31 | 32 | ## 可能な開発 33 | 検索した単語をオッフラインデータベースにする 34 | 35 | 辞書型が選べるにする 36 | 37 | 入力をみて、辞書型を自動に選ぶこと 38 | 39 | ネートから文脈とか例文を調べてアプリに入れること。 40 | (ツイッターとかから?JMDICTも) 41 | 42 | CLOZED型の暗記ドロイドカード? 43 | 44 | ネートから発音を調べ入れること 45 | (JMDICTから?) 46 | 47 | 日本語能力試験の級を検索した単語に付けること。 48 | 49 | UIのこと(全て)。。。 50 | 51 | 三省堂のサイトはこの遣い方は大丈夫かなあ。。。? 52 | 53 | 定義の単語を押せば、それを検索すること。 54 | 55 | 定義の漢字にふりがなを付けること。(JMDICT?) 56 | 57 | 皆の検索で自分のオンラインデータベースを作ること。 58 | 59 | ## 参考したレポジトリ 60 | ['ankidroid/apisample'](https://github.com/ankidroid/apisample): 61 | app/java/util.anki/AnkiDroidHelper.java 62 | app/java/util.anki/AnkiDroidConfig.java 63 | 64 | ## ライセンス 65 | GNU GPL 3.0 66 | -------------------------------------------------------------------------------- /app/src/test/java/data/enums/MatchTypeTest.kt: -------------------------------------------------------------------------------- 1 | package data.enums 2 | 3 | import org.junit.Test 4 | import kotlin.test.asserter 5 | 6 | 7 | class MatchTypeTest { 8 | @Test 9 | fun templateString_WordEquals(){ 10 | asserter.assertEquals("Unexpected template string", 11 | "%s", 12 | MatchType.WORD_EQUALS.templateString) 13 | } 14 | 15 | @Test 16 | fun templateString_WordStartsWith(){ 17 | asserter.assertEquals("Unexpected template string", 18 | "%s%%", 19 | MatchType.WORD_STARTS_WITH.templateString) 20 | } 21 | 22 | @Test 23 | fun templateString_WordEndsWith(){ 24 | asserter.assertEquals("Unexpected template string", 25 | "%%%s", 26 | MatchType.WORD_ENDS_WITH.templateString) 27 | } 28 | 29 | @Test 30 | fun templateString_WordContains(){ 31 | asserter.assertEquals("Unexpected template string", 32 | "%%%s%%", 33 | MatchType.WORD_CONTAINS.templateString) 34 | } 35 | 36 | @Test 37 | fun templateString_WordWildcards(){ 38 | asserter.assertEquals("Unexpected template string", 39 | "%s", 40 | MatchType.WORD_WILDCARDS.templateString) 41 | } 42 | 43 | @Test 44 | fun templateString_DefinitionContains(){ 45 | asserter.assertEquals("Unexpected template string", 46 | "%%%s%%", 47 | MatchType.DEFINITION_CONTAINS.templateString) 48 | } 49 | 50 | @Test 51 | fun templateString_WordOrDefinitionContains(){ 52 | asserter.assertEquals("Unexpected template string", 53 | "%%%s%%", 54 | MatchType.WORD_OR_DEFINITION_CONTAINS.templateString) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/ui/adapter/TextBlockRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.ui.adapter 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import android.widget.TextView 6 | import com.google.android.flexbox.AlignSelf 7 | import com.google.android.flexbox.FlexboxLayoutManager 8 | import com.limegrass.wanicchou.R 9 | 10 | class TextBlockRecyclerViewAdapter(list : List, 11 | private val onClickListener: View.OnClickListener? = null) : 12 | ListViewAdapter(list.toMutableList(), 14 | ::ViewHolder, 15 | R.layout.rv_item_text_block){ 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 17 | val viewHolder = super.onCreateViewHolder(parent, viewType) 18 | if(onClickListener != null){ 19 | viewHolder.itemView.setOnClickListener(onClickListener) 20 | } 21 | return viewHolder 22 | } 23 | class ViewHolder(itemView: View) : ListViewAdapter.ViewHolder(itemView) { 24 | private val textView : TextView = itemView.findViewById(R.id.tv_item_block) 25 | override fun bind(value: String) { 26 | //TODO: String resource template 27 | //TODO: Figure out if you can use interpolated strings with string resource 28 | textView.text = value 29 | if (itemView.layoutParams is FlexboxLayoutManager.LayoutParams){ 30 | val flexboxLayoutParams = itemView.layoutParams as FlexboxLayoutManager.LayoutParams 31 | flexboxLayoutParams.flexGrow = 1.0f 32 | flexboxLayoutParams.alignSelf = AlignSelf.AUTO 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/data/lang/JapaneseVocabulary.kt: -------------------------------------------------------------------------------- 1 | package data.lang 2 | 3 | import java.util.regex.Pattern 4 | 5 | /** 6 | * Helpers related to parsing Japanese Vocabulary 7 | */ 8 | class JapaneseVocabulary { 9 | companion object { 10 | // Kanji followed by Kana 11 | private const val WORD_WITH_KANJI_REGEX = 12 | """\p{script=Han}+[\p{script=Hiragana}|\p{script=Katakana}]*\p{script=Han}*""" 13 | // Pure Kana 14 | private const val KANA_REGEX = """[\p{script=Hiragana}|\p{script=Katakana}]+""" 15 | // Tone, accounting for full-width numbers 16 | private const val TONE_REGEX = """[\d0-9]+""" 17 | 18 | fun isolateWord(wordSource: String): String { 19 | val kanjiMatcher = Pattern.compile(WORD_WITH_KANJI_REGEX) 20 | .matcher(wordSource) 21 | val kanaMatcher = Pattern.compile(KANA_REGEX) 22 | .matcher(wordSource) 23 | return when { 24 | kanjiMatcher.find() -> kanjiMatcher.group(0) 25 | kanaMatcher.find() -> kanaMatcher.group(0) 26 | else -> wordSource 27 | } 28 | } 29 | 30 | fun isolatePitch(wordSource: String): String { 31 | val toneMatcher = Pattern.compile(TONE_REGEX) 32 | .matcher(wordSource) 33 | return when { 34 | toneMatcher.find() -> toneMatcher.group(0) 35 | else -> "" 36 | } 37 | } 38 | 39 | fun String?.isJapaneseInput() : Boolean { 40 | return when { 41 | !this.isNullOrBlank() -> this.trim().all { 42 | c: Char -> c.toInt() > 255 43 | } 44 | else -> false 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/room/repository/DefinitionNoteRepository.kt: -------------------------------------------------------------------------------- 1 | package room.repository 2 | 3 | import data.architecture.IRepository 4 | import data.models.INote 5 | import data.models.IDefinition 6 | import data.models.Note 7 | import room.database.WanicchouDatabase 8 | import room.dbo.entity.Definition 9 | import room.dbo.entity.DefinitionNote 10 | 11 | class DefinitionNoteRepository(private val database : WanicchouDatabase) 12 | : IRepository, IDefinition> { 13 | 14 | override suspend fun search(request: IDefinition): List> { 15 | val definitionID = Definition.getDefinitionID(database, request) ?: return listOf() 16 | val notes = database.definitionNoteDao().getNotesForDefinition(definitionID) 17 | return notes.map { 18 | Note(request, it) 19 | } 20 | } 21 | 22 | override suspend fun insert(entity: INote) { 23 | val definitionID = Definition.getDefinitionID(database, entity.topic)!! 24 | val definitionNote = DefinitionNote(entity.noteText, definitionID) 25 | database.definitionNoteDao().insert(definitionNote) 26 | } 27 | 28 | override suspend fun update(original: INote, 29 | updated: INote) { 30 | require(original.topic == updated.topic) { 31 | "Original and update notes reference different definitions." 32 | } 33 | val definitionID = Definition.getDefinitionID(database, original.topic)!! 34 | database.definitionNoteDao().updateNote(updated.noteText, original.noteText, definitionID) 35 | } 36 | 37 | override suspend fun delete(entity: INote) { 38 | val definitionID = Definition.getDefinitionID(database, entity.topic)!! 39 | database.definitionNoteDao().deleteNote(entity.noteText, definitionID) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/androidTest/java/room/dao/AbstractDaoTest.kt: -------------------------------------------------------------------------------- 1 | package room.dao 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import androidx.test.core.app.ApplicationProvider 6 | import room.database.WanicchouDatabase 7 | import room.dbo.entity.Dictionary 8 | import room.dbo.entity.Language 9 | import kotlinx.coroutines.runBlocking 10 | import org.junit.After 11 | import org.junit.Before 12 | import java.io.IOException 13 | 14 | abstract class AbstractDaoTest { 15 | protected lateinit var db : WanicchouDatabase 16 | 17 | @Before 18 | fun createDb() { 19 | val context = ApplicationProvider.getApplicationContext() 20 | db = Room.inMemoryDatabaseBuilder(context, 21 | WanicchouDatabase::class.java) 22 | .allowMainThreadQueries() 23 | .build() 24 | runBlocking { 25 | insertLanguages(db) 26 | } 27 | runBlocking { 28 | insertDictionaries(db) 29 | } 30 | } 31 | 32 | @After 33 | @Throws(IOException::class) 34 | fun closeDb() { 35 | db.close() 36 | } 37 | 38 | // 39 | private suspend fun insertLanguages(database: WanicchouDatabase){ 40 | for (language in data.enums.Language.values()){ 41 | val entity = Language(language.name, language.languageCode, language.languageID) 42 | database.languageDao().insert(entity) 43 | } 44 | } 45 | 46 | private suspend fun insertDictionaries(database: WanicchouDatabase){ 47 | for (dictionary in data.enums.Dictionary.values()){ 48 | val entity = Dictionary(dictionary.dictionaryName, 49 | dictionary.defaultVocabularyLanguage, 50 | dictionary.defaultVocabularyLanguage, 51 | dictionary.dictionaryID) 52 | database.dictionaryDao().insert(entity) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/test/java/data/enums/LanguageTest.kt: -------------------------------------------------------------------------------- 1 | package data.enums 2 | 3 | import org.junit.Test 4 | import kotlin.test.asserter 5 | 6 | class LanguageTest { 7 | @Test 8 | fun displayName_Japanese(){ 9 | asserter.assertEquals("Not translated to target language's alphabet", 10 | "日本語", 11 | Language.JAPANESE.displayName) 12 | } 13 | 14 | @Test 15 | fun displayName_English(){ 16 | asserter.assertEquals("Not translated to target language's alphabet", 17 | "English", 18 | Language.ENGLISH.displayName) 19 | } 20 | 21 | @Test 22 | fun languageCode_Japanese(){ 23 | asserter.assertEquals("Didn't get Japanese ISO3 code.", 24 | "jpn", 25 | Language.JAPANESE.languageCode) 26 | } 27 | 28 | @Test 29 | fun languageCode_English(){ 30 | asserter.assertEquals("Didn't get English ISO3 code", 31 | "eng", 32 | Language.ENGLISH.languageCode) 33 | } 34 | 35 | @Test 36 | fun getLanguageID_Japanese(){ 37 | asserter.assertEquals("Japanese LanguageID has changed from 1", 38 | 1L, 39 | Language.JAPANESE.languageID) 40 | } 41 | 42 | @Test 43 | fun getLanguageID_English(){ 44 | asserter.assertEquals("English LanguageID has changed from 2", 45 | 2L, 46 | Language.ENGLISH.languageID) 47 | } 48 | 49 | @Test 50 | fun getLanguage_Japanese(){ 51 | asserter.assertEquals("Expected Language.JAPANESE", 52 | Language.JAPANESE, 53 | Language.getLanguage(Language.JAPANESE.languageID)) 54 | } 55 | 56 | @Test 57 | fun getLanguage_English(){ 58 | asserter.assertEquals("Expected Language.ENGLISH", 59 | Language.ENGLISH, 60 | Language.getLanguage(Language.ENGLISH.languageID)) 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/ui/adapter/TextSpanRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.ui.adapter 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import android.widget.TextView 6 | import com.google.android.flexbox.AlignSelf 7 | import com.google.android.flexbox.FlexboxLayoutManager 8 | import com.limegrass.wanicchou.R 9 | 10 | //TODO: Change this implementation 11 | //TODO: DiffUtil for better animations 12 | // https://medium.com/@iammert/using-diffutil-in-android-recyclerview-bdca8e4fbb00 13 | class TextSpanRecyclerViewAdapter(list: List, 14 | private val onClickListener : View.OnClickListener? = null) 15 | : ListViewAdapter(list.toMutableList(), 16 | ::ViewHolder, 17 | R.layout.rv_item_text_span){ 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 19 | val viewHolder = super.onCreateViewHolder(parent, viewType) 20 | if(onClickListener != null){ 21 | viewHolder.itemView.setOnClickListener(onClickListener) 22 | } 23 | return viewHolder 24 | } 25 | class ViewHolder(itemView: View) : ListViewAdapter.ViewHolder(itemView) { 26 | private val textView : TextView = itemView.findViewById(R.id.tv_item_span) 27 | override fun bind(value: String) { 28 | //TODO: String resource template 29 | //TODO: Figure out if you can use interpolated strings with string resource 30 | textView.text = value 31 | if (itemView.layoutParams is FlexboxLayoutManager.LayoutParams){ 32 | val flexboxLayoutParams = itemView.layoutParams as FlexboxLayoutManager.LayoutParams 33 | flexboxLayoutParams.flexGrow = 1.0f 34 | flexboxLayoutParams.alignSelf = AlignSelf.AUTO 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/ui/adapter/SelectableAdapterDecorator.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.ui.adapter 2 | 3 | import android.util.SparseBooleanArray 4 | import android.view.ViewGroup 5 | import java.util.* 6 | 7 | abstract class SelectableAdapterDecorator(val adapter: androidx.recyclerview.widget.RecyclerView.Adapter) 8 | : androidx.recyclerview.widget.RecyclerView.Adapter() { 9 | 10 | override fun onCreateViewHolder(parent: ViewGroup, position: Int): VH { 11 | return adapter.onCreateViewHolder(parent, position) 12 | } 13 | 14 | override fun onBindViewHolder(parent: VH, position: Int) { 15 | adapter.onBindViewHolder(parent, position) 16 | } 17 | 18 | override fun getItemCount(): Int { 19 | return adapter.itemCount 20 | } 21 | 22 | 23 | private val mSelectedItems: SparseBooleanArray = SparseBooleanArray() 24 | val count: Int 25 | get() = mSelectedItems.size() 26 | 27 | val selectedItems: List 28 | get() { 29 | val selections = ArrayList(mSelectedItems.size()) 30 | for (i in 0 until mSelectedItems.size()) { 31 | selections.add(mSelectedItems.keyAt(i)) 32 | } 33 | return selections 34 | } 35 | 36 | fun isSelected(position: Int): Boolean { 37 | return mSelectedItems.get(position) 38 | } 39 | 40 | fun toggleSelection(position: Int) { 41 | if (mSelectedItems.get(position, false)) { 42 | mSelectedItems.delete(position) 43 | } else { 44 | mSelectedItems.put(position, true) 45 | } 46 | notifyItemChanged(position) 47 | } 48 | 49 | fun clearSelection() { 50 | val selections = selectedItems 51 | mSelectedItems.clear() 52 | for (position in selections) { 53 | notifyItemChanged(position) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 34 | 37 | 38 | 41 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/room/repository/VocabularyNoteRepository.kt: -------------------------------------------------------------------------------- 1 | package room.repository 2 | 3 | import data.architecture.IRepository 4 | import data.models.INote 5 | import data.models.IVocabulary 6 | import data.models.Note 7 | import room.database.WanicchouDatabase 8 | import room.dbo.entity.Vocabulary 9 | import room.dbo.entity.VocabularyNote 10 | 11 | class VocabularyNoteRepository(private val database : WanicchouDatabase) 12 | : IRepository, IVocabulary> { 13 | 14 | override suspend fun search(request: IVocabulary): List> { 15 | val vocabularyID = Vocabulary.getVocabularyID(database, request) ?: return listOf() 16 | val vocabularyNotes = database.vocabularyNoteDao().getNotesForVocabulary(vocabularyID) 17 | val vocabulary = if(request is Vocabulary) { 18 | request 19 | } 20 | else { 21 | // Wrapping in entity class to speed up update and delete within this repo 22 | Vocabulary(request, vocabularyID) 23 | } 24 | return vocabularyNotes.map { 25 | Note(vocabulary as IVocabulary, it) 26 | } 27 | } 28 | 29 | override suspend fun insert(entity: INote) { 30 | val vocabularyID = Vocabulary.getVocabularyID(database, entity.topic)!! 31 | val vocabularyNote = VocabularyNote(entity.noteText, vocabularyID) 32 | database.vocabularyNoteDao().insert(vocabularyNote) 33 | } 34 | 35 | override suspend fun update(original: INote, 36 | updated: INote) { 37 | require(original.topic == updated.topic) { 38 | "Original and update notes reference different vocabulary words." 39 | } 40 | val vocabularyID = Vocabulary.getVocabularyID(database, original.topic)!! 41 | database.vocabularyNoteDao().updateNote(updated.noteText, original.noteText, vocabularyID) 42 | } 43 | 44 | override suspend fun delete(entity: INote) { 45 | val vocabularyID = Vocabulary.getVocabularyID(database, entity.topic)!! 46 | database.vocabularyNoteDao().deleteNote(entity.noteText, vocabularyID) 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | Sanseido.biz 6 | 7 | 8 | 10 | 1 11 | 12 | 13 | 15 | Word Starts with 16 | Word Ends with 17 | Word Equals 18 | Word Contains 19 | Word Contains with Wildcards 20 | Definition Contains 21 | Word or Definition Contains 22 | 23 | 24 | 26 | WORD_STARTS_WITH 27 | WORD_ENDS_WITH 28 | WORD_EQUALS 29 | WORD_CONTAINS 30 | WORD_WILDCARDS 31 | DEFINITION_CONTAINS 32 | WORD_OR_DEFINITION_CONTAINS 33 | 34 | 35 | 37 | Never delete 38 | Delete on Anki import 39 | Delete on new search request 40 | 41 | 43 | NEVER 44 | ON_ANKI_IMPORT 45 | ON_SEARCH 46 | 47 | 48 | 50 | Japanese 51 | English 52 | 53 | 55 | 1 56 | 2 57 | 58 | 59 | 61 | Japanese 62 | English 63 | 64 | 66 | 1 67 | 2 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/java/room/repository/VocabularyTagRepository.kt: -------------------------------------------------------------------------------- 1 | package room.repository 2 | 3 | import data.models.ITaggedItem 4 | import data.models.IVocabulary 5 | import data.architecture.IRepository 6 | import room.database.WanicchouDatabase 7 | import room.dbo.composite.VocabularyAndTag 8 | import room.dbo.entity.Tag 9 | import room.dbo.entity.Vocabulary 10 | import room.dbo.entity.VocabularyTag 11 | import kotlinx.coroutines.runBlocking 12 | 13 | class VocabularyTagRepository(private val database : WanicchouDatabase) 14 | : IRepository, IVocabulary> { 15 | 16 | override suspend fun search(request: IVocabulary): List> { 17 | val vocabularyID = Vocabulary.getVocabularyID(database, request) ?: return listOf() 18 | return database.vocabularyAndTagDao().getVocabularyAndTag(vocabularyID) 19 | } 20 | 21 | override suspend fun insert(entity: ITaggedItem) { 22 | val tagText = entity.tag 23 | val vocabulary = entity.item 24 | val vocabularyID = Vocabulary.getVocabularyID(database, vocabulary)!! 25 | val tagID = database.tagDao().getExistingTagID(tagText) ?: runBlocking { 26 | val tag = Tag(tagText.trim()) 27 | database.tagDao().insert(tag) 28 | } 29 | val vocabularyTag = VocabularyTag(tagID, vocabularyID) 30 | database.vocabularyTagDao().insert(vocabularyTag) 31 | } 32 | 33 | /** 34 | * Updates the tag text for a given tag. Use Insert or Delete to add or remove relations 35 | */ 36 | override suspend fun update(original: ITaggedItem, 37 | updated: ITaggedItem) { 38 | require(original.item == updated.item) { 39 | "Original and updated tags reference different words." 40 | } 41 | val tagID = if (original is VocabularyAndTag) { 42 | original.tagEntity.tagID 43 | } 44 | else { 45 | database.tagDao().getExistingTagID(original.tag)!! 46 | } 47 | val tagEntity = Tag(updated.tag, tagID) 48 | database.tagDao().update(tagEntity) 49 | } 50 | 51 | override suspend fun delete(entity: ITaggedItem) { 52 | val vocabulary = entity.item 53 | val vocabularyID = Vocabulary.getVocabularyID(database, vocabulary)!! 54 | database.vocabularyTagDao().deleteVocabularyTag(entity.tag, vocabularyID) 55 | } 56 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 和日帳 (わにっちょう)Wanicchou 2 | Online dictionary drawing definitions from [Sanseido](https://www.sanseido.biz/) interfaced with the AnkiDroid API to quickly add words and their definitions. 3 | 4 | Other languages: [日本語](README.jp.md) 5 | 6 | [Get it on Google Play Store](https://play.google.com/store/apps/details?id=com.limegrass.wanicchou) 9 | 10 | ## Table of Contents 11 | - [Usage](#usage) 12 | - [Potential Development](#potential-development) 13 | - [Sources](#sources) 14 | - [LICENSE](#license) 15 | 16 | 17 | ![Screenshot](/docs/app-image.png) 18 | 19 | ## Usage 20 | * Type a word in the search box, then press enter to conduct a search. 21 | * The floating action button sends the searched word into AnkiDroid. 22 | * J-J and J-E preference can be changed in the settings. If English is entered, it will automatically search for an E-J definition entry. 23 | * If it does not find the word you wanted, change try changing the match type, or checking the related words generated from the search. 24 | * Autodeletion and Autosave settings are turned off by default. Change it in the settings to generate and manage an offline database on search. 25 | * Drag the top to force an online reload. 26 | * You can view saved searches through the options menu 27 | 28 | ## Potential Development 29 | Save searched words and definitions into an SQLiteDB for offline usage. 30 | 31 | SharedPreferences for which language dictionary the search should be conducted. 32 | 33 | Automatically select the E-J based on input being only in ASCII charset. 34 | 35 | Add a way to import sentences before Anki import. (Could scrap from Twitter? or JMDict) 36 | 37 | Add clozed type for the sentences. 38 | 39 | Add a way to import voices before Anki import. 40 | 41 | Label words by JLPT level. 42 | 43 | Everything about the UI. 44 | 45 | Figure out if it's even okay to use Sanseido's site like this. 46 | 47 | Parse definition text for possible words to search instead of having it as an EditText for users to copy and paste themselves. 48 | 49 | Furigana definition text after parsing, maybe with JE dict so there's less web requests. 50 | 51 | Maybe try to network DB from queries. 52 | 53 | ## Sources 54 | ['ankidroid/apisample'](https://github.com/ankidroid/apisample): 55 | app/java/util.anki/AnkiDroidHelper.java 56 | app/java/util.anki/AnkiDroidConfig.java 57 | 58 | ## License 59 | GNU GPL 3.0 60 | -------------------------------------------------------------------------------- /app/src/androidTest/java/room/dao/composite/VocabularyAndTagDaoTest.kt: -------------------------------------------------------------------------------- 1 | package room.dao.composite 2 | 3 | import data.enums.Language 4 | import kotlinx.coroutines.runBlocking 5 | import org.junit.Test 6 | import room.dao.AbstractDaoTest 7 | import room.dbo.entity.Tag 8 | import room.dbo.entity.Vocabulary 9 | import room.dbo.entity.VocabularyTag 10 | import kotlin.test.assertEquals 11 | 12 | class VocabularyAndTagDaoTest : AbstractDaoTest() { 13 | private fun insertVocabularyAndGetID(word: String) : Long { 14 | val vocabulary = Vocabulary(word, "", "", Language.JAPANESE) 15 | return runBlocking { 16 | db.vocabularyDao().insert(vocabulary) 17 | } 18 | } 19 | @Test 20 | fun getVocabularyAndTag_FindsAll() { 21 | val vocabularyID = insertVocabularyAndGetID("") 22 | val tags = listOf(Tag("1"), Tag("2"), Tag("3")) 23 | runBlocking { 24 | for (tag in tags){ 25 | val tagID = db.tagDao().insert(tag) 26 | db.vocabularyTagDao().insert(VocabularyTag(tagID, vocabularyID)) 27 | } 28 | } 29 | 30 | val vocabularyAndTags = runBlocking { 31 | db.vocabularyAndTagDao().getVocabularyAndTag(vocabularyID) 32 | } 33 | assertEquals(tags.size, vocabularyAndTags.size) 34 | val tagTexts = tags.map { it.tagText } 35 | for(tag in vocabularyAndTags){ 36 | assertEquals(vocabularyID, tag.vocabulary.vocabularyID) 37 | assert(tagTexts.contains(tag.tag)) 38 | } 39 | } 40 | 41 | @Test 42 | fun getVocabularyAndTag_AvoidsOtherTags() { 43 | val vocabularyIDs = listOf( 44 | insertVocabularyAndGetID("1"), 45 | insertVocabularyAndGetID("2")) 46 | val tags = listOf(Tag("1"), Tag("2"), Tag("3")) 47 | runBlocking { 48 | for (vocabularyID in vocabularyIDs){ 49 | for (tag in tags){ 50 | val tagID = db.tagDao().insert(tag) 51 | db.vocabularyTagDao().insert(VocabularyTag(tagID, vocabularyID)) 52 | } 53 | } 54 | } 55 | 56 | for (vocabularyID in vocabularyIDs){ 57 | val vocabularyAndTags = runBlocking { 58 | db.vocabularyAndTagDao().getVocabularyAndTag(vocabularyID) 59 | } 60 | assertEquals(tags.size, vocabularyAndTags.size) 61 | val tagTexts = tags.map { it.tagText } 62 | for(tag in vocabularyAndTags){ 63 | assertEquals(vocabularyID, tag.vocabulary.vocabularyID) 64 | assert(tagTexts.contains(tag.tag)) 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /WanicchouDB.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | graph TD; 3 | SearchActivity --> SettingsActivity; 4 | WordListAdapter --> WordListActivity; 5 | SearchActivity --> WordListActivity; 6 | SettingsFragment --> SettingsActivity; 7 | WanicchouSharedPreferenceHelper --> SearchActivity; 8 | ``` 9 | 10 | ```mermaid 11 | graph TD; 12 | SearchActivity --> |Request| SearchViewModel; 13 | SearchViewModel --> || VocabularyRepository 14 | ``` 15 | Get Vocabulary 16 | Display the first result 17 | From Vocabulary, grab the definition of it from the repo 18 | There can be multiple definitions per word LiveData>> 19 | List is per word, the outer list corresponds to the word 20 | Foreach word in LiveData, get List 21 | 22 | Maybe I don't need the whole Vocabulary? this could be potentially expensive. 23 | Should test whether it's more time expensive to get List 24 | or to get the all the relevant IDs and then display the only selected, querying for it after 25 | 26 | # DB Diagram 27 | ```mermaid 28 | classDiagram 29 | Definition --> Dictionary : DictionaryID 30 | Definition --> Vocabulary : VocabularyID 31 | 32 | DefinitionNote --> Definition : DefinitionID 33 | VocabularyNote --> Vocabulary : VocabularyID 34 | 35 | 36 | 37 | VocabularyRelation --> Vocabulary : VocabularyID 38 | VocabularyTag --> Vocabulary : VocabularyID 39 | 40 | VocabularyTag --> Tag : TagID 41 | 42 | 43 | 44 | DefinitionNote : INT DefinitionNoteID 45 | DefinitionNote : INT DefinitionID 46 | DefinitionNote : NVARCHAR(MAX) NoteText 47 | 48 | VocabularyNote : INT VocabularyNoteID 49 | VocabularyNote : INT VocabularyID 50 | VocabularyNote : NVARCHAR(MAX) NoteText 51 | 52 | Tag : INT TagID 53 | Tag : NVARCHAR(100) TagText 54 | 55 | VocabularyTag : INT VocabularyTagID 56 | VocabularyTag : INT VocabularyID 57 | VocabularyTag : INT TagID 58 | 59 | 60 | Dictionary : INT DictionaryID 61 | Dictionary : NVARCHAR(322) DictionaryName 62 | 63 | VocabularyRelation : INT VocabularyRelationID 64 | VocabularyRelation : INT SearchVocabularyID 65 | VocabularyRelation : INT ResultVocabularyID 66 | 67 | Definition : NVARCHAR(MAX) DefinitionText 68 | Definition : INT DefinitionID 69 | Definition : INT DictionaryID 70 | Definition : INT VocabularyID 71 | Definition : VARCHAR(2) LanguageCode 72 | 73 | Vocabulary : INT VocabularyID 74 | Vocabulary : NVARCHAR(420) Word 75 | Vocabulary : NVARCHAR(420) Pronunciation 76 | Vocabulary : VARCHAR(4) Pitch 77 | Vocabulary : VARCHAR(2) LanguageCode 78 | ``` 79 | > Note: VocabularyRelationID and VocabularyTagID can be used for sorting later 80 | 81 | > Drag right to assign the different options? 82 | Dictionary, MatchType, Language 83 | Populate Dictionary/Lang from DB ideally 84 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_tab_switch.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 27 | 28 | 42 | 43 | 44 | 55 | 63 | -------------------------------------------------------------------------------- /app/src/main/java/room/database/WanicchouDatabase.kt: -------------------------------------------------------------------------------- 1 | package room.database 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import androidx.room.TypeConverters 8 | import androidx.room.migration.Migration 9 | import androidx.sqlite.db.SupportSQLiteDatabase 10 | import data.architecture.SingletonHolder 11 | import room.dao.composite.DictionaryEntryDao 12 | import room.dao.composite.VocabularyAndTagDao 13 | import room.dao.entity.* 14 | import room.database.migration.WanicchouMigrationV2V3 15 | import room.dbo.composite.VocabularyAndTag 16 | import room.dbo.entity.* 17 | 18 | /** 19 | * Database object using the Room Persistence Library. 20 | * Invoke the singleton by calling the class with a context 21 | */ 22 | @Database( 23 | entities = [ 24 | Vocabulary::class, 25 | Definition::class, 26 | DefinitionNote::class, 27 | Dictionary::class, 28 | Tag::class, 29 | VocabularyNote::class, 30 | VocabularyTag::class, 31 | Language::class 32 | ], 33 | views = [VocabularyAndTag::class], 34 | version = 3, 35 | exportSchema = false 36 | ) 37 | @TypeConverters(Converters::class) 38 | abstract class WanicchouDatabase : RoomDatabase() { 39 | abstract fun definitionDao(): DefinitionDao 40 | abstract fun definitionNoteDao(): DefinitionNoteDao 41 | abstract fun dictionaryDao(): DictionaryDao 42 | abstract fun tagDao(): TagDao 43 | abstract fun vocabularyDao(): VocabularyDao 44 | abstract fun vocabularyNoteDao(): VocabularyNoteDao 45 | abstract fun vocabularyTagDao(): VocabularyTagDao 46 | abstract fun languageDao(): LanguageDao 47 | abstract fun dictionaryEntryDao(): DictionaryEntryDao 48 | abstract fun vocabularyAndTagDao(): VocabularyAndTagDao 49 | 50 | companion object : SingletonHolder({ 51 | val MIGRATION_1_2 = object : Migration(1, 2){ 52 | override fun migrate(database: SupportSQLiteDatabase) { 53 | database.execSQL(WanicchouMigration.MIGRATION_1_2_QUERY) 54 | } 55 | } 56 | 57 | val enumLikeValueInsertDatabaseCallback = EnumLikeValueInsertDatabaseCallback(it) 58 | Room.databaseBuilder(it.applicationContext, 59 | WanicchouDatabase::class.java, 60 | "WanicchouDatabase") 61 | .addMigrations(MIGRATION_1_2, WanicchouMigrationV2V3) 62 | .addCallback(enumLikeValueInsertDatabaseCallback) 63 | .build() 64 | }) { 65 | operator fun invoke(context: Context) : WanicchouDatabase { 66 | return getInstance(context) 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/test/java/data/lang/JapaneseVocabularyTest.kt: -------------------------------------------------------------------------------- 1 | package data.lang 2 | 3 | import data.lang.JapaneseVocabulary.Companion.isJapaneseInput 4 | import org.junit.Test 5 | import kotlin.test.asserter 6 | 7 | class JapaneseVocabularyTest { 8 | 9 | @Test 10 | fun isolateWord_Empty(){ 11 | val inputString = "" 12 | val isolatedString = JapaneseVocabulary.isolateWord(inputString) 13 | val message = "Expected $inputString, Got $isolatedString" 14 | asserter.assertEquals(message, inputString, isolatedString) 15 | } 16 | 17 | @Test 18 | fun isolateWord_NoJapanese(){ 19 | val inputString = "123ABC123 Test BTW" 20 | val isolatedString = JapaneseVocabulary.isolateWord(inputString) 21 | val message = "Expected $inputString, Got $isolatedString" 22 | asserter.assertEquals(message, inputString, isolatedString) 23 | } 24 | 25 | @Test 26 | fun isolateWord_KanaMatch(){ 27 | val inputString = "もうテストをかきたくない" 28 | val isolatedString = JapaneseVocabulary.isolateWord("I love testing! $inputString") 29 | val message = "Expected $inputString, Got $isolatedString" 30 | asserter.assertEquals(message, inputString, isolatedString) 31 | } 32 | 33 | @Test 34 | fun isolateWord_KanjiMatch(){ 35 | val inputString = "書きたくない" 36 | val isolatedString = JapaneseVocabulary.isolateWord("もうテストを$inputString") 37 | val message = "Expected $inputString, Got $isolatedString" 38 | asserter.assertEquals(message, inputString, isolatedString) 39 | } 40 | 41 | @Test 42 | fun isolatePitch_FullWidthMatch(){ 43 | val inputString = "ゆき 2[雪]" 44 | val isolatedString = JapaneseVocabulary.isolatePitch(inputString) 45 | val message = "Expected 2, Got $isolatedString" 46 | asserter.assertEquals(message, "2", isolatedString) 47 | } 48 | 49 | @Test 50 | fun isolatePitch_ASCIIMatch(){ 51 | val inputString = "ゆき 2 [雪]" 52 | val isolatedString = JapaneseVocabulary.isolatePitch(inputString) 53 | val message = "Expected 2, Got $isolatedString" 54 | asserter.assertEquals(message, "2", isolatedString) 55 | } 56 | 57 | // TODO: Expand on isJapaneseInput to allow romaaji one day. 58 | @Test 59 | fun isJapaneseInput_IsEnglish(){ 60 | val input = "English" 61 | val isJapaneseInput = input.isJapaneseInput() 62 | asserter.assertTrue("$input.isJapaneseInput() returned $isJapaneseInput, " + 63 | "expected ${!isJapaneseInput}", !isJapaneseInput) 64 | } 65 | 66 | @Test 67 | fun isJapaneseInput_IsJapanese(){ 68 | val input = "日本語" 69 | val isJapaneseInput = input.isJapaneseInput() 70 | asserter.assertTrue("$input.isJapaneseInput() returned ${!isJapaneseInput}, " + 71 | "expected $isJapaneseInput", isJapaneseInput) 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/ui/fragments/WordFragment.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.ui.fragments 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.TextView 9 | import android.widget.Toast 10 | import androidx.fragment.app.Fragment 11 | import androidx.lifecycle.LifecycleOwner 12 | import androidx.lifecycle.ViewModelProviders 13 | import com.limegrass.wanicchou.R 14 | import com.limegrass.wanicchou.util.cancelSetAndShowWanicchouToast 15 | import com.limegrass.wanicchou.viewmodel.DictionaryEntryViewModel 16 | 17 | class WordFragment : Fragment() { 18 | companion object { 19 | private val TAG : String = WordFragment::class.java.simpleName 20 | } 21 | 22 | private val dictionaryEntryViewModel : DictionaryEntryViewModel by lazy { 23 | ViewModelProviders.of(activity!!) 24 | .get(DictionaryEntryViewModel::class.java) 25 | } 26 | private lateinit var parentContext : Context 27 | 28 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 29 | val attachToRoot = false 30 | return inflater.inflate(R.layout.fragment_word, 31 | container, 32 | attachToRoot) 33 | } 34 | 35 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 36 | setVocabularyListObserver(view) 37 | setOnClickListener(view) 38 | parentContext = context!! 39 | super.onViewCreated(view, savedInstanceState) 40 | } 41 | 42 | private fun setOnClickListener(view: View){ 43 | val tvPronunciation = view.findViewById(R.id.tv_pronunciation) 44 | tvPronunciation.setOnClickListener { 45 | val dictionaryEntry = dictionaryEntryViewModel.value 46 | if (dictionaryEntry != null){ 47 | val toastText = getString(R.string.toast_pitch, dictionaryEntry.vocabulary.pitch) 48 | cancelSetAndShowWanicchouToast(parentContext, toastText, Toast.LENGTH_SHORT) 49 | } 50 | 51 | } 52 | } 53 | 54 | private fun setVocabularyListObserver(view : View?){ 55 | //TODO: Reset the wordIndex on new search 56 | val lifecycleOwner : LifecycleOwner = this 57 | dictionaryEntryViewModel.setObserver(lifecycleOwner){ 58 | val tvWord = view!!.findViewById(R.id.tv_word) 59 | val tvPronunciation = view.findViewById(R.id.tv_pronunciation) 60 | val dictionaryEntry = dictionaryEntryViewModel.value 61 | if (dictionaryEntry != null){ 62 | tvWord.text = dictionaryEntry.vocabulary.word 63 | tvPronunciation.text = dictionaryEntry.vocabulary.pronunciation 64 | } 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /app/src/androidTest/java/room/dao/entity/VocabularyNoteDaoTest.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import org.junit.Test 5 | import room.dao.AbstractDaoTest 6 | import room.dbo.entity.VocabularyNote 7 | import room.dbo.entity.Vocabulary 8 | import kotlin.test.assertEquals 9 | 10 | class VocabularyNoteDaoTest : AbstractDaoTest() { 11 | private fun insertVocabularyAndGetID() : Long { 12 | val vocabulary = Vocabulary("", "", "", data.enums.Language.JAPANESE) 13 | return runBlocking { 14 | db.vocabularyDao().insert(vocabulary) 15 | } 16 | } 17 | 18 | @Test 19 | fun getNotesForVocabulary_FindsNotes(){ 20 | val vocabularyID = insertVocabularyAndGetID() 21 | val notes = listOf( 22 | VocabularyNote("1", vocabularyID), 23 | VocabularyNote("2", vocabularyID)) 24 | runBlocking { 25 | for (note in notes){ 26 | db.vocabularyNoteDao().insert(note) 27 | } 28 | } 29 | val vocabularyNotes = runBlocking { 30 | db.vocabularyNoteDao().getNotesForVocabulary(vocabularyID) 31 | } 32 | assertEquals(notes.size, vocabularyNotes.size) 33 | } 34 | 35 | @Test 36 | fun updateNote_UpdatesDatabase() { 37 | val vocabularyID = insertVocabularyAndGetID() 38 | val notes = listOf( 39 | VocabularyNote("1", vocabularyID), 40 | VocabularyNote("2", vocabularyID)) 41 | runBlocking { 42 | for (note in notes){ 43 | db.vocabularyNoteDao().insert(note) 44 | } 45 | } 46 | runBlocking { 47 | db.vocabularyNoteDao().updateNote("3", "1", vocabularyID) 48 | } 49 | val vocabularyNotes = runBlocking { 50 | db.vocabularyNoteDao().getNotesForVocabulary(vocabularyID) 51 | } 52 | assertEquals(notes.size, vocabularyNotes.size) 53 | assert(!vocabularyNotes.contains("1")) 54 | assert(vocabularyNotes.contains("2")) 55 | assert(vocabularyNotes.contains("3")) 56 | } 57 | 58 | @Test 59 | fun deleteNote_Success() { 60 | val vocabularyID = insertVocabularyAndGetID() 61 | val notes = listOf( 62 | VocabularyNote("1", vocabularyID), 63 | VocabularyNote("2", vocabularyID)) 64 | runBlocking { 65 | for (note in notes){ 66 | db.vocabularyNoteDao().insert(note) 67 | } 68 | } 69 | runBlocking { 70 | db.vocabularyNoteDao().deleteNote("1", vocabularyID) 71 | } 72 | val vocabularyNotes = runBlocking { 73 | db.vocabularyNoteDao().getNotesForVocabulary(vocabularyID) 74 | } 75 | assertEquals(notes.size - 1, vocabularyNotes.size) 76 | assert(!vocabularyNotes.contains("1")) 77 | assert(vocabularyNotes.contains("2")) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/DatabaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.View 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.lifecycle.Observer 9 | import androidx.lifecycle.ViewModelProviders 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.google.android.flexbox.FlexDirection 12 | import com.google.android.flexbox.FlexboxLayoutManager 13 | import com.google.android.flexbox.JustifyContent 14 | import com.limegrass.wanicchou.ui.adapter.TextSpanRecyclerViewAdapter 15 | import com.limegrass.wanicchou.viewmodel.DatabaseViewModel 16 | import room.dbo.entity.Vocabulary 17 | 18 | 19 | /** 20 | * Separate activity to display the related words of a SanseidoSearch. 21 | * If a word is long pressed, it will be searched and brought back to the home activity. 22 | * TODO: Clean up this entire activity 23 | */ 24 | class DatabaseActivity : AppCompatActivity(){ 25 | companion object { 26 | private val TAG = DatabaseActivity::class.java.simpleName 27 | const val REQUEST_CODE = 3154 28 | } 29 | 30 | private val databaseViewModel : DatabaseViewModel by lazy { 31 | ViewModelProviders.of(this) 32 | .get(DatabaseViewModel::class.java) 33 | } 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | setContentView(R.layout.activity_database) 37 | } 38 | 39 | override fun onPostResume() { 40 | val recyclerView = findViewById(R.id.rv_database_list) 41 | val observer = Observer>{ 42 | val layoutManager = FlexboxLayoutManager(this) 43 | layoutManager.flexDirection = FlexDirection.ROW 44 | layoutManager.justifyContent = JustifyContent.SPACE_AROUND 45 | recyclerView.layoutManager = layoutManager 46 | val vocabularyList = databaseViewModel.vocabularyList.value!! 47 | val onClickListener = View.OnClickListener { v -> 48 | Log.v(TAG, "OnClick") 49 | val position = recyclerView.getChildLayoutPosition(v!!) 50 | val vocab = vocabularyList[position] 51 | val result = Intent() 52 | result.putExtra("Vocabulary", vocab) 53 | setResult(REQUEST_CODE, result) 54 | finish() 55 | } 56 | val vocabularyWords = vocabularyList.map{ "${it.word} [${it.pronunciation}]" } 57 | recyclerView.adapter = TextSpanRecyclerViewAdapter(vocabularyWords, onClickListener) 58 | } 59 | val obs = Observer>{ 60 | recyclerView.adapter?.notifyDataSetChanged() 61 | recyclerView.invalidate() 62 | } 63 | databaseViewModel.vocabularyList.observe(this, observer) 64 | databaseViewModel.vocabularyList.observe(this, obs) 65 | super.onPostResume() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/room/dbo/entity/Vocabulary.kt: -------------------------------------------------------------------------------- 1 | package room.dbo.entity 2 | 3 | import android.os.Parcelable 4 | import androidx.room.* 5 | import androidx.room.ForeignKey.CASCADE 6 | import data.models.IVocabulary 7 | import room.database.WanicchouDatabase 8 | import kotlinx.android.parcel.Parcelize 9 | import kotlinx.coroutines.runBlocking 10 | 11 | @Parcelize 12 | @Entity(tableName = "Vocabulary", 13 | foreignKeys = [ForeignKey( 14 | entity = Language::class, 15 | parentColumns = ["LanguageID"], 16 | childColumns = ["LanguageID"], 17 | onDelete = CASCADE)], 18 | indices = [Index( 19 | value = arrayOf("Word", 20 | "Pronunciation", 21 | "Pitch", 22 | "LanguageID"), 23 | unique = true)] 24 | ) 25 | 26 | data class Vocabulary ( 27 | @ColumnInfo(name = "Word") 28 | override var word: String, 29 | 30 | @ColumnInfo(name = "Pronunciation") 31 | override var pronunciation: String, 32 | 33 | @ColumnInfo(name = "Pitch") 34 | override var pitch: String, 35 | 36 | @ColumnInfo(name = "LanguageID") 37 | override var language: data.enums.Language, 38 | 39 | @PrimaryKey(autoGenerate = true) 40 | @ColumnInfo(name = "VocabularyID") 41 | var vocabularyID: Long = 0) : Parcelable, IVocabulary { 42 | 43 | constructor (model : IVocabulary, vocabularyID: Long = 0) 44 | : this(model.word, model.pronunciation, model.pitch, model.language, vocabularyID) 45 | 46 | override fun toString(): String { 47 | return word 48 | } 49 | 50 | override fun equals(other: Any?): Boolean { 51 | if (other == null || other !is IVocabulary){ 52 | return false 53 | } 54 | return this.word == other.word 55 | && this.pronunciation == other.pronunciation 56 | && this.language == other.language 57 | && this.pitch == other.pitch 58 | } 59 | 60 | override fun hashCode(): Int { 61 | return word.hashCode() xor 62 | pronunciation.hashCode() xor 63 | language.hashCode() xor 64 | pitch.hashCode() 65 | 66 | } 67 | companion object { 68 | /** 69 | * Gets the VocabularyID from the IVocabulary if it's an instance of the entity class, 70 | * else will perform a database request for it from the database given 71 | */ 72 | fun getVocabularyID(database: WanicchouDatabase, 73 | vocabulary: IVocabulary) : Long? { 74 | return if (vocabulary is Vocabulary){ 75 | vocabulary.vocabularyID 76 | } 77 | else runBlocking { 78 | database.vocabularyDao().getVocabularyID(vocabulary.word, 79 | vocabulary.pronunciation, 80 | vocabulary.pitch, 81 | vocabulary.language) 82 | } 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/room/dao/composite/DictionaryEntryDao.kt: -------------------------------------------------------------------------------- 1 | package room.dao.composite 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import androidx.room.Transaction 6 | import data.enums.Language 7 | import room.dbo.composite.DictionaryEntry 8 | 9 | @Dao 10 | interface DictionaryEntryDao { 11 | @Transaction 12 | @Query(""" 13 | SELECT v.* 14 | FROM Vocabulary v 15 | WHERE v.LanguageID = :vocabularyLanguage 16 | AND (v.Word LIKE :searchTerm 17 | OR v.Pronunciation LIKE :searchTerm) 18 | UNION 19 | SELECT v.* 20 | FROM Vocabulary v 21 | JOIN Definition d 22 | ON v.VocabularyID = d.VocabularyID 23 | WHERE v.LanguageID = :vocabularyLanguage 24 | AND IFNULL(d.LanguageID, :definitionLanguage) = :definitionLanguage 25 | AND (v.Word LIKE :searchTerm 26 | OR v.Pronunciation LIKE :searchTerm)""") 27 | fun searchWordLike(searchTerm: String, 28 | vocabularyLanguage: Language, 29 | definitionLanguage: Language) : List 30 | 31 | @Transaction 32 | @Query(""" 33 | SELECT v.* 34 | FROM Vocabulary v 35 | WHERE v.LanguageID = :vocabularyLanguage 36 | AND (v.Word = :searchTerm 37 | OR v.Pronunciation = :searchTerm) 38 | UNION 39 | SELECT v.* 40 | FROM Vocabulary v 41 | JOIN Definition d 42 | ON v.VocabularyID = d.VocabularyID 43 | WHERE v.LanguageID = :vocabularyLanguage 44 | AND IFNULL(d.LanguageID, :definitionLanguage) = :definitionLanguage 45 | AND (v.Word = :searchTerm 46 | OR v.Pronunciation = :searchTerm)""") 47 | fun searchWordEqual(searchTerm: String, 48 | vocabularyLanguage: Language, 49 | definitionLanguage: Language) : List 50 | 51 | @Transaction 52 | @Query(""" 53 | SELECT v.* 54 | FROM Vocabulary v 55 | JOIN Definition d 56 | ON v.VocabularyID = d.VocabularyID 57 | WHERE v.LanguageID = :vocabularyLanguage 58 | AND IFNULL(d.LanguageID, :definitionLanguage) = :definitionLanguage 59 | AND d.DefinitionText LIKE :searchTerm""") 60 | fun searchDefinitionLike(searchTerm: String, 61 | vocabularyLanguage: Language, 62 | definitionLanguage : Language) : List 63 | 64 | @Transaction 65 | @Query(""" 66 | SELECT v.* 67 | FROM Vocabulary v 68 | JOIN Definition d 69 | ON d.VocabularyID = v.VocabularyID 70 | WHERE v.LanguageID = :vocabularyLanguage 71 | AND IFNULL(d.LanguageID, :definitionLanguage) = :definitionLanguage 72 | AND (v.Word LIKE :searchTerm 73 | OR v.Pronunciation LIKE :searchTerm 74 | OR d.DefinitionText LIKE :searchTerm)""") 75 | fun searchWordOrDefinitionLike(searchTerm: String, 76 | vocabularyLanguage : Language, 77 | definitionLanguage: Language) : List 78 | } -------------------------------------------------------------------------------- /app/src/main/java/room/dbo/entity/Definition.kt: -------------------------------------------------------------------------------- 1 | package room.dbo.entity 2 | 3 | import androidx.room.* 4 | import androidx.room.ForeignKey.CASCADE 5 | import data.models.IDefinition 6 | import room.database.WanicchouDatabase 7 | 8 | @Entity(tableName = "Definition", 9 | foreignKeys = [ 10 | ForeignKey( 11 | entity = Dictionary::class, 12 | parentColumns = ["DictionaryID"], 13 | childColumns = ["DictionaryID"], 14 | onDelete = CASCADE), 15 | ForeignKey( 16 | entity = Vocabulary::class, 17 | parentColumns = ["VocabularyID"], 18 | childColumns = ["VocabularyID"], 19 | onDelete = CASCADE), 20 | ForeignKey( 21 | entity = Language::class, 22 | parentColumns = ["LanguageID"], 23 | childColumns = ["LanguageID"], 24 | onDelete = CASCADE) 25 | ], 26 | indices = [Index( 27 | value = arrayOf("VocabularyID", "LanguageID", "DictionaryID"), 28 | unique = true)] 29 | ) 30 | data class Definition ( 31 | @ColumnInfo(name = "DefinitionText") 32 | override var definitionText: String, 33 | 34 | @ColumnInfo(name = "LanguageID") 35 | override var language: data.enums.Language, 36 | 37 | @ColumnInfo(name = "DictionaryID") 38 | override var dictionary: data.enums.Dictionary, 39 | 40 | @ColumnInfo(name = "VocabularyID") 41 | var vocabularyID: Long, 42 | 43 | @PrimaryKey(autoGenerate = true) 44 | @ColumnInfo(name = "DefinitionID") 45 | var definitionID: Long = 0 ) 46 | : IDefinition { 47 | constructor(definition : IDefinition, vocabularyID: Long, definitionID : Long = 0) 48 | : this(definition.definitionText, 49 | definition.language, 50 | definition.dictionary, 51 | vocabularyID, 52 | definitionID) 53 | 54 | companion object { 55 | /** 56 | * Gets the DefinitionID from the IDefinition if it's an instance of the entity class, 57 | * else will perform a database request for it from the database given 58 | */ 59 | suspend fun getDefinitionID(database: WanicchouDatabase, 60 | definition: IDefinition, 61 | vocabularyID: Long? = null) : Long? { 62 | return when { 63 | definition is Definition -> definition.definitionID 64 | vocabularyID != null -> database.definitionDao() 65 | .getDefinitionIDByVocabularyID(vocabularyID, 66 | definition.language, 67 | definition.dictionary) 68 | else -> database.definitionDao() 69 | .getDefinitionIDByDefinitionText(definition.definitionText, 70 | definition.language, 71 | definition.dictionary) 72 | } 73 | } 74 | 75 | } 76 | 77 | override fun toString(): String { 78 | return definitionText 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/room/dao/entity/DefinitionNoteDaoTest.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import org.junit.Test 5 | import room.dao.AbstractDaoTest 6 | import room.dbo.entity.Definition 7 | import room.dbo.entity.DefinitionNote 8 | import room.dbo.entity.Vocabulary 9 | import kotlin.test.assertEquals 10 | 11 | class DefinitionNoteDaoTest : AbstractDaoTest() { 12 | private fun insertDefinitionAndGetID() : Long { 13 | val vocabulary = Vocabulary("", "", "", data.enums.Language.JAPANESE) 14 | val vocabularyID = runBlocking { 15 | db.vocabularyDao().insert(vocabulary) 16 | } 17 | val definition = Definition("", 18 | data.enums.Language.JAPANESE, 19 | data.enums.Dictionary.SANSEIDO, 20 | vocabularyID) 21 | return runBlocking { 22 | db.definitionDao().insert(definition) 23 | } 24 | } 25 | 26 | @Test 27 | fun getNotesForDefinition_FindsNotes(){ 28 | val definitionID = insertDefinitionAndGetID() 29 | val notes = listOf( 30 | DefinitionNote("1", definitionID), 31 | DefinitionNote("2", definitionID)) 32 | runBlocking { 33 | for (note in notes){ 34 | db.definitionNoteDao().insert(note) 35 | } 36 | } 37 | val definitionNotes = runBlocking { 38 | db.definitionNoteDao().getNotesForDefinition(definitionID) 39 | } 40 | 41 | assertEquals(notes.size, definitionNotes.size) 42 | } 43 | 44 | @Test 45 | fun updateNote_UpdatesDatabase() { 46 | val definitionID = insertDefinitionAndGetID() 47 | val notes = listOf( 48 | DefinitionNote("1", definitionID), 49 | DefinitionNote("2", definitionID)) 50 | runBlocking { 51 | for (note in notes){ 52 | db.definitionNoteDao().insert(note) 53 | } 54 | } 55 | runBlocking { 56 | db.definitionNoteDao().updateNote("3", "1", definitionID) 57 | } 58 | 59 | val definitionNotes = runBlocking { 60 | db.definitionNoteDao().getNotesForDefinition(definitionID) 61 | } 62 | assertEquals(notes.size, definitionNotes.size) 63 | assert(!definitionNotes.contains("1")) 64 | assert(definitionNotes.contains("2")) 65 | assert(definitionNotes.contains("3")) 66 | } 67 | 68 | @Test 69 | fun deleteNote_Success() { 70 | val definitionID = insertDefinitionAndGetID() 71 | val notes = listOf( 72 | DefinitionNote("1", definitionID), 73 | DefinitionNote("2", definitionID)) 74 | runBlocking { 75 | for (note in notes) { 76 | db.definitionNoteDao().insert(note) 77 | } 78 | } 79 | runBlocking { 80 | db.definitionNoteDao().deleteNote("1", definitionID) 81 | } 82 | val definitionNotes = runBlocking { 83 | db.definitionNoteDao().getNotesForDefinition(definitionID) 84 | } 85 | assertEquals(notes.size - 1, definitionNotes.size) 86 | assert(!definitionNotes.contains("1")) 87 | assert(definitionNotes.contains("2")) 88 | } 89 | } -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: 'com.google.gms.google-services' 6 | apply plugin: 'io.fabric' 7 | 8 | android { 9 | compileSdkVersion 28 10 | 11 | defaultConfig { 12 | applicationId "com.limegrass.wanicchou" 13 | minSdkVersion 16 14 | targetSdkVersion 28 15 | versionCode 12 16 | versionName "v1.0.2" 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | androidExtensions { 21 | experimental = true 22 | } 23 | 24 | buildTypes { 25 | release { 26 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation 'org.jsoup:jsoup:1.11.3' 32 | implementation 'com.ichi2.anki:api:1.1.0alpha5' 33 | implementation 'com.google.android:flexbox:1.1.0' 34 | 35 | implementation "android.arch.lifecycle:extensions:1.1.1" 36 | 37 | implementation "com.android.support:design:28.0.0" 38 | implementation "androidx.room:room-runtime:2.1.0" 39 | // Seems room compiler and room-coroutines version must match 40 | def roomVersion = "2.1.0-alpha04" 41 | kapt "androidx.room:room-compiler:$roomVersion" 42 | implementation "androidx.room:room-coroutines:$roomVersion" 43 | implementation "androidx.room:room-rxjava2:2.1.0" 44 | 45 | implementation 'com.google.firebase:firebase-core:17.2.0' 46 | implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' 47 | } 48 | 49 | testOptions { 50 | unitTests { 51 | returnDefaultValues = true 52 | } 53 | } 54 | 55 | lintOptions { 56 | checkReleaseBuilds false 57 | } 58 | 59 | dataBinding { 60 | enabled = true 61 | } 62 | 63 | bundle { 64 | language { 65 | enableSplit false 66 | } 67 | } 68 | } 69 | 70 | dependencies { 71 | implementation fileTree(dir: 'libs', include: ['*.jar']) 72 | implementation "androidx.appcompat:appcompat:1.1.0" 73 | implementation "androidx.preference:preference:1.1.0" 74 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 75 | implementation 'com.github.AndroidDeveloperLB:AutoFitTextView:4' 76 | testImplementation 'junit:junit:4.12' 77 | testImplementation "io.mockk:mockk:1.9.3" 78 | androidTestImplementation 'androidx.test:runner:1.3.0-alpha02' 79 | androidTestImplementation 'androidx.test:core:1.2.0' 80 | androidTestImplementation 'androidx.arch.core:core-testing:2.1.0' 81 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0-alpha02' 82 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 83 | androidTestImplementation "androidx.annotation:annotation:1.1.0" 84 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41" 85 | implementation "org.jetbrains.kotlin:kotlin-test:1.3.31" 86 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1" 87 | implementation 'com.google.firebase:firebase-core:17.2.0' 88 | } 89 | 90 | repositories { 91 | jcenter() 92 | google() 93 | mavenCentral() 94 | } 95 | -------------------------------------------------------------------------------- /app/src/androidTest/java/room/dao/entity/DefinitionDaoTest.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import data.enums.Language 4 | import room.dbo.entity.Definition 5 | import room.dbo.entity.Vocabulary 6 | import kotlinx.coroutines.runBlocking 7 | import org.junit.Test 8 | import room.dao.AbstractDaoTest 9 | import kotlin.test.assertEquals 10 | 11 | class DefinitionDaoTest : AbstractDaoTest() { 12 | private val definitionText = "Test" 13 | private val dictionary = data.enums.Dictionary.SANSEIDO 14 | private val language = Language.JAPANESE 15 | private var definitionID : Long = 0 16 | private fun insertTestData(){ 17 | val vocabularyID = runBlocking { 18 | db.vocabularyDao().insert(Vocabulary("", "", "", Language.JAPANESE)) 19 | } 20 | 21 | val definition = Definition(definitionText, language, dictionary, vocabularyID) 22 | 23 | definitionID = runBlocking { 24 | db.definitionDao().insert(definition) 25 | } 26 | } 27 | 28 | @Test 29 | fun getDefinitionIDByDefinitionText_Exists_ReturnsID() { 30 | insertTestData() 31 | val daoDefinitionID = runBlocking { 32 | db.definitionDao().getDefinitionIDByDefinitionText(definitionText, language, dictionary) 33 | } 34 | assertEquals(definitionID, daoDefinitionID) 35 | } 36 | 37 | @Test 38 | fun getDefinitionIDByDefinitionText_DefinitionTextMismatch_ReturnsNull() { 39 | insertTestData() 40 | val daoDefinitionID = runBlocking{ 41 | db.definitionDao().getDefinitionIDByDefinitionText("", language, dictionary) 42 | } 43 | assertEquals(null, daoDefinitionID) 44 | } 45 | 46 | @Test 47 | fun getDefinitionIDByDefinitionText_LanguageMismatch_ReturnsNull() { 48 | insertTestData() 49 | val daoDefinitionID = runBlocking { 50 | db.definitionDao().getDefinitionIDByDefinitionText("", Language.ENGLISH, dictionary) 51 | } 52 | assertEquals(null, daoDefinitionID) 53 | } 54 | 55 | @Test 56 | fun getDefinitionIDByVocabularyID_LanguageMismatch_ReturnsNull() { 57 | insertTestData() 58 | val daoDefinitionID = runBlocking { 59 | db.definitionDao().getDefinitionIDByDefinitionText("", Language.ENGLISH, dictionary) 60 | } 61 | assertEquals(null, daoDefinitionID) 62 | } 63 | @Test 64 | fun getDefinitionIDByVocabularyID_IDMismatch_ReturnsNull() { 65 | insertTestData() 66 | val daoDefinitionID = runBlocking { 67 | db.definitionDao().getDefinitionIDByDefinitionText("", Language.ENGLISH, dictionary) 68 | } 69 | assertEquals(null, daoDefinitionID) 70 | } 71 | @Test 72 | fun getDefinitionIDByVocabularyID_VocabularyIDMismatch_ReturnsNull() { 73 | insertTestData() 74 | val daoDefinitionID = runBlocking { 75 | db.definitionDao().getDefinitionIDByDefinitionText("", Language.ENGLISH, dictionary) 76 | } 77 | assertEquals(null, daoDefinitionID) 78 | } 79 | @Test 80 | fun getDefinitionIDByVocabularyID_Match_ReturnsID() { 81 | insertTestData() 82 | val daoDefinitionID = runBlocking { 83 | db.definitionDao().getDefinitionIDByDefinitionText("", Language.ENGLISH, dictionary) 84 | } 85 | assertEquals(null, daoDefinitionID) 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/room/database/EnumLikeValueInsertDatabaseCallback.kt: -------------------------------------------------------------------------------- 1 | package room.database 2 | 3 | import android.content.Context 4 | import androidx.room.RoomDatabase 5 | import androidx.sqlite.db.SupportSQLiteDatabase 6 | import room.dbo.entity.* 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.launch 9 | import kotlinx.coroutines.runBlocking 10 | import java.util.concurrent.Executors 11 | 12 | class EnumLikeValueInsertDatabaseCallback(private val context : Context) : RoomDatabase.Callback() { 13 | override fun onCreate(db: SupportSQLiteDatabase) { 14 | Executors.newSingleThreadExecutor().execute { 15 | val database = WanicchouDatabase(context) 16 | super.onCreate(db) 17 | runBlocking { 18 | insertLanguages(database) 19 | } 20 | runBlocking { 21 | insertDictionaries(database) 22 | } 23 | insertDefaultEntry(database) 24 | } 25 | } 26 | 27 | // 28 | private suspend fun insertLanguages(database: WanicchouDatabase){ 29 | for (language in data.enums.Language.values()){ 30 | val entity = Language(language.name, language.languageCode, language.languageID) 31 | database.languageDao().insert(entity) 32 | } 33 | } 34 | 35 | private suspend fun insertDictionaries(database: WanicchouDatabase){ 36 | for (dictionary in data.enums.Dictionary.values()){ 37 | val entity = Dictionary(dictionary.dictionaryName, 38 | dictionary.defaultVocabularyLanguage, 39 | dictionary.defaultVocabularyLanguage, 40 | dictionary.dictionaryID) 41 | database.dictionaryDao().insert(entity) 42 | } 43 | } 44 | 45 | private fun insertDefaultEntry(database: WanicchouDatabase){ 46 | GlobalScope.launch { 47 | val defaultVocabulary = Vocabulary( 48 | word = "和日帳", 49 | pronunciation = "わにっちょう", 50 | pitch = "", 51 | language = data.enums.Language.JAPANESE, 52 | vocabularyID = 1) 53 | val defaultDefinition = Definition( 54 | definitionText = """タイトルバーを押して、検索できる辞書アプリ。 55 | Tap the title bar to begin entering a search term. 56 | Navigate to the settings screen from the menu bar at the top right. 57 | The floating action button will send the current definition to AnkiDroid. 58 | Tags will be imported to AnkiDroid. 59 | Vocabulary Notes will persist across the vocabulary term (for example, a note that is relevant to both 60 | the Japanese-Japanese definition and the Japanese-English definition for a given word.) 61 | Tapping on dark grey boxes will bring up a window to edit the text. 62 | Tapping on an entry in Related will attempt to search that word. 63 | Report bugs at github.com/Limegrass/Wanicchou/issues. 64 | 設定画面は右上隅のメニューに移られます。 65 | FABを押して、カードは暗記ドロイドに送ります。 66 | 単語のメモはどんな定義でも、そのメモが出てきます。 67 | 普通の灰色より暗い灰色のところを押せば、そのテキストのエディットボックスが現れます。 68 | バグが現れたら、github.com/Limegrass/Wanicchou/issues に新しいIssueを追加してください。""", 69 | language = data.enums.Language.JAPANESE, 70 | dictionary = data.enums.Dictionary.SANSEIDO, 71 | vocabularyID = 1, 72 | definitionID = 1) 73 | database.vocabularyDao().insert(defaultVocabulary) 74 | database.definitionDao().insert(defaultDefinition) 75 | } 76 | } 77 | // 78 | } -------------------------------------------------------------------------------- /app/src/test/java/data/web/sanseido/SanseidoDictionaryEntryFactoryTest.kt: -------------------------------------------------------------------------------- 1 | package data.web.sanseido 2 | 3 | import data.enums.Dictionary 4 | import data.enums.Language 5 | import org.jsoup.Jsoup 6 | import org.junit.Test 7 | import kotlin.test.asserter 8 | 9 | //TODO: Add more test cases 10 | // One that has pitch and uses the exact regex matcher/other 11 | class SanseidoDictionaryEntryFactoryTest { 12 | @Test 13 | fun getDictionaryEntries_ExactMatchWord(){ 14 | val filePath = "/data/web/sanseido/JPN-ENGテスト.html" // /app/src/test/... 15 | val documentString = SanseidoDictionaryEntryFactoryTest::class.java 16 | .getResource(filePath)!! 17 | .readText() 18 | val document = Jsoup.parse(documentString) 19 | val dictionaryEntryFactory = SanseidoDictionaryEntryFactory() 20 | val dictionaryEntries = dictionaryEntryFactory.getDictionaryEntries(document, 21 | Language.JAPANESE, 22 | Language.ENGLISH) 23 | asserter.assertTrue("Empty DictionaryEntry array", 24 | dictionaryEntries.isNotEmpty()) 25 | asserter.assertTrue("Expected 2 elements", 26 | dictionaryEntries.size == 2) 27 | val dictionaryEntry = dictionaryEntries.single{ 28 | it.definitions.isNotEmpty() 29 | } 30 | asserter.assertEquals("Word does not match", 31 | "テスト", 32 | dictionaryEntry.vocabulary.word) 33 | asserter.assertEquals("Pronunciation does not match", 34 | "テスト", 35 | dictionaryEntry.vocabulary.pronunciation) 36 | asserter.assertEquals("Pitch not empty", 37 | "", 38 | dictionaryEntry.vocabulary.pitch) 39 | asserter.assertEquals("VocabularyLanguage should be Japanese", 40 | Language.JAPANESE, 41 | dictionaryEntry.vocabulary.language) 42 | 43 | asserter.assertEquals("Expected only 1 definition", 44 | 1, 45 | dictionaryEntry.definitions.size) 46 | val definition = dictionaryEntry.definitions[0] 47 | asserter.assertEquals("DefinitionText does not match", 48 | "a test. ・~する(を受ける) give (take) a test ((in, for)). " + 49 | "◆テスト・ケース a test case. " + 50 | "◆テスト・パイロット a test pilot. ◆テスト・パターン a test pattern.", 51 | definition.definitionText) 52 | asserter.assertEquals("Expected definition to be English.", 53 | Language.ENGLISH, 54 | definition.language) 55 | asserter.assertEquals("Expected definition to be English.", 56 | Dictionary.SANSEIDO, 57 | definition.dictionary) 58 | 59 | val otherEntry = dictionaryEntries.single{ 60 | it != dictionaryEntry 61 | } 62 | asserter.assertEquals("Expected no definitions", 63 | 0, 64 | otherEntry.definitions.size) 65 | asserter.assertEquals("Expected repeat", 66 | dictionaryEntry.vocabulary.word, 67 | otherEntry.vocabulary.word) 68 | asserter.assertEquals("Expected repeat", 69 | dictionaryEntry.vocabulary.pronunciation, 70 | otherEntry.vocabulary.pronunciation) 71 | asserter.assertEquals("Expected repeat", 72 | dictionaryEntry.vocabulary.pitch, 73 | otherEntry.vocabulary.pitch) 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/data/web/sanseido/SanseidoSource.kt: -------------------------------------------------------------------------------- 1 | package data.web.sanseido 2 | 3 | import android.net.Uri 4 | import data.search.IDictionarySource 5 | import data.search.SearchRequest 6 | import data.enums.Language 7 | import data.enums.MatchType 8 | import java.net.MalformedURLException 9 | import java.net.URL 10 | 11 | class SanseidoSource : IDictionarySource { 12 | override val supportedMatchTypes: Set 13 | get() = SUPPORTED_MATCH_TYPES.keys 14 | 15 | override val supportedTranslations: Map> 16 | get() = SUPPORTED_TRANSLATIONS 17 | 18 | @Throws(MalformedURLException::class) 19 | override fun buildSearchQueryURL(searchRequest : SearchRequest): URL { 20 | val uriBuilder = Uri.parse(SANSEIDO_BASE_URL).buildUpon() 21 | uriBuilder.setSearchType(searchRequest.matchType) 22 | uriBuilder.setSearchTerm(searchRequest.searchTerm) 23 | uriBuilder.setDictionaryOrder(DORDER_DEFAULT) 24 | uriBuilder.setQueryLanguage(searchRequest.vocabularyLanguage, searchRequest.definitionLanguage) 25 | return URL(uriBuilder.build().toString()) 26 | } 27 | 28 | // ====================== PRIVATE ====================== 29 | companion object { 30 | 31 | private val TAG = SanseidoSource::class.java.simpleName 32 | 33 | private const val SANSEIDO_BASE_URL = "https://www.sanseido.biz/User/Dic/Index.aspx" 34 | private const val PARAM_WORD_QUERY = "TWords" 35 | // Order of dictionaries under select dictionaries 36 | private const val PARAM_DICTIONARY_ORDER = "DORDER" 37 | private const val DORDER_JJ = "15" 38 | private const val DORDER_JE = "17" 39 | private const val DORDER_EJ = "16" 40 | private const val DORDER_DEFAULT = DORDER_JJ + DORDER_JE + DORDER_EJ 41 | private const val PARAM_SEARCH_TYPE = "st" 42 | // Enabling and disabling of languages 43 | // Display will go by DORDER 44 | private const val PARAM_DIC_PREFIX = "Daily" 45 | private const val SET_LANG = "checkbox" 46 | 47 | private val SUPPORTED_MATCH_TYPES = hashMapOf( 48 | MatchType.WORD_STARTS_WITH to "0", 49 | MatchType.WORD_EQUALS to "1", 50 | MatchType.WORD_ENDS_WITH to "2", 51 | MatchType.WORD_OR_DEFINITION_CONTAINS to "3", 52 | MatchType.WORD_CONTAINS to "5") 53 | 54 | private fun Uri.Builder.setSearchType(matchType: MatchType) { 55 | val sanseidoMatchTypeID = SUPPORTED_MATCH_TYPES[matchType] 56 | ?: throw IllegalArgumentException("Unsupported MatchType: $matchType") 57 | appendQueryParameter(PARAM_SEARCH_TYPE, sanseidoMatchTypeID ) 58 | } 59 | 60 | private fun Uri.Builder.setDictionaryOrder(dictionaryOrder : String) { 61 | appendQueryParameter(PARAM_DICTIONARY_ORDER, dictionaryOrder) 62 | } 63 | 64 | private fun Uri.Builder.setQueryLanguage(wordLanguage: Language, definitionLanguage: Language){ 65 | appendQueryParameter( 66 | PARAM_DIC_PREFIX 67 | + getLanguagePrefix(wordLanguage) 68 | + getLanguagePrefix(definitionLanguage), 69 | SET_LANG) 70 | } 71 | 72 | private fun Uri.Builder.setSearchTerm(searchTerm: String){ 73 | appendQueryParameter(PARAM_WORD_QUERY, searchTerm) 74 | } 75 | 76 | private fun getLanguagePrefix(language : Language) : Char { 77 | return when (language){ 78 | Language.ENGLISH-> 'E' 79 | Language.JAPANESE -> 'J' 80 | } 81 | } 82 | 83 | private val SUPPORTED_TRANSLATIONS : Map> = hashMapOf( 84 | Language.JAPANESE to hashSetOf(Language.JAPANESE, Language.ENGLISH), 85 | Language.ENGLISH to hashSetOf(Language.JAPANESE) 86 | ) 87 | } 88 | } -------------------------------------------------------------------------------- /app/src/test/java/data/web/sanseido/SanseidoDictionarySourceTest.kt: -------------------------------------------------------------------------------- 1 | package data.web.sanseido 2 | 3 | import data.search.SearchRequest 4 | import data.enums.Language 5 | import data.enums.MatchType 6 | import org.junit.Test 7 | import kotlin.test.asserter 8 | 9 | // Reminder: unsupported language tests when new languages are introduced. 10 | class SanseidoDictionarySourceTest { 11 | private val dictionarySource = SanseidoSource() 12 | @Test 13 | fun buildQueryURL_SupportedMatchType(){ 14 | val searchTerm = "テスト" 15 | val wordLanguage = Language.JAPANESE 16 | val definitionLanguage = Language.ENGLISH 17 | val matchType = MatchType.WORD_EQUALS 18 | val searchRequest = SearchRequest(searchTerm, 19 | wordLanguage, 20 | definitionLanguage, 21 | matchType) 22 | val actual = dictionarySource.buildSearchQueryURL(searchRequest).toString() 23 | val expected = "https://www.sanseido.biz/User/Dic/Index.aspx" + 24 | "?st=1&TWords=%E3%83%86%E3%82%B9%E3%83%88&DORDER=151716&DailyJE=checkbox" 25 | val message = "Unexpected search URL" 26 | asserter.assertEquals(message, expected, actual) 27 | } 28 | 29 | @Test(expected = IllegalArgumentException::class) 30 | fun buildQueryURL_UnsupportedMatchType(){ 31 | val searchTerm = "Test" 32 | val wordLanguage = Language.JAPANESE 33 | val definitionLanguage = Language.ENGLISH 34 | val matchType = MatchType.DEFINITION_CONTAINS 35 | val searchRequest = SearchRequest(searchTerm, wordLanguage, definitionLanguage, matchType) 36 | dictionarySource.buildSearchQueryURL(searchRequest) 37 | } 38 | 39 | @Test 40 | fun supportedMatchTypes_HasWordEqual(){ 41 | val hasMatchType = dictionarySource.supportedMatchTypes.contains(MatchType.WORD_EQUALS) 42 | asserter.assertTrue("Missing MatchType Equals", hasMatchType) 43 | } 44 | 45 | @Test 46 | fun supportedMatchTypes_HasWordStartsWith(){ 47 | val hasMatchType = dictionarySource.supportedMatchTypes.contains(MatchType.WORD_STARTS_WITH) 48 | asserter.assertTrue("Missing MatchType Word Starts With", hasMatchType) 49 | } 50 | 51 | @Test 52 | fun supportedMatchTypes_HasWordEndsWith(){ 53 | val hasMatchType = dictionarySource.supportedMatchTypes.contains(MatchType.WORD_ENDS_WITH) 54 | asserter.assertTrue("Missing MatchType Word Ends With", hasMatchType) 55 | } 56 | 57 | @Test 58 | fun supportedMatchTypes_HasWordContains(){ 59 | val hasMatchType = dictionarySource.supportedMatchTypes.contains(MatchType.WORD_CONTAINS) 60 | asserter.assertTrue("Missing MatchType Word Or Definition Contains", hasMatchType) 61 | } 62 | 63 | @Test 64 | fun supportedMatchTypes_HasWordOrDefinitionContains(){ 65 | val hasMatchType = dictionarySource.supportedMatchTypes.contains(MatchType.WORD_OR_DEFINITION_CONTAINS) 66 | asserter.assertTrue("Missing MatchType Word Or Definition Contains", hasMatchType) 67 | } 68 | 69 | @Test 70 | fun supportedTranslations_JapaneseInput(){ 71 | val japaneseInputTranslations = dictionarySource.supportedTranslations[Language.JAPANESE] 72 | ?: error("Japanese Language Input missing") 73 | asserter.assertTrue("Missing MatchType Word Or Definition Contains", 74 | japaneseInputTranslations.contains(Language.JAPANESE)) 75 | asserter.assertTrue("Missing MatchType Word Or Definition Contains", 76 | japaneseInputTranslations.contains(Language.ENGLISH)) 77 | } 78 | 79 | @Test 80 | fun supportedTranslations_EnglishInput(){ 81 | val englishInputTranslations = dictionarySource.supportedTranslations[Language.JAPANESE] 82 | ?: error("English Language input missing") 83 | asserter.assertTrue("Missing MatchType Word Or Definition Contains", 84 | englishInputTranslations.contains(Language.JAPANESE)) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/androidTest/java/room/dao/entity/VocabularyDaoTest.kt: -------------------------------------------------------------------------------- 1 | package room.dao.entity 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import data.enums.Dictionary 5 | import data.enums.Language 6 | import room.dbo.entity.Definition 7 | import room.dbo.entity.Vocabulary 8 | import kotlinx.coroutines.runBlocking 9 | import org.junit.Rule 10 | import org.junit.Test 11 | import room.dao.AbstractDaoTest 12 | import util.awaitValue 13 | import kotlin.test.assertEquals 14 | 15 | class VocabularyDaoTest : AbstractDaoTest() { 16 | @get:Rule 17 | var instantTaskExecutorRule = InstantTaskExecutorRule() 18 | 19 | @Test 20 | fun getAllWithDefinition_NoResults(){ 21 | runBlocking { 22 | db.vocabularyDao().insert(Vocabulary("1", "", "", Language.JAPANESE)) 23 | db.vocabularyDao().insert(Vocabulary("2", "", "", Language.JAPANESE)) 24 | db.vocabularyDao().insert(Vocabulary("3", "", "", Language.JAPANESE)) 25 | } 26 | val liveData = db.vocabularyDao().getAllWithDefinition() 27 | val vocabularies = liveData.awaitValue() 28 | assertEquals(0, vocabularies.size) 29 | } 30 | 31 | @Test 32 | fun getAllWithDefinition_HasResult(){ 33 | runBlocking { 34 | db.vocabularyDao().insert(Vocabulary("1", "", "", Language.JAPANESE)) 35 | db.vocabularyDao().insert(Vocabulary("2", "", "", Language.JAPANESE)) 36 | db.definitionDao().insert(Definition("Test", Language.JAPANESE, Dictionary.SANSEIDO, 1)) 37 | } 38 | val liveData = db.vocabularyDao().getAllWithDefinition() 39 | val vocabularies = liveData.awaitValue() 40 | assertEquals(1, vocabularies.size) 41 | } 42 | 43 | @Test 44 | fun getVocabularyID_Exists_ReturnsID(){ 45 | val word = "1" 46 | val pronunciation = "" 47 | val pitch = "" 48 | val language = Language.JAPANESE 49 | val vocabularyID = runBlocking { 50 | db.vocabularyDao().insert(Vocabulary(word, pronunciation, pitch, language)) 51 | } 52 | val reacquiredID = runBlocking { 53 | db.vocabularyDao().getVocabularyID(word, pronunciation, pitch, language) 54 | } 55 | assertEquals(vocabularyID, reacquiredID) 56 | } 57 | 58 | @Test 59 | fun getVocabularyID_WordMismatch_ReturnsNull(){ 60 | val word = "1" 61 | val pronunciation = "" 62 | val pitch = "" 63 | val language = Language.JAPANESE 64 | runBlocking { 65 | db.vocabularyDao().insert(Vocabulary(word, pronunciation, pitch, language)) 66 | } 67 | val reacquiredID = runBlocking { 68 | db.vocabularyDao().getVocabularyID("One", pronunciation, pitch, language) 69 | } 70 | assertEquals(null, reacquiredID) 71 | } 72 | 73 | @Test 74 | fun getVocabularyID_PronunciationMismatch_ReturnsNull(){ 75 | val word = "1" 76 | val pronunciation = "" 77 | val pitch = "" 78 | val language = Language.JAPANESE 79 | runBlocking { 80 | db.vocabularyDao().insert(Vocabulary(word, pronunciation, pitch, language)) 81 | } 82 | val reacquiredID = runBlocking { 83 | db.vocabularyDao().getVocabularyID(word, "One", pitch, language) 84 | } 85 | assertEquals(null, reacquiredID) 86 | } 87 | 88 | @Test 89 | fun getVocabularyID_PitchMismatch_ReturnsNull(){ 90 | val word = "1" 91 | val pronunciation = "" 92 | val pitch = "" 93 | val language = Language.JAPANESE 94 | runBlocking { 95 | db.vocabularyDao().insert(Vocabulary(word, pronunciation, pitch, language)) 96 | } 97 | val reacquiredID = runBlocking { 98 | db.vocabularyDao().getVocabularyID(word, pronunciation, "1", language) 99 | } 100 | assertEquals(null, reacquiredID) 101 | } 102 | 103 | @Test 104 | fun getVocabularyID_LanguageMismatch_ReturnsNull(){ 105 | val word = "1" 106 | val pronunciation = "" 107 | val pitch = "" 108 | val language = Language.JAPANESE 109 | runBlocking { 110 | db.vocabularyDao().insert(Vocabulary(word, pronunciation, pitch, language)) 111 | } 112 | val reacquiredID = runBlocking { 113 | db.vocabularyDao().getVocabularyID(word, pronunciation, pitch, Language.ENGLISH) 114 | } 115 | assertEquals(null, reacquiredID) 116 | } 117 | } -------------------------------------------------------------------------------- /app/src/main/res/values/shared_preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PREF_DICTIONARY 5 | PREF_WORD_LANGUAGE 6 | PREF_DEFINITION_LANGUAGE 7 | PREF_DICTIONARY_MATCH_TYPE 8 | PREF_DATABASE_MATCH_TYPE 9 | PREF_AUTO_SAVE 10 | PREF_AUTO_DELETE 11 | 12 | lastSearched 13 | relatedWords 14 | desiredRelatedWord 15 | searchWord 16 | dicType 17 | pageUrl 18 | pageHtml 19 | webPage 20 | 21 | 22 | 1 23 | 1 24 | 1 25 | WORD_STARTS_WITH 26 | NEVER 27 | NEVER 28 | 29 | 30 | 31 | PREF_PREVIOUS_WORD 32 | 和日帳 33 | PREF_PREVIOUS_PRONUNCIATION 34 | わにっちょう 35 | PREF_PREVIOUS_PITCH 36 | 37 | PREF_PREVIOUS_WORD_LANGUAGE 38 | JAPANESE 39 | PREF_PREVIOUS_DEFINITION_TEXT 40 | タイトルバーを押して、検索できる辞書アプリ。\n 41 | Tap the title bar to begin entering a search term. \n 42 | Navigate to the settings screen from the menu bar at the top right. \n 43 | The floating action button will send the current definition to AnkiDroid. \n 44 | Tags will be imported to AnkiDroid. \n 45 | Vocabulary Notes will persist across the vocabulary term (for example, a note that is relevant to both 46 | the Japanese-Japanese definition and the Japanese-English definition for a given word.) \n 47 | Tapping on dark grey boxes will bring up a window to edit the text. \n 48 | Tapping on an entry in Related will attempt to search that word. \n 49 | Report bugs at github.com/Limegrass/Wanicchou/issues. \n 50 | 設定画面は右上隅のメニューに移られます。 \n 51 | FABを押して、カードは暗記ドロイドに送ります。 \n 52 | 単語のメモはどんな定義でも、そのメモが出てきます。 \n 53 | 普通の灰色より暗い灰色のところを押せば、そのテキストのエディットボックスが現れます。 \n 54 | バグが現れたら、github.com/Limegrass/Wanicchou/issues に新しいIssueを追加してください。 55 | PREF_PREVIOUS_DEFINITION_LANGUAGE 56 | JAPANESE 57 | PREF_PREVIOUS_DEFINITION_DICTIONARY 58 | SANSEIDO 59 | 60 | 61 | com.ichi2.anki.api.decks 62 | com.ichi2.anki.api.models 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/util/WanicchouSearchManager.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.util 2 | 3 | import android.net.ConnectivityManager 4 | import android.widget.Toast 5 | import androidx.fragment.app.FragmentActivity 6 | import androidx.lifecycle.ViewModelProviders 7 | import com.limegrass.wanicchou.R 8 | import com.limegrass.wanicchou.enums.AutoDelete 9 | import com.limegrass.wanicchou.viewmodel.DictionaryEntryViewModel 10 | import data.models.IDictionaryEntry 11 | import data.search.DictionarySearchManager 12 | import data.search.SearchRequest 13 | import data.architecture.IRepository 14 | import data.enums.Language 15 | import data.enums.MatchType 16 | import data.web.DictionarySearchProviderFactory 17 | import kotlinx.coroutines.Dispatchers 18 | import kotlinx.coroutines.GlobalScope 19 | import kotlinx.coroutines.launch 20 | 21 | //FIXME: Make this not garbage 22 | // Damn it became even worse 23 | // Decorator to handle deletion, 24 | // Decorator/Interface to handle connectivity checking 25 | // Handling the toast elsewhere 26 | class WanicchouSearchManager(private val repository : IRepository, 27 | private val connectivityManager: ConnectivityManager, 28 | private val sharedPreferences : WanicchouSharedPreferences, 29 | private val activity : FragmentActivity) { 30 | private val dictionaryEntryViewModel: DictionaryEntryViewModel by lazy { 31 | ViewModelProviders.of(activity) 32 | .get(DictionaryEntryViewModel::class.java) 33 | } 34 | 35 | suspend fun search(searchTerm : String, 36 | vocabularyLanguage : Language = sharedPreferences.vocabularyLanguage, 37 | definitionLanguage : Language = sharedPreferences.definitionLanguage, 38 | dictionaryMatchType : MatchType = sharedPreferences.dictionaryMatchType, 39 | databaseMatchType : MatchType = sharedPreferences.databaseMatchType) : List { 40 | 41 | activity.runOnUiThread{ 42 | Toast.makeText(activity, 43 | activity.getString(R.string.word_searching, 44 | searchTerm), 45 | Toast.LENGTH_LONG).show() 46 | } 47 | val searchManager = DictionarySearchManager() 48 | val databaseRequest = SearchRequest(searchTerm, 49 | vocabularyLanguage, 50 | definitionLanguage, 51 | dictionaryMatchType) 52 | searchManager.register(repository, databaseRequest) 53 | 54 | if(connectivityManager.activeNetworkInfo != null) { 55 | val dictionary = sharedPreferences.dictionary 56 | val dictionaryRequest = SearchRequest(searchTerm, 57 | vocabularyLanguage, 58 | definitionLanguage, 59 | databaseMatchType) 60 | val dictionarySource = DictionarySearchProviderFactory(dictionary).get() 61 | searchManager.register(dictionarySource, dictionaryRequest) 62 | } 63 | val searchResults = searchManager.executeSearches() 64 | if(searchResults.isNotEmpty()){ 65 | GlobalScope.launch(Dispatchers.IO){ 66 | val oldDictionaryEntry = dictionaryEntryViewModel.value 67 | activity.runOnUiThread { 68 | dictionaryEntryViewModel.availableDictionaryEntries = searchResults 69 | } 70 | val dictionaryEntry = dictionaryEntryViewModel.value!! 71 | if (oldDictionaryEntry != null 72 | && sharedPreferences.autoDelete == AutoDelete.ON_SEARCH){ 73 | repository.delete(oldDictionaryEntry) 74 | } 75 | activity.runOnUiThread { 76 | val message = activity.getString(R.string.word_search_success, 77 | searchTerm, 78 | dictionaryEntry.definitions[0].dictionary.dictionaryName) 79 | cancelSetAndShowWanicchouToast(activity, message, Toast.LENGTH_LONG) 80 | } 81 | for(result in searchResults){ 82 | repository.insert(result) 83 | } 84 | } 85 | 86 | } 87 | else { 88 | activity.runOnUiThread{ 89 | val message = activity.getString(R.string.word_search_failure, searchTerm) 90 | cancelSetAndShowWanicchouToast(activity, message, Toast.LENGTH_LONG) 91 | } 92 | } 93 | return searchResults 94 | } 95 | } -------------------------------------------------------------------------------- /app/src/main/res/values-jp/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 和日帳 3 | 4 | 5 | 6 | 設定 7 | 8 | 9 | 検索 10 | キーボードで入力して 11 | 12 | 単語 13 | 何か調べてみて 14 | 15 | 定義 16 | 単語が見つけませんでした。。。 17 | 18 | 「%1$s」検索中。。。 19 | %1$sは%2$sから出来ました! 20 | %1$sが見つけませんでした。。。 21 | 22 | 23 | 文脈 24 | どこで見つけました? 25 | 26 | メモ 27 | 記憶法など 28 | 29 | 30 | 暗記に送る 31 | 32 | 関係ある言葉へ 33 | 34 | 35 | 許可もらえませんでした 36 | %1$sは暗記に遅れました! 37 | 選択した単語の検索中。。。 38 | ピッチ: %1$s 39 | 40 | 41 | 42 | 関係ある言葉 43 | 44 | 45 | 検索設定 46 | 辞書型 47 | マッチ型 48 | オートセーブ 49 | オートデリート 50 | ピッチ 51 | 辞典型 52 | 読み方 53 | 辞典 54 | " インポート " 55 | 削除 56 | %1$d 単語をセレクトしています。 57 | データベース 58 | 定義 59 | 類語 60 | 単語 61 | 単語言語 62 | 定義言語 63 | 単語のメモ追加 64 | 単語のメモ 65 | タグ追加 66 | "エディット " 67 | タグ 68 | セーブ 69 | オートデリート 70 | 辞書・辞典 71 | 定義のメモ追加 72 | 定義のメモ 73 | 追加 74 | 削除 75 | メモ 76 | 単語言語 77 | 定義言語 78 | データベースマッチタイプ 79 | 辞書マッチタイプ 80 | ネットに繋がれませんでした。 81 | 単語のメモ 82 | 定義のメモ 83 | タグ 84 | 暗記ドロイドの保存許可は許していません。 85 | 意味 86 | 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/limegrass/wanicchou/ui/fragments/RelatedFragment.kt: -------------------------------------------------------------------------------- 1 | package com.limegrass.wanicchou.ui.fragments 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.fragment.app.Fragment 11 | import androidx.fragment.app.FragmentActivity 12 | import androidx.lifecycle.LifecycleOwner 13 | import androidx.lifecycle.ViewModelProviders 14 | import androidx.recyclerview.widget.RecyclerView 15 | import com.google.android.flexbox.FlexDirection 16 | import com.google.android.flexbox.FlexboxLayoutManager 17 | import com.google.android.flexbox.JustifyContent 18 | import com.limegrass.wanicchou.R 19 | import com.limegrass.wanicchou.ui.adapter.TextSpanRecyclerViewAdapter 20 | import com.limegrass.wanicchou.util.WanicchouSearchManager 21 | import com.limegrass.wanicchou.util.WanicchouSharedPreferences 22 | import com.limegrass.wanicchou.viewmodel.DictionaryEntryViewModel 23 | import room.database.WanicchouDatabase 24 | import room.repository.DictionaryEntryRepository 25 | import kotlinx.coroutines.Dispatchers 26 | import kotlinx.coroutines.runBlocking 27 | 28 | class RelatedFragment : Fragment() { 29 | companion object { 30 | private val TAG : String = RelatedFragment::class.java.simpleName 31 | } 32 | private val dictionaryEntryViewModel : DictionaryEntryViewModel by lazy { 33 | ViewModelProviders.of(parentFragmentActivity) 34 | .get(DictionaryEntryViewModel::class.java) 35 | } 36 | 37 | private val searchManager by lazy { 38 | val database = WanicchouDatabase(parentFragmentActivity) 39 | val repository = DictionaryEntryRepository(database) 40 | val connectivityManager = parentFragmentActivity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 41 | val sharedPreferences = WanicchouSharedPreferences(parentFragmentActivity) 42 | WanicchouSearchManager(repository, connectivityManager, sharedPreferences, parentFragmentActivity) 43 | } 44 | 45 | private lateinit var parentFragmentActivity : FragmentActivity 46 | override fun onCreateView(inflater: LayoutInflater, 47 | container: ViewGroup?, 48 | savedInstanceState: Bundle?): View? { 49 | val attachToRoot = false 50 | parentFragmentActivity = activity!! 51 | return inflater.inflate(R.layout.fragment_related, 52 | container, 53 | attachToRoot) 54 | } 55 | 56 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 57 | setRelatedObserver(view) 58 | super.onViewCreated(view, savedInstanceState) 59 | } 60 | 61 | private fun setRelatedObserver(view : View){ 62 | val context = context!! 63 | val lifecycleOwner : LifecycleOwner = this 64 | dictionaryEntryViewModel.setObserver(lifecycleOwner){ 65 | val recyclerView = view.findViewById(R.id.rv_related) 66 | Log.v(TAG, "LiveData emitted.") 67 | val dictionaryEntries = dictionaryEntryViewModel.availableDictionaryEntries 68 | if(!dictionaryEntries.isNullOrEmpty()){ 69 | val onClickListener = View.OnClickListener { v -> 70 | Log.v(TAG, "OnClick") 71 | val position = recyclerView.getChildLayoutPosition(v!!) 72 | val dictionaryEntry = dictionaryEntries[position] 73 | if(dictionaryEntry.definitions.isNotEmpty()){ 74 | dictionaryEntryViewModel.value = dictionaryEntry 75 | } else { 76 | runBlocking(Dispatchers.IO){ 77 | val searchResults = searchManager.search(dictionaryEntry.vocabulary.word) 78 | Log.v(TAG, "Result size: [${searchResults.size}].") 79 | } 80 | } 81 | } 82 | val layoutManager = FlexboxLayoutManager(context) 83 | layoutManager.flexDirection = FlexDirection.ROW 84 | layoutManager.justifyContent = JustifyContent.SPACE_AROUND 85 | val activeEntry = dictionaryEntryViewModel.value 86 | val relatedVocabularies = dictionaryEntries.filter { 87 | it != activeEntry 88 | }.map { 89 | "${it.vocabulary.word} [${it.vocabulary.pronunciation}]" 90 | } 91 | recyclerView.layoutManager = layoutManager 92 | recyclerView.adapter = TextSpanRecyclerViewAdapter(relatedVocabularies, onClickListener) 93 | } 94 | 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/data/anki/AnkiDroidApi.kt: -------------------------------------------------------------------------------- 1 | package data.anki 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.os.Build 6 | import android.util.SparseArray 7 | import androidx.core.content.ContextCompat 8 | import com.ichi2.anki.FlashCardsContract 9 | import com.ichi2.anki.api.AddContentApi 10 | import com.ichi2.anki.api.NoteInfo 11 | 12 | class AnkiDroidApi(private val context : Context) 13 | : IAnkiDroidApi { 14 | 15 | override val hasAvailableApi: Boolean 16 | get() = AddContentApi.getAnkiDroidPackageName(context) != null 17 | 18 | override val hasAnkiReadWritePermission: Boolean 19 | get() { 20 | val currentBuildVersion = Build.VERSION.SDK_INT 21 | val firstBuildRequiringPermissions = Build.VERSION_CODES.M 22 | val isBuildWithPermissionRequired = currentBuildVersion >= firstBuildRequiringPermissions 23 | return isBuildWithPermissionRequired && 24 | (ContextCompat.checkSelfPermission(context, FlashCardsContract.READ_WRITE_PERMISSION) 25 | == PackageManager.PERMISSION_GRANTED) 26 | } 27 | 28 | override val hasStoragePermission: Boolean 29 | get() { 30 | val packageManager = context.packageManager 31 | val ankiDroidPackageName = AddContentApi.getAnkiDroidPackageName(context) 32 | return packageManager.checkPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, 33 | ankiDroidPackageName) == PackageManager.PERMISSION_GRANTED 34 | } 35 | 36 | private val api = AddContentApi(context) 37 | 38 | override val modelList : Map 39 | get() = api.modelList 40 | override val deckList : Map 41 | get() = api.deckList 42 | override val currentModelID: Long 43 | get() = api.currentModelId 44 | override val apiHostSpecVersion: Int 45 | get() = api.apiHostSpecVersion 46 | override val selectedDeckName: String 47 | get() = api.selectedDeckName 48 | 49 | override fun addNewDeck(deckName: String): Long { 50 | return api.addNewDeck(deckName) 51 | } 52 | 53 | /** 54 | * null if deckID is not found. 55 | */ 56 | override fun getDeckName(deckID: Long): String? { 57 | return api.getDeckName(deckID) 58 | } 59 | 60 | /** 61 | * null if modelID is not found. 62 | */ 63 | override fun getModelName(modelID: Long): String? { 64 | return api.getModelName(modelID) 65 | } 66 | 67 | /** 68 | * null if modelID is not found. 69 | */ 70 | override fun getFieldList(modelID: Long): Array? { 71 | return api.getFieldList(modelID) 72 | } 73 | 74 | /** 75 | * true is note is found and updated 76 | */ 77 | override fun updateNoteFields(noteID: Long, fields: Array): Boolean { 78 | return api.updateNoteFields(noteID, fields) 79 | } 80 | 81 | /** 82 | * true is note is found and updated 83 | */ 84 | override fun updateNoteTags(noteID: Long, tags: Set): Boolean { 85 | return api.updateNoteTags(noteID, tags) 86 | } 87 | 88 | override fun findDuplicateNotes(modelID: Long, firstFieldValue: String): List { 89 | return api.findDuplicateNotes(modelID, firstFieldValue) 90 | } 91 | 92 | /** 93 | * Each index in the SparseArray corresponds to the index in the given list of keys 94 | */ 95 | override fun findDuplicateNotes(modelID: Long, firstFieldValues : List) 96 | : SparseArray> { 97 | return api.findDuplicateNotes(modelID, firstFieldValues) ?: SparseArray() 98 | } 99 | 100 | override fun addNewCustomModel(configuration : IAnkiDroidConfig, 101 | deckID : Long): Long { 102 | return api.addNewCustomModel(configuration.modelName, 103 | configuration.fields, 104 | configuration.cardFormats.map{ it.formatName }.toTypedArray(), 105 | configuration.cardFormats.map{ it.questionFormat }.toTypedArray(), 106 | configuration.cardFormats.map{ it.answerFormat }.toTypedArray(), 107 | configuration.css, 108 | deckID, 109 | configuration.sortField) 110 | } 111 | 112 | override fun addNote(modelID: Long, deckID: Long, fields: Array, tags: Set): Long { 113 | return api.addNote(modelID, deckID, fields, tags) 114 | } 115 | 116 | override fun getModelList(minFieldCount: Int): Map { 117 | return api.getModelList(minFieldCount) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/room/repository/DictionaryEntryRepository.kt: -------------------------------------------------------------------------------- 1 | package room.repository 2 | 3 | import androidx.annotation.WorkerThread 4 | import data.architecture.IRepository 5 | import data.enums.MatchType 6 | import data.models.IDictionaryEntry 7 | import data.search.SearchRequest 8 | import kotlinx.coroutines.runBlocking 9 | import room.database.WanicchouDatabase 10 | import room.dbo.entity.Definition 11 | import room.dbo.entity.Vocabulary 12 | 13 | class DictionaryEntryRepository(private val database : WanicchouDatabase) 14 | : IRepository { 15 | @WorkerThread 16 | override suspend fun search(request: SearchRequest) : List { 17 | val formattedSearchTerm = request.matchType.templateString.format(request.searchTerm) 18 | return when(request.matchType){ 19 | MatchType.WORD_EQUALS -> database.dictionaryEntryDao() 20 | .searchWordEqual(formattedSearchTerm, request.vocabularyLanguage, request.definitionLanguage) 21 | MatchType.WORD_STARTS_WITH -> database.dictionaryEntryDao() 22 | .searchWordLike(formattedSearchTerm, request.vocabularyLanguage, request.definitionLanguage) 23 | MatchType.WORD_ENDS_WITH -> database.dictionaryEntryDao() 24 | .searchWordLike(formattedSearchTerm, request.vocabularyLanguage, request.definitionLanguage) 25 | MatchType.WORD_CONTAINS -> database.dictionaryEntryDao() 26 | .searchWordLike(formattedSearchTerm, request.vocabularyLanguage, request.definitionLanguage) 27 | MatchType.WORD_WILDCARDS -> database.dictionaryEntryDao() 28 | .searchWordLike(formattedSearchTerm, request.vocabularyLanguage, request.definitionLanguage) 29 | MatchType.DEFINITION_CONTAINS -> database.dictionaryEntryDao() 30 | .searchDefinitionLike(formattedSearchTerm, request.vocabularyLanguage, request.definitionLanguage) 31 | MatchType.WORD_OR_DEFINITION_CONTAINS -> database.dictionaryEntryDao() 32 | .searchWordOrDefinitionLike(formattedSearchTerm, request.vocabularyLanguage, request.definitionLanguage) 33 | } 34 | } 35 | 36 | override suspend fun insert(entity: IDictionaryEntry) { 37 | val vocabularyID = Vocabulary.getVocabularyID(database, entity.vocabulary) ?: runBlocking { 38 | val vocabularyEntity = Vocabulary(entity.vocabulary) 39 | database.vocabularyDao().insert(vocabularyEntity) 40 | } 41 | 42 | for (definition in entity.definitions){ 43 | val definitionEntity = Definition(definition, vocabularyID) 44 | database.definitionDao().insert(definitionEntity) 45 | } 46 | } 47 | 48 | /** 49 | * Updates every vocabulary and definition entry provided. 50 | */ 51 | override suspend fun update(original: IDictionaryEntry, updated: IDictionaryEntry) { 52 | require(original.vocabulary == updated.vocabulary) 53 | val originalDefinitions = original.definitions 54 | val updatedDefinitions = updated.definitions 55 | require(originalDefinitions.size == updatedDefinitions.size) 56 | 57 | val vocabularyID = Vocabulary.getVocabularyID(database, original.vocabulary)!! 58 | val vocabularyEntity = Vocabulary(updated.vocabulary.word, 59 | updated.vocabulary.pronunciation, 60 | updated.vocabulary.pitch, 61 | updated.vocabulary.language, 62 | vocabularyID) 63 | database.vocabularyDao().update(vocabularyEntity) 64 | 65 | for (i in originalDefinitions.indices){ 66 | val definitionID = Definition.getDefinitionID(database, 67 | originalDefinitions[i], 68 | vocabularyID)!! 69 | val definitionEntity = Definition(updatedDefinitions[i].definitionText, 70 | updatedDefinitions[i].language, 71 | updatedDefinitions[i].dictionary, 72 | vocabularyID, 73 | definitionID) 74 | database.definitionDao().update(definitionEntity) 75 | } 76 | } 77 | 78 | /** 79 | * Deletes the linked definition from the database. The vocabulary row remains intact. 80 | */ 81 | override suspend fun delete(entity: IDictionaryEntry) { 82 | val vocabularyID = Vocabulary.getVocabularyID(database, entity.vocabulary)!! 83 | for (definition in entity.definitions) { 84 | val definitionID = Definition.getDefinitionID(database, definition, vocabularyID)!! 85 | val definitionEntity = Definition(definition.definitionText, 86 | definition.language, 87 | definition.dictionary, 88 | vocabularyID, 89 | definitionID) 90 | database.definitionDao().delete(definitionEntity) 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/test/java/data/anki/AnkiDroidConfigTest.kt: -------------------------------------------------------------------------------- 1 | package data.anki 2 | 3 | import data.enums.Dictionary 4 | import data.enums.Language 5 | import data.models.Definition 6 | import data.models.Vocabulary 7 | import org.junit.Test 8 | import kotlin.test.assertEquals 9 | 10 | class AnkiDroidConfigTest { 11 | @Test 12 | fun `mapToNoteField maps all fields to proper labels`() { 13 | val ankiEntry = ankiEntry 14 | val fields = AnkiDroidConfig.fields 15 | val fieldValues = AnkiDroidConfig.mapToNoteFields(ankiEntry) 16 | val wordIndex = fields.indexOf("Word") 17 | val vocabularyLanguageIndex = fields.indexOf("Word Language") 18 | val definitionIndex = fields.indexOf("Definition") 19 | val definitionLanguageIndex = fields.indexOf("Definition Language") 20 | val dictionaryIndex = fields.indexOf("Dictionary") 21 | val pronunciationIndex = fields.indexOf("Pronunciation") 22 | val pitchIndex = fields.indexOf("Pitch") 23 | val notesIndex = fields.indexOf("Notes") 24 | assertEquals(fieldValues[wordIndex], ankiEntry.vocabulary.word) 25 | assertEquals(fieldValues[pronunciationIndex], ankiEntry.vocabulary.pronunciation) 26 | assertEquals(fieldValues[vocabularyLanguageIndex], ankiEntry.vocabulary.language.languageCode) 27 | assertEquals(fieldValues[pitchIndex], ankiEntry.vocabulary.pitch) 28 | assertEquals(fieldValues[definitionIndex], ankiEntry.definition.definitionText) 29 | assertEquals(fieldValues[definitionLanguageIndex], ankiEntry.definition.language.languageCode) 30 | assertEquals(fieldValues[dictionaryIndex], ankiEntry.definition.dictionary.dictionaryName) 31 | 32 | for (note in ankiEntry.notes){ 33 | assert(fieldValues[notesIndex].contains(note)) 34 | } 35 | } 36 | 37 | @Test 38 | fun `mapFromNoteField maps to and from as expected`() { 39 | val ankiEntry = ankiEntry 40 | val fieldValues = AnkiDroidConfig.mapToNoteFields(ankiEntry) 41 | val mappedEntry = AnkiDroidConfig.mapFromNoteFields(fieldValues) 42 | assertEquals(mappedEntry.vocabulary.word, ankiEntry.vocabulary.word) 43 | assertEquals(mappedEntry.vocabulary.pronunciation, ankiEntry.vocabulary.pronunciation) 44 | assertEquals(mappedEntry.vocabulary.language, ankiEntry.vocabulary.language) 45 | assertEquals(mappedEntry.vocabulary.pitch, ankiEntry.vocabulary.pitch) 46 | assertEquals(mappedEntry.definition.definitionText, ankiEntry.definition.definitionText) 47 | assertEquals(mappedEntry.definition.language, ankiEntry.definition.language) 48 | assertEquals(mappedEntry.definition.dictionary, ankiEntry.definition.dictionary) 49 | 50 | assertEquals(ankiEntry.notes.size, mappedEntry.notes.size) 51 | for (i in ankiEntry.notes.indices){ 52 | assertEquals(ankiEntry.notes[i], mappedEntry.notes[i]) 53 | } 54 | } 55 | 56 | @Test 57 | fun `mapToNoteFields maps vocabulary word as first field`() { 58 | val ankiEntry = ankiEntry 59 | val fieldValues = AnkiDroidConfig.mapToNoteFields(ankiEntry) 60 | assertEquals(fieldValues[0], ankiEntry.vocabulary.word) 61 | } 62 | 63 | @Test 64 | fun `mapToNoteFields has Anki furigana format`() { 65 | val furiganaField = AnkiDroidConfig.fields.indexOf("Furigana") 66 | val fieldValues = AnkiDroidConfig.mapToNoteFields(ankiEntry) 67 | assertEquals("${ankiEntry.vocabulary.word}[${ankiEntry.vocabulary.pronunciation}]", 68 | fieldValues[furiganaField]) 69 | } 70 | 71 | @Test 72 | fun `mapToNoteFields has content of all notes`() { 73 | val notesField = AnkiDroidConfig.fields.indexOf("Notes") 74 | val fieldValues = AnkiDroidConfig.mapToNoteFields(ankiEntry) 75 | for (note in ankiEntry.notes){ 76 | assert(fieldValues[notesField].contains(note)) 77 | } 78 | } 79 | 80 | @Test 81 | fun `frontSideKey is same as first field name`() { 82 | assertEquals(AnkiDroidConfig.fields[0], AnkiDroidConfig.frontSideKey) 83 | } 84 | 85 | @Test 86 | fun `backSideKey is Definition`() { 87 | assertEquals("Definition", AnkiDroidConfig.backSideKey) 88 | } 89 | 90 | private val ankiEntry = run { 91 | val word = "テスト" 92 | val pronunciation = "てすと" 93 | val pitch = "3154" 94 | val vocabularyLanguage = Language.JAPANESE 95 | val vocabulary = Vocabulary(word, pronunciation, pitch, vocabularyLanguage) 96 | val definitionText = "this" 97 | val definitionLanguage = Language.ENGLISH 98 | val dictionary = Dictionary.SANSEIDO 99 | val definition = Definition(definitionText, definitionLanguage, dictionary) 100 | val notes = listOf("Whatever", "OK") 101 | WanicchouAnkiEntry(vocabulary, definition, notes) 102 | } 103 | } -------------------------------------------------------------------------------- /app/src/main/java/data/anki/AnkiDroidHelper.kt: -------------------------------------------------------------------------------- 1 | package data.anki 2 | 3 | /** 4 | * Helper to encompass the logic of an anki card add/update 5 | */ 6 | class AnkiDroidHelper(private val ankiDroidApi : IAnkiDroidApi, 7 | private val configuration : IAnkiDroidConfig, 8 | private val ankiIdStorage: IAnkiDroidConfigIdentifierStorage) { 9 | // 10 | fun addUpdateNote(ankiEntry : WanicchouAnkiEntry, 11 | tags: Set): Long { 12 | val existingNoteID = getExistingNoteID(ankiEntry) 13 | val fields = configuration.mapToNoteFields(ankiEntry) 14 | return if (existingNoteID == null){ 15 | ankiDroidApi.addNote(wanicchouModelID, wanicchouDeckID, fields, tags) 16 | } 17 | else{ 18 | ankiDroidApi.updateNoteFields(existingNoteID, fields) 19 | ankiDroidApi.updateNoteTags(existingNoteID, tags) 20 | existingNoteID 21 | } 22 | } 23 | // 24 | 25 | // 26 | /** 27 | * Returns an active model ID for the current configuration. Adds one if it does not exist. 28 | */ 29 | private val wanicchouDeckID: Long 30 | get() { 31 | return storedDeckID ?: run { 32 | val deckID = ankiNameMatchedDeckID ?: ankiDroidApi.addNewDeck(configuration.deckName) 33 | ankiIdStorage.addDeckID(configuration.deckName, deckID) 34 | return deckID 35 | } 36 | } 37 | // Helper property 38 | private val storedDeckID : Long? 39 | get () { 40 | val deckID = ankiIdStorage.getDeckID(configuration.deckName) ?: return null 41 | ankiDroidApi.getDeckName(deckID) ?: return null 42 | return deckID 43 | } 44 | 45 | /** 46 | * Returns an active model ID for the current configuration. Adds one if it does not exist. 47 | */ 48 | private val wanicchouModelID: Long 49 | get() { 50 | return storedModelID ?: run { 51 | val modelID = ankiNameMatchedModelID 52 | ?: ankiDroidApi.addNewCustomModel(configuration, wanicchouDeckID) 53 | ankiIdStorage.addModelID(configuration.modelName, modelID) 54 | return modelID 55 | } 56 | } 57 | private val storedModelID : Long? 58 | get(){ 59 | val minimumFieldCount = configuration.fields.size 60 | val modelID = ankiIdStorage.getModelID(configuration.modelName, 61 | minimumFieldCount) ?: return null 62 | ankiDroidApi.getModelName(modelID) ?: return null 63 | if(ankiDroidApi.getFieldList(modelID)!!.size >= minimumFieldCount) { 64 | return modelID 65 | } 66 | return null 67 | } 68 | // 69 | 70 | // 71 | //TODO: Get working duplicate checking 72 | private fun getExistingNoteID(ankiEntry: WanicchouAnkiEntry) : Long? { 73 | val existingNotes = ankiDroidApi.findDuplicateNotes(wanicchouModelID, ankiEntry.vocabulary.word) 74 | for (note in existingNotes) { 75 | val entryFromNote = configuration.mapFromNoteFields(note.fields) 76 | //All existing notes already have same word 77 | if (entryFromNote.vocabulary.language == ankiEntry.vocabulary.language 78 | && entryFromNote.definition.language == ankiEntry.definition.language 79 | && entryFromNote.definition.dictionary == ankiEntry.definition.dictionary 80 | && entryFromNote.vocabulary.pronunciation == ankiEntry.vocabulary.pronunciation 81 | && entryFromNote.definition.definitionText == ankiEntry.definition.definitionText) { 82 | return note.id 83 | } 84 | } 85 | return null 86 | } 87 | 88 | private val ankiNameMatchedModelID : Long? 89 | get() { 90 | val modelList = ankiDroidApi.getModelList(configuration.fields.size) 91 | val ignoreCase = true 92 | for ((modelID, ankiModelName) in modelList) { 93 | if (ankiModelName.equals(configuration.modelName, ignoreCase)) { 94 | return modelID 95 | } 96 | } 97 | return null 98 | } 99 | 100 | private val ankiNameMatchedDeckID : Long? 101 | get() { 102 | val deckList = ankiDroidApi.deckList 103 | val ignoreCase = true 104 | for ((deckID, ankiDeckName) in deckList) { 105 | if (ankiDeckName.contains(configuration.deckName, ignoreCase)) { 106 | return deckID 107 | } 108 | } 109 | return null 110 | } 111 | // 112 | } 113 | --------------------------------------------------------------------------------