├── .gitignore ├── .gitmodules ├── LICENSE ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── masterwok │ │ └── shrimplesearch │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── masterwok │ │ │ └── shrimplesearch │ │ │ ├── Maneki.kt │ │ │ ├── common │ │ │ ├── Config.kt │ │ │ ├── components │ │ │ │ └── ClearableAutoCompleteTextView.kt │ │ │ ├── constants │ │ │ │ ├── AnalyticEvent.kt │ │ │ │ └── Theme.kt │ │ │ ├── contracts │ │ │ │ ├── Configurable.kt │ │ │ │ └── ViewComponent.kt │ │ │ ├── data │ │ │ │ ├── models │ │ │ │ │ ├── PersistedUserSettings.kt │ │ │ │ │ └── UserSettings.kt │ │ │ │ ├── repositories │ │ │ │ │ ├── CardigannDefinitionRepository.kt │ │ │ │ │ ├── JackettServiceImpl.kt │ │ │ │ │ ├── SharedPreferencesUserSettingsRepository.kt │ │ │ │ │ └── contracts │ │ │ │ │ │ ├── JackettService.kt │ │ │ │ │ │ └── UserSettingsRepository.kt │ │ │ │ └── services │ │ │ │ │ ├── FirebaseAnalyticsService.kt │ │ │ │ │ └── contracts │ │ │ │ │ └── AnalyticService.kt │ │ │ ├── extensions │ │ │ │ ├── ActivityExtensions.kt │ │ │ │ ├── AppBarLayoutExtensions.kt │ │ │ │ ├── ContextExtensions.kt │ │ │ │ ├── IntExtensions.kt │ │ │ │ ├── LiveDataExtensions.kt │ │ │ │ ├── LongExtensions.kt │ │ │ │ └── ViewExtensions.kt │ │ │ ├── models │ │ │ │ ├── Event.kt │ │ │ │ └── EventState.kt │ │ │ └── utils │ │ │ │ ├── DialogUtil.kt │ │ │ │ └── Expressions.kt │ │ │ ├── di │ │ │ ├── AppInjector.kt │ │ │ ├── annotations │ │ │ │ └── ViewModelKey.kt │ │ │ ├── components │ │ │ │ └── AppComponent.kt │ │ │ ├── factories │ │ │ │ └── DaggerViewModelFactory.kt │ │ │ └── modules │ │ │ │ ├── AppSubcomponentModule.kt │ │ │ │ ├── JackettHarnessModule.kt │ │ │ │ ├── RepositoryModule.kt │ │ │ │ ├── ServiceModule.kt │ │ │ │ └── ViewModelFactoryModule.kt │ │ │ ├── features │ │ │ ├── about │ │ │ │ ├── di │ │ │ │ │ ├── AboutScope.kt │ │ │ │ │ └── AboutSubcomponent.kt │ │ │ │ └── fragments │ │ │ │ │ └── AboutFragment.kt │ │ │ ├── query │ │ │ │ ├── adapters │ │ │ │ │ ├── IndexerQueryResultsAdapter.kt │ │ │ │ │ ├── MaterialDIalogIconListItemAdapter.kt │ │ │ │ │ └── QueryResultsAdapter.kt │ │ │ │ ├── components │ │ │ │ │ └── SortComponent.kt │ │ │ │ ├── constants │ │ │ │ │ ├── IndexerQueryResultSortBy.kt │ │ │ │ │ ├── OrderBy.kt │ │ │ │ │ └── QueryResultSortBy.kt │ │ │ │ ├── di │ │ │ │ │ ├── QueryModule.kt │ │ │ │ │ ├── QueryScope.kt │ │ │ │ │ └── QuerySubcomponent.kt │ │ │ │ ├── fragments │ │ │ │ │ ├── IndexerQueryResultsFragment.kt │ │ │ │ │ └── QueryFragment.kt │ │ │ │ └── viewmodels │ │ │ │ │ └── QueryViewModel.kt │ │ │ ├── settings │ │ │ │ ├── di │ │ │ │ │ ├── SettingsModule.kt │ │ │ │ │ ├── SettingsScope.kt │ │ │ │ │ └── SettingsSubcomponent.kt │ │ │ │ ├── fragments │ │ │ │ │ └── SettingsFragment.kt │ │ │ │ └── viewmodels │ │ │ │ │ └── SettingsViewModel.kt │ │ │ └── splash │ │ │ │ ├── activities │ │ │ │ └── SplashActivity.kt │ │ │ │ ├── di │ │ │ │ ├── SplashModule.kt │ │ │ │ ├── SplashScope.kt │ │ │ │ └── SplashSubcomponent.kt │ │ │ │ ├── models │ │ │ │ └── BootstrapInfo.kt │ │ │ │ └── viewmodels │ │ │ │ └── SplashViewModel.kt │ │ │ └── main │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivityViewModel.kt │ │ │ └── di │ │ │ ├── MainModule.kt │ │ │ ├── MainScope.kt │ │ │ └── MainSubcomponent.kt │ └── res │ │ ├── color │ │ ├── button_text_color.xml │ │ ├── pill_text_color.xml │ │ └── radio_button_tint.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── background_button.xml │ │ ├── background_button_selected.xml │ │ ├── background_button_unselected.xml │ │ ├── background_edit_text_rounded.xml │ │ ├── background_indexer_query_result_rounded.xml │ │ ├── background_pill.xml │ │ ├── background_pill_selected.xml │ │ ├── background_pill_unselected.xml │ │ ├── background_query_auto_complete_popup.xml │ │ ├── background_stat.xml │ │ ├── cursor.xml │ │ ├── divider_flexbox.xml │ │ ├── divider_recycler_view.xml │ │ ├── ic_arrow_downward_black_24dp.xml │ │ ├── ic_arrow_upward_black_24dp.xml │ │ ├── ic_auto_complete_clear.xml │ │ ├── ic_baseline_open_in_new_24.xml │ │ ├── ic_baseline_share_24.xml │ │ ├── ic_chevron_right_black_24dp.xml │ │ ├── ic_content_copy_black_24dp.xml │ │ ├── ic_insert_drive_file_black_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_link_black_24dp.xml │ │ ├── ic_magnet_black.xml │ │ └── ic_sort_black_24dp.xml │ │ ├── font │ │ ├── eina_03_regular.ttf │ │ └── eina_03_semi_bold.ttf │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_splash.xml │ │ ├── component_sort_by.xml │ │ ├── fragment_about.xml │ │ ├── fragment_indexer_query_results.xml │ │ ├── fragment_query.xml │ │ ├── fragment_settings.xml │ │ ├── include_toolbar_maneki.xml │ │ ├── include_toolbar_query.xml │ │ ├── view_dialog_exit.xml │ │ ├── view_indexer_query_result_item.xml │ │ ├── view_material_dialog_icon_item.xml │ │ └── view_query_result_item.xml │ │ ├── menu │ │ ├── drawer_layout_menu.xml │ │ └── menu_sort.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ ├── ic_no_search_results.png │ │ ├── ic_search.png │ │ └── ic_shrimple_smile.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ ├── ic_no_search_results.png │ │ ├── ic_search.png │ │ └── ic_shrimple_smile.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ ├── ic_no_search_results.png │ │ ├── ic_search.png │ │ └── ic_shrimple_smile.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ ├── ic_no_search_results.png │ │ ├── ic_search.png │ │ └── ic_shrimple_smile.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ ├── ic_no_search_results.png │ │ ├── ic_search.png │ │ └── ic_shrimple_smile.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── masterwok │ └── shrimplesearch │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── xamarin ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── masterwok │ │ └── xamarin │ │ ├── JackettHarnessFactory.kt │ │ └── JackettHarnessFactoryImpl.kt │ └── res │ └── values │ └── strings.xml └── xamarininterface ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── masterwok │ └── xamarininterface │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── masterwok │ │ └── xamarininterface │ │ ├── contracts │ │ ├── ICardigannDefinitionRepository.kt │ │ ├── IJackettHarness.kt │ │ └── IJackettHarnessListener.kt │ │ ├── enums │ │ ├── IndexerQueryState.kt │ │ ├── IndexerType.kt │ │ └── QueryState.kt │ │ └── models │ │ ├── Indexer.kt │ │ ├── IndexerQueryResult.kt │ │ ├── Query.kt │ │ └── QueryResultItem.kt └── res │ └── values │ └── strings.xml └── test └── java └── com └── masterwok └── xamarininterface └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | .idea/ 16 | app/google-services.json 17 | app/release/ 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "jackett-harness"] 2 | path = jackett-harness 3 | url = git@github.com:masterwok/jackett-harness.git 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /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: 'com.google.firebase.crashlytics' 7 | apply plugin: 'kotlinx-serialization' 8 | 9 | 10 | android { 11 | compileSdkVersion project.compileSdkVersion 12 | buildToolsVersion "30.0.2" 13 | 14 | defaultConfig { 15 | applicationId "com.masterwok.shrimplesearch" 16 | minSdkVersion project.minSdkVersion 17 | targetSdkVersion project.targetSdkVersion 18 | versionCode 500053 19 | versionName "2.1.6" 20 | 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | 24 | buildTypes { 25 | debug { 26 | minifyEnabled false 27 | debuggable true 28 | resValue("bool", "is_firebase_analytics_enabled", "true") 29 | } 30 | release { 31 | minifyEnabled true 32 | debuggable false 33 | resValue("bool", "is_firebase_analytics_enabled", "false") 34 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 35 | } 36 | } 37 | 38 | compileOptions { 39 | sourceCompatibility JavaVersion.VERSION_1_8 40 | targetCompatibility JavaVersion.VERSION_1_8 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = JavaVersion.VERSION_1_8.toString() 45 | } 46 | 47 | aaptOptions { 48 | noCompress 'dll' 49 | } 50 | 51 | sourceSets { 52 | main { 53 | assets.srcDirs += '../jackett-harness/Jackett/src/Jackett.Common/Definitions/' 54 | } 55 | } 56 | } 57 | 58 | dependencies { 59 | implementation fileTree(include: ['*.jar'], dir: 'libs') 60 | 61 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 62 | 63 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 64 | testImplementation 'junit:junit:4.13' 65 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 67 | 68 | implementation project(':xamarin') 69 | 70 | def nav_version = '2.3.5' 71 | def lifecycle_version = '2.3.1' 72 | def dagger_version = '2.28.3' 73 | 74 | implementation 'androidx.appcompat:appcompat:1.3.1' 75 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0' 76 | 77 | implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" 78 | implementation "androidx.navigation:navigation-ui-ktx:$nav_version" 79 | 80 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 81 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" 82 | implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" 83 | 84 | implementation "com.google.dagger:dagger:$dagger_version" 85 | kapt "com.google.dagger:dagger-compiler:$dagger_version" 86 | 87 | implementation 'com.github.masterwok:MaterialProgressBar:e4cf00b62f' 88 | implementation 'com.afollestad.material-dialogs:core:3.3.0' 89 | implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0' 90 | implementation 'com.google.android:flexbox:2.0.1' 91 | implementation 'com.google.firebase:firebase-analytics-ktx:19.0.1' 92 | implementation 'com.google.firebase:firebase-crashlytics-ktx:18.2.1' 93 | 94 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // or "kotlin-stdlib-jdk8" 95 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC" // JVM dependency 96 | } 97 | -------------------------------------------------------------------------------- /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 | 23 | -dontobfuscate 24 | -dontshrink 25 | 26 | -keepattributes *Annotation*, InnerClasses 27 | -dontnote kotlinx.serialization.SerializationKt 28 | -keep,includedescriptorclasses class com.masterwok.shrimplesearch.**$$serializer { *; } 29 | -keepclassmembers class com.masterwok.shrimplesearch.common.data.models.*.** { 30 | *** Companion; 31 | } 32 | -keepclasseswithmembers class com.masterwok.shrimplesearch.common.data.models.*.** { 33 | kotlinx.serialization.KSerializer serializer(...); 34 | } 35 | 36 | -keepnames class * extends androidx.fragment.app.Fragment 37 | -keepnames class * extends androidx.fragment.app.FragmentActivity 38 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/masterwok/shrimplesearch/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.masterwok.shrimplesearch", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 20 | 21 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/Maneki.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch 2 | 3 | import android.app.Application 4 | import com.masterwok.shrimplesearch.di.AppInjector 5 | 6 | class Maneki : Application() { 7 | 8 | override fun onCreate() { 9 | super.onCreate() 10 | 11 | AppInjector.init(this) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/Config.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common 2 | 3 | import com.masterwok.shrimplesearch.common.constants.Theme 4 | import com.masterwok.shrimplesearch.common.data.models.UserSettings 5 | 6 | /** 7 | * The name of the application shared preferences. 8 | */ 9 | const val SHARED_PREFERENCES_NAME = "maenki.shared_preferences" 10 | 11 | /** 12 | * The unique identifier for the aggregate indexer. 13 | */ 14 | const val AGGREGATE_INDEXER_ID = "aggregate_indexer" 15 | 16 | /** 17 | * The indexer block list defines the unique identifiers of indexers blocked to comply with Google 18 | * store policy. 19 | */ 20 | val INDEXER_BLOCK_LIST = listOf( 21 | "empornium", 22 | "empornium2fa", 23 | "gay-torrents", 24 | "gay-torrentsorg", 25 | "gaytorrentru", 26 | "leporno", 27 | "lepornoinfo", 28 | "mypornclub", 29 | "mypornclub", 30 | "pornbay", 31 | "pornbits", 32 | "pornforall", 33 | "pornleech", 34 | "pornolive", 35 | "pornorip", 36 | "pornotor", 37 | "pussytorrents", 38 | "sexypics", 39 | "trupornolabs", 40 | "xxxadulttorrent", 41 | "xxxtor", 42 | "xxxtorrents" 43 | ) 44 | 45 | /** 46 | * The default user settings configuration. 47 | */ 48 | val DEFAULT_USER_SETTINGS = UserSettings( 49 | theme = Theme.Light, 50 | isScrollToTopNotificationsEnabled = true, 51 | isOnlyMagnetQueryResultItemsEnabled = false, 52 | isExitDialogEnabled = true 53 | ) 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/components/ClearableAutoCompleteTextView.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.components 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.os.Parcelable 7 | import android.util.AttributeSet 8 | import android.view.MotionEvent 9 | import androidx.appcompat.widget.AppCompatAutoCompleteTextView 10 | import androidx.core.content.ContextCompat 11 | import com.masterwok.shrimplesearch.R 12 | import com.masterwok.shrimplesearch.common.extensions.hideSoftKeyboard 13 | 14 | class ClearableAutoCompleteTextView : AppCompatAutoCompleteTextView { 15 | 16 | private var onTextClearedListener: (() -> Unit)? = null 17 | 18 | constructor(context: Context) : super(context) { 19 | onFinishInflate() 20 | } 21 | 22 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 23 | 24 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 25 | context, 26 | attrs, 27 | defStyleAttr 28 | ) 29 | 30 | override fun onFinishInflate() { 31 | super.onFinishInflate() 32 | 33 | setOnTouchListener(null) 34 | } 35 | 36 | override fun setOnFocusChangeListener(l: OnFocusChangeListener?) { 37 | super.setOnFocusChangeListener { view, isFocused -> 38 | setDrawableRight() 39 | l?.onFocusChange(view, isFocused) 40 | } 41 | } 42 | 43 | override fun setOnEditorActionListener(l: OnEditorActionListener?) { 44 | super.setOnEditorActionListener { view, actionId, keyEvent -> 45 | setDrawableRight() 46 | l?.onEditorAction(view, actionId, keyEvent) ?: false 47 | } 48 | } 49 | 50 | @SuppressLint("ClickableViewAccessibility") 51 | override fun setOnTouchListener(l: OnTouchListener?) { 52 | super.setOnTouchListener { view, motionEvent -> 53 | val drawableRight = compoundDrawables[2] 54 | 55 | if ( 56 | drawableRight != null 57 | && motionEvent.action == MotionEvent.ACTION_UP 58 | && motionEvent.x >= width - totalPaddingRight 59 | ) { 60 | text = null 61 | setDrawableRight() 62 | onTextClearedListener?.invoke() 63 | (context as? Activity)?.hideSoftKeyboard() 64 | true 65 | } else { 66 | l?.onTouch(view, motionEvent) ?: false 67 | } 68 | 69 | } 70 | } 71 | 72 | private fun setDrawableRight() = setCompoundDrawablesWithIntrinsicBounds( 73 | null, null, if (text.isNullOrEmpty()) { 74 | null 75 | } else { 76 | ContextCompat.getDrawable(context, R.drawable.ic_auto_complete_clear) 77 | }, null 78 | ) 79 | 80 | override fun onRestoreInstanceState(state: Parcelable?) { 81 | super.onRestoreInstanceState(state) 82 | 83 | setDrawableRight() 84 | } 85 | 86 | fun setOnTextClearedListener(listener: () -> Unit) { 87 | onTextClearedListener = listener 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/constants/AnalyticEvent.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.constants 2 | 3 | enum class AnalyticEvent(val eventName: String) { 4 | Search("search"), 5 | MenuItemSortTapped("menu_item_sort_tapped"), 6 | MenuItemAboutTapped("menu_item_about_tapped"), 7 | OpenResult("result_open"), 8 | CopyResult("result_copy"), 9 | ShareResult("result_share"), 10 | ShareManeki("share_maneki") 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/constants/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.constants 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | enum class Theme(val id: Int) { 7 | Light(0), 8 | Oled(1); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/contracts/Configurable.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.contracts 2 | 3 | interface Configurable { 4 | 5 | fun configure(model: T) 6 | 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/contracts/ViewComponent.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.contracts 2 | 3 | /** 4 | * This contract provides a pattern to follow when configuring and reading view state from custom 5 | * view components. 6 | */ 7 | internal interface ViewComponent : Configurable { 8 | 9 | /** 10 | * Get the current view component state. 11 | */ 12 | fun getModel(): T 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/data/models/PersistedUserSettings.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.data.models 2 | 3 | import com.masterwok.shrimplesearch.common.constants.Theme 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class PersistedUserSettings( 8 | val theme: Theme, 9 | val isScrollToTopNotificationsEnabled: Boolean? = false, 10 | val isOnlyMagnetQueryResultItemsEnabled: Boolean? = false, 11 | val isExitDialogEnabled: Boolean? = false 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/data/models/UserSettings.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.data.models 2 | 3 | import com.masterwok.shrimplesearch.common.constants.Theme 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class UserSettings( 8 | val theme: Theme, 9 | val isScrollToTopNotificationsEnabled: Boolean, 10 | val isOnlyMagnetQueryResultItemsEnabled: Boolean, 11 | val isExitDialogEnabled: Boolean 12 | ) { 13 | companion object 14 | } 15 | 16 | fun UserSettings.Companion.from( 17 | persistedUserSettings: PersistedUserSettings, 18 | defaultUserSettings: UserSettings 19 | ) = UserSettings( 20 | theme = persistedUserSettings.theme, 21 | isScrollToTopNotificationsEnabled = persistedUserSettings.isScrollToTopNotificationsEnabled 22 | ?: defaultUserSettings.isScrollToTopNotificationsEnabled, 23 | isOnlyMagnetQueryResultItemsEnabled = persistedUserSettings.isOnlyMagnetQueryResultItemsEnabled 24 | ?: defaultUserSettings.isOnlyMagnetQueryResultItemsEnabled, 25 | isExitDialogEnabled = persistedUserSettings.isExitDialogEnabled 26 | ?: defaultUserSettings.isExitDialogEnabled 27 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/CardigannDefinitionRepository.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.data.repositories 2 | 3 | import android.content.Context 4 | import android.content.res.AssetManager 5 | import com.masterwok.xamarininterface.contracts.ICardigannDefinitionRepository 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.SupervisorJob 9 | import kotlinx.coroutines.runBlocking 10 | import java.io.BufferedReader 11 | import java.io.InputStreamReader 12 | import javax.inject.Inject 13 | 14 | class CardigannDefinitionRepository @Inject constructor( 15 | private val appContext: Context 16 | ) : ICardigannDefinitionRepository 17 | , CoroutineScope by CoroutineScope(Dispatchers.IO + SupervisorJob()) { 18 | 19 | private val assetManager: AssetManager by lazy { 20 | appContext.assets 21 | } 22 | 23 | override fun getDefinitions(): List = runBlocking { 24 | return@runBlocking readDefinitionPaths().map( 25 | this@CardigannDefinitionRepository::synchronousReadDefinitions 26 | ) 27 | } 28 | 29 | // Why synchronous: https://www.remlab.net/op/nonblock.shtml 30 | private fun synchronousReadDefinitions( 31 | path: String 32 | ): String = BufferedReader(InputStreamReader(assetManager.open(path))).let { 33 | val text = it.readText() 34 | it.close() 35 | text 36 | } 37 | 38 | override fun getIndexerCount(): Int = readDefinitionPaths().count() 39 | 40 | private fun readDefinitionPaths(): List { 41 | val assets = assetManager 42 | .list("") 43 | ?: emptyArray() 44 | 45 | return assets.filter { it.endsWith("yml") } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/JackettServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.data.repositories 2 | 3 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.JackettService 4 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository 5 | import com.masterwok.shrimplesearch.common.utils.notNull 6 | import com.masterwok.xamarininterface.enums.QueryState 7 | import com.masterwok.xamarininterface.contracts.IJackettHarness 8 | import com.masterwok.xamarininterface.contracts.IJackettHarnessListener 9 | import com.masterwok.xamarininterface.models.IndexerQueryResult 10 | import com.masterwok.xamarininterface.models.Query 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.ExperimentalCoroutinesApi 13 | import kotlinx.coroutines.withContext 14 | import java.lang.ref.WeakReference 15 | 16 | class JackettServiceImpl constructor( 17 | private val jackettHarness: IJackettHarness, 18 | private val userSettingsRepository: UserSettingsRepository, 19 | private val indexerBlockList: List 20 | ) : JackettService { 21 | 22 | private val jackettHarnessListener: IJackettHarnessListener = JackettHarnessListener(this) 23 | 24 | private val listeners = mutableListOf() 25 | 26 | private val userSettings get() = userSettingsRepository.read() 27 | 28 | init { 29 | jackettHarness.setListener(jackettHarnessListener) 30 | } 31 | 32 | override val isInitialized: Boolean get() = jackettHarness.isInitialized 33 | 34 | override val queryState: QueryState? get() = jackettHarness.queryState 35 | 36 | override val queryResults: List 37 | get() = jackettHarness 38 | .queryResults 39 | .filterNot { indexerBlockList.contains(it.indexer.id) } 40 | .map { indexerQueryResult -> 41 | if (!userSettings.isOnlyMagnetQueryResultItemsEnabled) { 42 | indexerQueryResult 43 | } else { 44 | indexerQueryResult.copy( 45 | items = indexerQueryResult 46 | .items 47 | .filterNot { it.linkInfo.magnetUri == null }, 48 | linkCount = 0 49 | ) 50 | } 51 | } 52 | 53 | @ExperimentalCoroutinesApi 54 | override suspend fun initialize() = withContext(Dispatchers.IO) { 55 | if (!isInitialized) { 56 | jackettHarness.initialize() 57 | } 58 | } 59 | 60 | override suspend fun query(query: Query) = withContext(Dispatchers.IO) { 61 | jackettHarness.cancelQuery() 62 | jackettHarness.query(query) 63 | } 64 | 65 | override suspend fun cancelQuery() = withContext(Dispatchers.IO) { 66 | jackettHarness.cancelQuery() 67 | } 68 | 69 | override suspend fun getIndexerCount(): Int = withContext(Dispatchers.IO) { 70 | jackettHarness.getIndexerCount() 71 | } 72 | 73 | override fun addListener(listener: JackettService.Listener) { 74 | listeners.add(listener) 75 | } 76 | 77 | override fun removeListener(listener: JackettService.Listener) { 78 | listeners.remove(listener) 79 | } 80 | 81 | private class JackettHarnessListener(jackettService: JackettServiceImpl) : 82 | IJackettHarnessListener { 83 | 84 | private val weakJackettService = WeakReference(jackettService) 85 | 86 | override fun onIndexersInitialized() = weakJackettService.get().notNull { jackettService -> 87 | jackettService.listeners.forEach { it.onIndexersInitialized() } 88 | } 89 | 90 | override fun onIndexerInitialized() = weakJackettService.get().notNull { jackettService -> 91 | jackettService.listeners.forEach { it.onIndexerInitialized() } 92 | } 93 | 94 | override fun onResultsUpdated() = weakJackettService.get().notNull { jackettService -> 95 | if (jackettService.queryState != QueryState.Aborted) { 96 | jackettService.listeners.forEach { it.onResultsUpdated() } 97 | } 98 | } 99 | 100 | override fun onQueryStateChange(queryState: QueryState) = 101 | weakJackettService.get().notNull { jackettService -> 102 | jackettService.listeners.forEach { it.onQueryStateChange(queryState) } 103 | } 104 | } 105 | 106 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/SharedPreferencesUserSettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.data.repositories 2 | 3 | import android.content.Context 4 | import com.masterwok.shrimplesearch.R 5 | import com.masterwok.shrimplesearch.common.constants.Theme 6 | import com.masterwok.shrimplesearch.common.data.models.UserSettings 7 | import com.masterwok.shrimplesearch.common.data.models.from 8 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository 9 | import kotlinx.serialization.decodeFromString 10 | import kotlinx.serialization.encodeToString 11 | import kotlinx.serialization.json.Json 12 | import javax.inject.Inject 13 | import javax.inject.Named 14 | 15 | class SharedPreferencesUserSettingsRepository @Inject constructor( 16 | appContext: Context, 17 | @Named("shared_preferences_name") sharedPreferencesName: String, 18 | @Named("default_user_settings") private val defaultUserSettings: UserSettings 19 | ) : UserSettingsRepository { 20 | 21 | private val sharedPreferences = appContext.getSharedPreferences( 22 | sharedPreferencesName, 23 | Context.MODE_PRIVATE 24 | ) 25 | 26 | override fun read(): UserSettings { 27 | val serialized = sharedPreferences 28 | .getString(NAME_USER_SETTINGS, null) 29 | ?: return defaultUserSettings 30 | 31 | return try { 32 | UserSettings.from( 33 | Json { ignoreUnknownKeys = true }.decodeFromString(serialized), 34 | defaultUserSettings 35 | ) 36 | } catch (ignored: Exception) { 37 | defaultUserSettings 38 | } 39 | } 40 | 41 | override fun update(userSettings: UserSettings) { 42 | sharedPreferences 43 | .edit() 44 | .putString(NAME_USER_SETTINGS, Json.encodeToString(userSettings)) 45 | .apply() 46 | } 47 | 48 | override fun getThemeId(): Int = when (read().theme) { 49 | Theme.Light -> R.style.AppTheme 50 | Theme.Oled -> R.style.AppTheme_Oled 51 | } 52 | 53 | override fun getSplashThemeId() = when (read().theme) { 54 | Theme.Light -> R.style.AppTheme_Splash 55 | Theme.Oled -> R.style.AppTheme_Splash_Oled 56 | } 57 | 58 | companion object { 59 | private const val NAME_USER_SETTINGS = "preference.user_settings" 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/contracts/JackettService.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.data.repositories.contracts 2 | 3 | import com.masterwok.xamarininterface.enums.QueryState 4 | import com.masterwok.xamarininterface.models.IndexerQueryResult 5 | import com.masterwok.xamarininterface.models.Query 6 | 7 | 8 | interface JackettService { 9 | 10 | val isInitialized: Boolean 11 | 12 | val queryState: QueryState? 13 | 14 | val queryResults: List 15 | 16 | suspend fun initialize() 17 | 18 | suspend fun query(query: Query) 19 | 20 | suspend fun cancelQuery() 21 | 22 | suspend fun getIndexerCount(): Int 23 | 24 | fun addListener(listener: Listener) 25 | 26 | fun removeListener(listener: Listener) 27 | 28 | interface Listener { 29 | 30 | fun onIndexersInitialized() 31 | 32 | fun onIndexerInitialized() 33 | 34 | fun onResultsUpdated() 35 | 36 | fun onQueryStateChange(queryState: QueryState) 37 | 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/contracts/UserSettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.data.repositories.contracts 2 | 3 | import com.masterwok.shrimplesearch.common.data.models.UserSettings 4 | 5 | interface UserSettingsRepository { 6 | 7 | fun read(): UserSettings 8 | 9 | fun update(userSettings: UserSettings) 10 | 11 | fun getThemeId(): Int 12 | 13 | fun getSplashThemeId(): Int 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/data/services/FirebaseAnalyticsService.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.data.services 2 | 3 | import android.os.Bundle 4 | import com.google.firebase.analytics.FirebaseAnalytics 5 | import com.google.firebase.analytics.ktx.analytics 6 | import com.google.firebase.crashlytics.FirebaseCrashlytics 7 | import com.google.firebase.ktx.Firebase 8 | import com.masterwok.shrimplesearch.common.constants.AnalyticEvent 9 | import com.masterwok.shrimplesearch.common.data.services.contracts.AnalyticService 10 | import javax.inject.Inject 11 | 12 | 13 | class FirebaseAnalyticsService @Inject constructor() : AnalyticService { 14 | 15 | private val firebaseAnalytics: FirebaseAnalytics by lazy { 16 | Firebase.analytics 17 | } 18 | 19 | private val crashlytics: FirebaseCrashlytics by lazy { 20 | FirebaseCrashlytics.getInstance() 21 | } 22 | 23 | override fun logEvent(event: AnalyticEvent) = firebaseAnalytics.logEvent(event.eventName, null) 24 | 25 | override fun logException(exception: Exception, message: String?) { 26 | message?.let(crashlytics::log) 27 | crashlytics.recordException(exception) 28 | } 29 | 30 | override fun logScreen(screenClass: Class<*>) { 31 | firebaseAnalytics.logEvent( 32 | FirebaseAnalytics.Event.SCREEN_VIEW, Bundle().apply { 33 | putString(FirebaseAnalytics.Param.SCREEN_NAME, screenClass.simpleName) 34 | } 35 | ) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/data/services/contracts/AnalyticService.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.data.services.contracts 2 | 3 | import com.masterwok.shrimplesearch.common.constants.AnalyticEvent 4 | import java.lang.Exception 5 | 6 | interface AnalyticService { 7 | fun logEvent(event: AnalyticEvent) 8 | fun logException(exception: Exception, message: String? = null) 9 | fun logScreen(screenClass: Class<*>) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/extensions/ActivityExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.extensions 2 | 3 | import android.app.Activity 4 | import android.view.View 5 | import android.view.inputmethod.InputMethodManager 6 | 7 | 8 | /** 9 | * Because hiding the keyboard on Android is a tedious process, 10 | * this is an extension which does that for us. 11 | */ 12 | internal fun Activity.hideSoftKeyboard() { 13 | (getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager).run { 14 | val view = currentFocus ?: View(this@hideSoftKeyboard) 15 | 16 | hideSoftInputFromWindow(view.windowToken, 0) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/extensions/AppBarLayoutExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.extensions 2 | 3 | import androidx.appcompat.widget.Toolbar 4 | import androidx.coordinatorlayout.widget.CoordinatorLayout 5 | import com.google.android.material.appbar.AppBarLayout 6 | 7 | fun AppBarLayout.disableScroll(toolbar: Toolbar) { 8 | toolbar.layoutParams = (toolbar.layoutParams as AppBarLayout.LayoutParams).apply { 9 | scrollFlags = 0 10 | } 11 | 12 | layoutParams = (layoutParams as CoordinatorLayout.LayoutParams).apply { 13 | behavior = null 14 | } 15 | } 16 | 17 | fun AppBarLayout.enableScroll(toolbar: Toolbar, scrollFlags: Int) { 18 | toolbar.layoutParams = (toolbar.layoutParams as AppBarLayout.LayoutParams).apply { 19 | this.scrollFlags = scrollFlags 20 | } 21 | 22 | layoutParams = (layoutParams as CoordinatorLayout.LayoutParams).apply { 23 | behavior = AppBarLayout.Behavior() 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/extensions/ContextExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.extensions 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ClipData 5 | import android.content.ClipboardManager 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.net.Uri 9 | import android.util.TypedValue 10 | import androidx.annotation.AttrRes 11 | import androidx.annotation.RequiresApi 12 | import androidx.core.content.ContextCompat 13 | import androidx.core.os.ConfigurationCompat 14 | import java.text.NumberFormat 15 | import java.util.* 16 | 17 | 18 | fun Context.getCurrentLocale(): Locale = ConfigurationCompat 19 | .getLocales(resources.configuration) 20 | .get(0) 21 | 22 | fun Context.getLocaleNumberFormat(): NumberFormat = NumberFormat 23 | .getNumberInstance(getCurrentLocale()) 24 | 25 | @RequiresApi(api = android.os.Build.VERSION_CODES.LOLLIPOP) 26 | fun Context.getPlayStoreUri(): Uri = Uri 27 | .parse("http://play.google.com/store/apps/details?id=$packageName") 28 | 29 | @SuppressLint("ObsoleteSdkInt") 30 | fun Context.startPlayStoreActivity() { 31 | 32 | val intent = if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { 33 | Intent( 34 | Intent.ACTION_VIEW, 35 | getPlayStoreUri() 36 | ) 37 | } else { 38 | Intent( 39 | Intent.ACTION_VIEW, 40 | Uri.parse("market://details?id=$packageName") 41 | ).apply { 42 | flags = Intent.FLAG_ACTIVITY_NO_HISTORY or 43 | Intent.FLAG_ACTIVITY_NEW_DOCUMENT or 44 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK 45 | } 46 | } 47 | 48 | startActivity(intent) 49 | } 50 | 51 | 52 | fun Context.getColorByAttribute(@AttrRes attributeResourceId: Int): Int = TypedValue().apply { 53 | theme.resolveAttribute( 54 | attributeResourceId, 55 | this, 56 | true 57 | ) 58 | }.data 59 | 60 | 61 | fun Context.copyToClipboard(label: String, text: String) { 62 | (getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).apply { 63 | setPrimaryClip(ClipData(label, emptyArray(), ClipData.Item(text))) 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/extensions/IntExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.extensions 2 | 3 | import android.content.Context 4 | 5 | 6 | /** 7 | * Convert value from device pixel unit to pixel unit. 8 | * 9 | * @param context the current context. 10 | * @return the equivalent pixel unit. 11 | */ 12 | fun Int.dpToPx(context: Context): Int = (this * context.resources.displayMetrics.density).toInt() 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/extensions/LiveDataExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.extensions 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.MutableLiveData 5 | import com.masterwok.shrimplesearch.common.models.* 6 | import kotlinx.coroutines.Deferred 7 | import kotlinx.coroutines.async 8 | import kotlinx.coroutines.coroutineScope 9 | import kotlinx.coroutines.delay 10 | import java.lang.Exception 11 | 12 | /** 13 | * Track an operation through [Event] states. 14 | */ 15 | internal suspend fun MutableLiveData>.trackEvent( 16 | block: suspend () -> T 17 | ): T? = coroutineScope { 18 | postValue(Event(EventPending)) 19 | 20 | var networkFailure: Event? = null 21 | 22 | val deferred: Deferred = async { 23 | try { 24 | block() 25 | } catch (ex: Exception) { 26 | networkFailure = Event(EventFailure(ex.message ?: "Error")) 27 | Log.e("TRACK_REQUEST", "Async call failed", ex) 28 | null 29 | } 30 | } 31 | 32 | delay(1000) 33 | 34 | deferred.await().also { 35 | postValue(networkFailure ?: Event(EventSuccess)) 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/extensions/LongExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.extensions 2 | 3 | import java.text.DecimalFormat 4 | import kotlin.math.ln 5 | import kotlin.math.pow 6 | 7 | private val decimalFormat = DecimalFormat("#,###.00") 8 | 9 | /** 10 | * Convert the [Long] to a human readable unit string. Similar to doing `ls -h` in bash. 11 | */ 12 | fun Long.toHumanReadableByteCount( 13 | si: Boolean = false 14 | ): String { 15 | val unit = if (si) 1000 else 1024 16 | 17 | if (this < unit) return toString() + " B" 18 | 19 | val exp = (ln(toDouble()) / ln(unit.toDouble())).toInt() 20 | 21 | val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i" 22 | val value = this / unit.toDouble().pow(exp.toDouble()) 23 | 24 | return String.format( 25 | "%s %sB" 26 | , decimalFormat.format(value) 27 | , pre 28 | ) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/extensions/ViewExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.extensions 2 | 3 | import android.view.View 4 | import com.google.android.material.snackbar.Snackbar 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.channels.awaitClose 7 | import kotlinx.coroutines.flow.callbackFlow 8 | 9 | internal fun View.showSnackbar( 10 | message: CharSequence, 11 | length: Int, 12 | actionMessage: CharSequence? = null, 13 | backgroundColor: Int, 14 | textColor: Int, 15 | action: ((View) -> Unit)? = null 16 | ) = Snackbar.make(this, message, length).apply { 17 | view.setBackgroundColor(backgroundColor) 18 | 19 | setTextColor(textColor) 20 | 21 | if (actionMessage != null && action != null) { 22 | setAction(actionMessage, action) 23 | setActionTextColor(textColor) 24 | } 25 | 26 | show() 27 | } 28 | 29 | @ExperimentalCoroutinesApi 30 | fun View.onClicked() = callbackFlow { 31 | setOnClickListener { offer(Unit) } 32 | awaitClose { setOnClickListener(null) } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/models/Event.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.models 2 | 3 | /** 4 | * This class represents some emitted event. 5 | */ 6 | class Event(private val data: T) { 7 | 8 | /** 9 | * Whether or not the event has been consumed. 10 | */ 11 | var isConsumed = false 12 | private set 13 | 14 | /** 15 | * Consume the [Event]. An event can only be consumed once; subsequent calls will return null. 16 | * Use [peek] to see the consumed [data] of the [Event]. 17 | */ 18 | fun consume(): T? { 19 | if (isConsumed) { 20 | return null 21 | } 22 | 23 | isConsumed = true 24 | 25 | return data 26 | } 27 | 28 | /** 29 | * Get the [data] of the [Event] regardless of whether or not the [Event] has already been consumed. 30 | */ 31 | fun peek() = data 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/models/EventState.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.models 2 | 3 | /** 4 | * Represents the event state of an operation. 5 | */ 6 | sealed class EventState 7 | 8 | /** 9 | * Indicates that the event is currently pending (performing some operation). 10 | */ 11 | object EventPending : EventState() 12 | 13 | /** 14 | * Indicates that a pending event completed successfully. 15 | */ 16 | object EventSuccess : EventState() 17 | 18 | /** 19 | * Indicates that a pending event failed due to the [errorMessage]. 20 | */ 21 | class EventFailure(val errorMessage: CharSequence) : EventState() 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/utils/DialogUtil.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.utils 2 | 3 | import android.content.Context 4 | import com.afollestad.materialdialogs.MaterialDialog 5 | import com.afollestad.materialdialogs.customview.customView 6 | import com.masterwok.shrimplesearch.R 7 | import com.masterwok.shrimplesearch.features.query.components.SortComponent 8 | 9 | object DialogUtil { 10 | 11 | fun presentSortDialog( 12 | context: Context, 13 | sortComponentModel: SortComponent.Model, 14 | onDialogDismiss: (SortComponent.Model) -> Unit 15 | ) { 16 | val sortComponent = SortComponent(context).apply { 17 | configure(sortComponentModel) 18 | } 19 | 20 | MaterialDialog(context).show { 21 | customView(view = sortComponent) 22 | positiveButton { 23 | title(res = R.string.button_done) 24 | onDialogDismiss(sortComponent.getModel()) 25 | } 26 | negativeButton { 27 | title(res = R.string.button_cancel) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/common/utils/Expressions.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.common.utils 2 | 3 | /** 4 | * Invoke the provided [block] if and only if it's not null. 5 | */ 6 | internal inline fun T?.notNull(block: (T) -> Unit) { 7 | this?.let(block) 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/di/AppInjector.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.di 2 | 3 | import android.app.Application 4 | import com.masterwok.shrimplesearch.di.components.AppComponent 5 | import com.masterwok.shrimplesearch.di.components.DaggerAppComponent 6 | import com.masterwok.shrimplesearch.main.MainActivity 7 | 8 | class AppInjector { 9 | 10 | companion object { 11 | 12 | private lateinit var application: Application 13 | 14 | private val appComponent: AppComponent by lazy { 15 | DaggerAppComponent 16 | .factory() 17 | .create(application.applicationContext) 18 | } 19 | 20 | val mainComponent by lazy { 21 | appComponent.mainComponent().create() 22 | } 23 | 24 | val queryComponent by lazy { 25 | appComponent.queryComponent().create() 26 | } 27 | 28 | val splashComponent by lazy { 29 | appComponent.splashComponent().create() 30 | } 31 | 32 | val aboutComponent by lazy { 33 | appComponent.aboutComponent().create() 34 | } 35 | 36 | val settingsComponent by lazy { 37 | appComponent.settingsComponent().create() 38 | } 39 | 40 | fun init(application: Application) { 41 | this.application = application 42 | } 43 | 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/di/annotations/ViewModelKey.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.di.annotations 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.MapKey 5 | import kotlin.reflect.KClass 6 | 7 | @Target( 8 | AnnotationTarget.FUNCTION 9 | , AnnotationTarget.PROPERTY_GETTER 10 | , AnnotationTarget.PROPERTY_SETTER 11 | ) 12 | @MapKey 13 | annotation class ViewModelKey(val value: KClass) -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/di/components/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.di.components 2 | 3 | import android.content.Context 4 | import com.masterwok.shrimplesearch.di.modules.AppSubcomponentModule 5 | import com.masterwok.shrimplesearch.di.modules.RepositoryModule 6 | import com.masterwok.shrimplesearch.di.modules.ServiceModule 7 | import com.masterwok.shrimplesearch.di.modules.ViewModelFactoryModule 8 | import com.masterwok.shrimplesearch.features.about.di.AboutSubcomponent 9 | import com.masterwok.shrimplesearch.features.query.di.QuerySubcomponent 10 | import com.masterwok.shrimplesearch.features.settings.di.SettingsSubcomponent 11 | import com.masterwok.shrimplesearch.features.splash.di.SplashSubcomponent 12 | import com.masterwok.shrimplesearch.main.MainActivity 13 | import com.masterwok.shrimplesearch.main.di.MainSubcomponent 14 | import dagger.BindsInstance 15 | import dagger.Component 16 | import javax.inject.Singleton 17 | 18 | @Singleton 19 | @Component( 20 | modules = [ 21 | ViewModelFactoryModule::class, 22 | AppSubcomponentModule::class, 23 | RepositoryModule::class, 24 | ServiceModule::class 25 | ] 26 | ) 27 | interface AppComponent { 28 | @Component.Factory 29 | interface Factory { 30 | fun create(@BindsInstance context: Context): AppComponent 31 | } 32 | 33 | fun mainComponent(): MainSubcomponent.Factory 34 | 35 | fun queryComponent(): QuerySubcomponent.Factory 36 | 37 | fun splashComponent(): SplashSubcomponent.Factory 38 | 39 | fun aboutComponent(): AboutSubcomponent.Factory 40 | 41 | fun settingsComponent(): SettingsSubcomponent.Factory 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/di/factories/DaggerViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.di.factories 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import javax.inject.Inject 6 | import javax.inject.Provider 7 | 8 | @Suppress("UNCHECKED_CAST") 9 | class DaggerViewModelFactory @Inject constructor( 10 | private val viewModelsMap: Map, @JvmSuppressWildcards Provider> 11 | ) : ViewModelProvider.Factory { 12 | 13 | override fun create(modelClass: Class): T { 14 | val creator = viewModelsMap[modelClass] 15 | ?: viewModelsMap 16 | .asIterable() 17 | .firstOrNull { modelClass.isAssignableFrom(it.key) } 18 | ?.value 19 | ?: throw IllegalArgumentException("Unknown ViewModel class: $modelClass") 20 | return try { 21 | creator.get() as T 22 | } catch (e: Exception) { 23 | throw RuntimeException(e) 24 | } 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/di/modules/AppSubcomponentModule.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.di.modules 2 | 3 | import com.masterwok.shrimplesearch.features.about.di.AboutSubcomponent 4 | import com.masterwok.shrimplesearch.features.query.di.QuerySubcomponent 5 | import com.masterwok.shrimplesearch.features.settings.di.SettingsSubcomponent 6 | import com.masterwok.shrimplesearch.features.splash.di.SplashSubcomponent 7 | import dagger.Module 8 | 9 | @Module( 10 | subcomponents = [ 11 | QuerySubcomponent::class, 12 | SplashSubcomponent::class, 13 | AboutSubcomponent::class, 14 | SettingsSubcomponent::class 15 | ] 16 | ) 17 | class AppSubcomponentModule -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/di/modules/JackettHarnessModule.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.di.modules 2 | 3 | import com.masterwok.shrimplesearch.common.data.repositories.CardigannDefinitionRepository 4 | import com.masterwok.xamarin.JackettHarnessFactory 5 | import com.masterwok.xamarin.JackettHarnessFactoryImpl 6 | import com.masterwok.xamarininterface.contracts.ICardigannDefinitionRepository 7 | import dagger.Binds 8 | import dagger.Module 9 | import dagger.Provides 10 | import javax.inject.Singleton 11 | 12 | @Module(includes = [JackettHarnessModule.Declarations::class]) 13 | class JackettHarnessModule { 14 | 15 | @Suppress("RedundantModalityModifier", "unused") 16 | @Module 17 | interface Declarations { 18 | @Singleton 19 | @Binds 20 | abstract fun bindCardigannDefinitionRepository( 21 | cardigannDefinitionRepository: CardigannDefinitionRepository 22 | ): ICardigannDefinitionRepository 23 | } 24 | 25 | @Singleton 26 | @Provides 27 | fun provideJackettFactory(): JackettHarnessFactory = JackettHarnessFactoryImpl 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/di/modules/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.di.modules 2 | 3 | import com.masterwok.shrimplesearch.common.DEFAULT_USER_SETTINGS 4 | import com.masterwok.shrimplesearch.common.SHARED_PREFERENCES_NAME 5 | import com.masterwok.shrimplesearch.common.data.models.UserSettings 6 | import com.masterwok.shrimplesearch.common.data.repositories.SharedPreferencesUserSettingsRepository 7 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository 8 | import dagger.Binds 9 | import dagger.Module 10 | import dagger.Provides 11 | import javax.inject.Named 12 | import javax.inject.Singleton 13 | 14 | @Module( 15 | includes = [ 16 | RepositoryModule.Declarations::class 17 | ] 18 | ) 19 | class RepositoryModule { 20 | 21 | @Suppress("RedundantModalityModifier", "unused") 22 | @Module 23 | interface Declarations { 24 | @Singleton 25 | @Binds 26 | abstract fun bindSharedPreferencesUserSettingsRepository( 27 | sharedPreferencesUserSettingsRepository: SharedPreferencesUserSettingsRepository 28 | ): UserSettingsRepository 29 | } 30 | 31 | @Provides 32 | @Named("shared_preferences_name") 33 | fun provideSharedPreferencesName(): String = SHARED_PREFERENCES_NAME 34 | 35 | @Provides 36 | @Named("default_user_settings") 37 | fun provideDefaultUserSettings(): UserSettings = DEFAULT_USER_SETTINGS 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/di/modules/ServiceModule.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.di.modules 2 | 3 | import com.masterwok.shrimplesearch.common.INDEXER_BLOCK_LIST 4 | import com.masterwok.shrimplesearch.common.data.repositories.JackettServiceImpl 5 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.JackettService 6 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository 7 | import com.masterwok.shrimplesearch.common.data.services.FirebaseAnalyticsService 8 | import com.masterwok.shrimplesearch.common.data.services.contracts.AnalyticService 9 | import com.masterwok.xamarin.JackettHarnessFactory 10 | import com.masterwok.xamarininterface.contracts.ICardigannDefinitionRepository 11 | import dagger.Binds 12 | import dagger.Module 13 | import dagger.Provides 14 | import javax.inject.Singleton 15 | 16 | @Module( 17 | includes = [ 18 | JackettHarnessModule::class, 19 | ServiceModule.Declarations::class 20 | ] 21 | ) 22 | class ServiceModule { 23 | 24 | @Suppress("RedundantModalityModifier", "unused") 25 | @Module 26 | interface Declarations { 27 | @Singleton 28 | @Binds 29 | abstract fun bindAnalyticService( 30 | firebaseAnalyticsService: FirebaseAnalyticsService 31 | ): AnalyticService 32 | } 33 | 34 | @Suppress("unused") 35 | @Singleton 36 | @Provides 37 | fun provideJackettService( 38 | jackettHarnessFactory: JackettHarnessFactory, 39 | userSettingsRepository: UserSettingsRepository, 40 | cardigannDefinitionRepository: ICardigannDefinitionRepository 41 | ): JackettService = JackettServiceImpl( 42 | jackettHarnessFactory.createInstance(cardigannDefinitionRepository), 43 | userSettingsRepository, 44 | INDEXER_BLOCK_LIST 45 | ) 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/di/modules/ViewModelFactoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.di.modules 2 | 3 | import androidx.lifecycle.ViewModelProvider 4 | import com.masterwok.shrimplesearch.di.factories.DaggerViewModelFactory 5 | import dagger.Binds 6 | import dagger.Module 7 | 8 | @Module 9 | abstract class ViewModelFactoryModule { 10 | @Binds 11 | abstract fun bindViewModelFactory(viewModelFactory: DaggerViewModelFactory): ViewModelProvider.Factory 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/about/di/AboutScope.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.about.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention 7 | annotation class AboutScope 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/about/di/AboutSubcomponent.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.about.di 2 | 3 | import com.masterwok.shrimplesearch.features.about.fragments.AboutFragment 4 | import dagger.Subcomponent 5 | 6 | @AboutScope 7 | @Subcomponent() 8 | interface AboutSubcomponent { 9 | @Subcomponent.Factory 10 | interface Factory { 11 | fun create(): AboutSubcomponent 12 | } 13 | 14 | fun inject(fragment: AboutFragment) 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/about/fragments/AboutFragment.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.about.fragments 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import androidx.core.app.ShareCompat 12 | import androidx.fragment.app.Fragment 13 | import com.afollestad.materialdialogs.MaterialDialog 14 | import com.masterwok.shrimplesearch.BuildConfig 15 | import com.masterwok.shrimplesearch.R 16 | import com.masterwok.shrimplesearch.common.constants.AnalyticEvent 17 | import com.masterwok.shrimplesearch.common.data.services.contracts.AnalyticService 18 | import com.masterwok.shrimplesearch.common.extensions.getPlayStoreUri 19 | import com.masterwok.shrimplesearch.common.extensions.startPlayStoreActivity 20 | import com.masterwok.shrimplesearch.common.utils.notNull 21 | import com.masterwok.shrimplesearch.di.AppInjector 22 | import com.masterwok.shrimplesearch.features.settings.fragments.SettingsFragment 23 | import kotlinx.android.synthetic.main.fragment_about.* 24 | import javax.inject.Inject 25 | 26 | class AboutFragment : Fragment() { 27 | 28 | @Inject 29 | lateinit var analyticService: AnalyticService 30 | 31 | override fun onCreateView( 32 | inflater: LayoutInflater, container: ViewGroup?, 33 | savedInstanceState: Bundle? 34 | ): View = inflater.inflate( 35 | R.layout.fragment_about, container, false 36 | ) 37 | 38 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 39 | super.onViewCreated(view, savedInstanceState) 40 | 41 | configureVersionTextView() 42 | 43 | subscribeToViewComponents() 44 | } 45 | 46 | override fun onAttach(context: Context) { 47 | super.onAttach(context) 48 | 49 | AppInjector 50 | .aboutComponent 51 | .inject(this) 52 | } 53 | 54 | 55 | override fun onResume() { 56 | super.onResume() 57 | 58 | analyticService.logScreen(AboutFragment::class.java) 59 | } 60 | 61 | private fun subscribeToViewComponents() { 62 | buttonViewOnGitHub.setOnClickListener { openGitHubProjectUri() } 63 | buttonViewReview.setOnClickListener { openReviewPlayStore() } 64 | buttonViewShare.setOnClickListener { onShareButtonTapped() } 65 | } 66 | 67 | private fun onShareButtonTapped() = activity.notNull { activity -> 68 | analyticService.logEvent(AnalyticEvent.ShareManeki) 69 | 70 | val chooserTitle = activity.getString(R.string.share_chooser_title) 71 | val shareText = activity.getString( 72 | R.string.share_text, 73 | activity.getPlayStoreUri().toString() 74 | ) 75 | 76 | val intent = ShareCompat 77 | .IntentBuilder 78 | .from(activity) 79 | .setType("text/plain") 80 | .setText(shareText) 81 | .intent 82 | 83 | activity.startActivity(Intent.createChooser(intent, chooserTitle)) 84 | } 85 | 86 | private fun openGitHubProjectUri() = context.notNull { context -> 87 | val gitHubUri = Uri.parse(context.getString(R.string.gitHubUrl)) 88 | 89 | try { 90 | startActivity(Intent(Intent.ACTION_VIEW, gitHubUri).apply { 91 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP 92 | }) 93 | } catch (exception: ActivityNotFoundException) { 94 | analyticService.logException(exception, "No activity found to handle open GitHub Uri") 95 | presentUnableToOpenGitHubDialog() 96 | } 97 | } 98 | 99 | private fun openReviewPlayStore() = context.notNull { context -> 100 | try { 101 | context.startPlayStoreActivity() 102 | } catch (exception: ActivityNotFoundException) { 103 | analyticService.logException(exception, "No activity found to handle open GitHub Uri") 104 | presentUnableToOpenPlayStoreDialog() 105 | } 106 | } 107 | 108 | private fun presentUnableToOpenPlayStoreDialog() = context.notNull { context -> 109 | MaterialDialog(context).show { 110 | title(res = R.string.dialog_header_whoops) 111 | message(res = R.string.dialog_unable_to_open_play_store) 112 | positiveButton { 113 | title(res = R.string.button_ok) 114 | } 115 | } 116 | } 117 | 118 | private fun presentUnableToOpenGitHubDialog() = context.notNull { context -> 119 | MaterialDialog(context).show { 120 | title(res = R.string.dialog_header_whoops) 121 | message(res = R.string.dialog_unable_to_open_github_uri) 122 | positiveButton { 123 | title(res = R.string.button_ok) 124 | } 125 | } 126 | } 127 | 128 | private fun configureVersionTextView() = context.notNull { context -> 129 | textViewAboutVersion.text = context.getString( 130 | R.string.version, 131 | BuildConfig.VERSION_NAME 132 | ) 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/query/adapters/IndexerQueryResultsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.query.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.core.view.isVisible 7 | import androidx.recyclerview.widget.DiffUtil 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.masterwok.shrimplesearch.R 10 | import com.masterwok.shrimplesearch.common.contracts.Configurable 11 | import com.masterwok.shrimplesearch.common.extensions.getCurrentLocale 12 | import com.masterwok.shrimplesearch.common.extensions.getLocaleNumberFormat 13 | import com.masterwok.shrimplesearch.common.extensions.onClicked 14 | import com.masterwok.shrimplesearch.common.extensions.toHumanReadableByteCount 15 | import com.masterwok.xamarininterface.models.QueryResultItem 16 | import kotlinx.android.synthetic.main.view_indexer_query_result_item.view.* 17 | import kotlinx.coroutines.* 18 | import kotlinx.coroutines.flow.debounce 19 | import kotlinx.coroutines.flow.launchIn 20 | import kotlinx.coroutines.flow.onEach 21 | import java.text.DateFormat 22 | 23 | class IndexerQueryResultsAdapter( 24 | private val onQueryResultItemClicked: (QueryResultItem) -> Unit 25 | ) : RecyclerView.Adapter(), 26 | Configurable> { 27 | 28 | private var configuredModel: List = emptyList() 29 | 30 | override fun onCreateViewHolder( 31 | parent: ViewGroup, viewType: Int 32 | ): ViewHolder = ViewHolder( 33 | LayoutInflater 34 | .from(parent.context) 35 | .inflate( 36 | R.layout.view_indexer_query_result_item, parent, false 37 | ), onQueryResultItemClicked 38 | ) 39 | 40 | override fun getItemCount(): Int = configuredModel.count() 41 | 42 | override fun onBindViewHolder( 43 | holder: ViewHolder, position: Int 44 | ) = holder.configure(configuredModel[position]) 45 | 46 | override fun configure(model: List) { 47 | val diffCallback = QueryResultItemsDiffCallback(configuredModel, model) 48 | 49 | configuredModel = model 50 | 51 | DiffUtil.calculateDiff(diffCallback).also { 52 | it.dispatchUpdatesTo(this) 53 | } 54 | } 55 | 56 | class ViewHolder( 57 | itemView: View, private val onQueryResultItemClicked: (QueryResultItem) -> Unit 58 | ) : RecyclerView.ViewHolder(itemView), Configurable { 59 | 60 | private val scope = CoroutineScope(Job() + Dispatchers.Main) 61 | 62 | @FlowPreview 63 | @ExperimentalCoroutinesApi 64 | override fun configure(model: QueryResultItem) { 65 | val context = itemView.context 66 | val statInfo = model.statInfo 67 | val stringNotAvailable = context.getString(R.string.stat_info_not_available) 68 | val numberFormat = context.getLocaleNumberFormat() 69 | val dateFormat = DateFormat.getDateInstance( 70 | DateFormat.SHORT, context.getCurrentLocale() 71 | ) 72 | 73 | itemView.textViewTitle.text = model.title 74 | 75 | val seeders = statInfo.seeders 76 | val peers = statInfo.peers 77 | 78 | val leechers = if (seeders != null && peers != null) { 79 | peers - seeders 80 | } else { 81 | null 82 | } 83 | 84 | itemView.textViewSeeders.text = seeders 85 | ?.let(numberFormat::format) 86 | ?: stringNotAvailable 87 | 88 | itemView.textViewLeechers.text = leechers 89 | ?.let(numberFormat::format) 90 | ?: stringNotAvailable 91 | 92 | itemView.textViewFileCount.text = statInfo 93 | .files 94 | ?.let(numberFormat::format) 95 | ?: stringNotAvailable 96 | 97 | itemView.textViewPublishedOn.text = statInfo 98 | .publishedOn 99 | ?.let(dateFormat::format) 100 | ?: stringNotAvailable 101 | 102 | itemView.textViewSize.text = statInfo 103 | .size 104 | ?.toHumanReadableByteCount(true) 105 | ?: stringNotAvailable 106 | 107 | itemView.imageViewMagnet.isVisible = model.linkInfo.magnetUri != null 108 | 109 | itemView.setOnClickListener { onQueryResultItemClicked(model) } 110 | 111 | itemView 112 | .onClicked() 113 | .debounce(BUTTON_DEBOUNCE_MS) 114 | .onEach { onQueryResultItemClicked(model) } 115 | .launchIn(scope) 116 | } 117 | 118 | private companion object { 119 | private const val BUTTON_DEBOUNCE_MS = 250L 120 | } 121 | } 122 | } 123 | 124 | private class QueryResultItemsDiffCallback( 125 | val old: List, 126 | val new: List 127 | ) : DiffUtil.Callback() { 128 | 129 | override fun getOldListSize(): Int = old.count() 130 | 131 | override fun getNewListSize(): Int = new.count() 132 | 133 | override fun areItemsTheSame( 134 | oldItemPosition: Int, 135 | newItemPosition: Int 136 | ): Boolean = old[oldItemPosition] == new[newItemPosition] 137 | 138 | override fun areContentsTheSame( 139 | oldItemPosition: Int, 140 | newItemPosition: Int 141 | ): Boolean = old[oldItemPosition] == new[newItemPosition] 142 | 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/query/adapters/MaterialDIalogIconListItemAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.query.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.annotation.DrawableRes 7 | import androidx.annotation.StringRes 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.masterwok.shrimplesearch.R 10 | import com.masterwok.shrimplesearch.common.contracts.Configurable 11 | import com.masterwok.shrimplesearch.common.utils.notNull 12 | import kotlinx.android.synthetic.main.view_material_dialog_icon_item.view.* 13 | 14 | class MaterialDialogIconListItemAdapter : 15 | RecyclerView.Adapter(), 16 | Configurable> { 17 | 18 | private lateinit var configuredModel: List 19 | 20 | override fun onCreateViewHolder( 21 | parent: ViewGroup, viewType: Int 22 | ): ViewHolder = ViewHolder( 23 | LayoutInflater 24 | .from(parent.context) 25 | .inflate( 26 | R.layout.view_material_dialog_icon_item, parent, false 27 | ) 28 | ) 29 | 30 | override fun getItemCount(): Int = configuredModel.count() 31 | 32 | override fun onBindViewHolder( 33 | holder: ViewHolder, 34 | position: Int 35 | ) = holder.configure(configuredModel[position]) 36 | 37 | override fun configure(model: List) { 38 | configuredModel = model 39 | 40 | notifyDataSetChanged() 41 | } 42 | 43 | class ViewHolder( 44 | itemView: View 45 | ) : RecyclerView.ViewHolder(itemView), Configurable { 46 | override fun configure(model: Item) = itemView.context.notNull { context -> 47 | itemView.imageView.setImageResource(model.drawable) 48 | itemView.textView.text = context.getString(model.label) 49 | itemView.setOnClickListener { model.onItemTap() } 50 | } 51 | } 52 | 53 | data class Item( 54 | @DrawableRes val drawable: Int, 55 | @StringRes val label: Int, 56 | val onItemTap: () -> Unit 57 | ) 58 | 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/query/adapters/QueryResultsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.query.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.masterwok.shrimplesearch.R 9 | import com.masterwok.shrimplesearch.common.contracts.Configurable 10 | import com.masterwok.shrimplesearch.common.extensions.getLocaleNumberFormat 11 | import com.masterwok.shrimplesearch.common.extensions.onClicked 12 | import com.masterwok.xamarininterface.models.IndexerQueryResult 13 | import kotlinx.android.synthetic.main.view_query_result_item.view.* 14 | import kotlinx.coroutines.* 15 | import kotlinx.coroutines.flow.collect 16 | import kotlinx.coroutines.flow.debounce 17 | import kotlinx.coroutines.flow.launchIn 18 | import kotlinx.coroutines.flow.onEach 19 | 20 | class QueryResultsAdapter( 21 | private val onQueryResultClicked: (IndexerQueryResult) -> Unit 22 | ) : RecyclerView.Adapter(), 23 | Configurable> { 24 | 25 | private var configuredModel: List = emptyList() 26 | 27 | override fun onCreateViewHolder( 28 | parent: ViewGroup, viewType: Int 29 | ): ViewHolder = ViewHolder( 30 | LayoutInflater 31 | .from(parent.context) 32 | .inflate( 33 | R.layout.view_query_result_item, parent, false 34 | ), onQueryResultClicked 35 | ) 36 | 37 | override fun getItemCount(): Int = configuredModel.count() 38 | 39 | override fun onBindViewHolder( 40 | holder: ViewHolder, 41 | position: Int 42 | ) = holder.configure(configuredModel[position]) 43 | 44 | override fun configure(model: List) { 45 | val diffCallback = IndexerQueryResultsDiffCallback(configuredModel, model) 46 | 47 | configuredModel = model 48 | 49 | DiffUtil.calculateDiff(diffCallback).also { 50 | it.dispatchUpdatesTo(this) 51 | } 52 | } 53 | 54 | class ViewHolder( 55 | itemView: View, 56 | private val onQueryResultClicked: (IndexerQueryResult) -> Unit 57 | ) : RecyclerView.ViewHolder(itemView), Configurable { 58 | 59 | private val scope = CoroutineScope(Job() + Dispatchers.Main) 60 | 61 | @ExperimentalCoroutinesApi 62 | @FlowPreview 63 | override fun configure(model: IndexerQueryResult) { 64 | val numberFormat = itemView.context.getLocaleNumberFormat() 65 | 66 | itemView.textViewIndexerName.text = model.indexer.displayName 67 | itemView.textViewMagnetCount.text = numberFormat.format(model.magnetCount) 68 | itemView.textViewLinkCount.text = numberFormat.format(model.linkCount) 69 | 70 | itemView 71 | .onClicked() 72 | .debounce(BUTTON_DEBOUNCE_MS) 73 | .onEach { onQueryResultClicked(model) } 74 | .launchIn(scope) 75 | } 76 | 77 | private companion object { 78 | private const val BUTTON_DEBOUNCE_MS = 250L 79 | } 80 | } 81 | } 82 | 83 | private class IndexerQueryResultsDiffCallback( 84 | val old: List, 85 | val new: List 86 | ) : DiffUtil.Callback() { 87 | 88 | override fun getOldListSize(): Int = old.count() 89 | 90 | override fun getNewListSize(): Int = new.count() 91 | 92 | override fun areItemsTheSame( 93 | oldItemPosition: Int, 94 | newItemPosition: Int 95 | ): Boolean = old[oldItemPosition].indexer.id == new[newItemPosition].indexer.id 96 | 97 | override fun areContentsTheSame( 98 | oldItemPosition: Int, 99 | newItemPosition: Int 100 | ): Boolean = old[oldItemPosition].items == new[newItemPosition].items 101 | 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/query/components/SortComponent.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.query.components 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.Gravity 6 | import android.widget.CompoundButton 7 | import androidx.appcompat.widget.AppCompatRadioButton 8 | import androidx.constraintlayout.widget.ConstraintLayout 9 | import androidx.core.content.ContextCompat 10 | import androidx.core.content.res.ResourcesCompat 11 | import androidx.core.view.children 12 | import androidx.core.view.setPadding 13 | import com.google.android.flexbox.FlexboxLayout 14 | import com.masterwok.shrimplesearch.R 15 | import com.masterwok.shrimplesearch.common.contracts.ViewComponent 16 | import com.masterwok.shrimplesearch.common.extensions.dpToPx 17 | import com.masterwok.shrimplesearch.common.extensions.getColorByAttribute 18 | import kotlinx.android.synthetic.main.component_sort_by.view.* 19 | 20 | class SortComponent : ConstraintLayout, ViewComponent { 21 | 22 | private lateinit var configuredModel: Model 23 | 24 | constructor(context: Context) : super(context) { 25 | inflate(null) 26 | onFinishInflate() 27 | } 28 | 29 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { 30 | inflate(attrs) 31 | } 32 | 33 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 34 | context, 35 | attrs, 36 | defStyleAttr 37 | ) { 38 | inflate(attrs) 39 | } 40 | 41 | @Suppress("UNUSED_PARAMETER") 42 | private fun inflate(attrs: AttributeSet?) { 43 | inflate(context, R.layout.component_sort_by, this) 44 | } 45 | 46 | override fun onFinishInflate() { 47 | super.onFinishInflate() 48 | 49 | setBackgroundColor(context.getColorByAttribute(R.attr.color_background)) 50 | 51 | setOnTouchListener(null) 52 | } 53 | 54 | override fun configure(model: Model) { 55 | configuredModel = model 56 | configureFlexBoxPills(flexboxLayoutSort, model.sortPills, model.selectedSortPill) 57 | configureFlexBoxPills(flexboxLayoutOrder, model.orderPills, model.selectedOrderPill) 58 | } 59 | 60 | override fun getModel(): Model = Model( 61 | configuredModel.sortPills, 62 | configuredModel.orderPills, 63 | configuredModel 64 | .sortPills 65 | .first { it.id == getSelectedId(flexboxLayoutSort) }, 66 | configuredModel 67 | .sortPills 68 | .first { it.id == getSelectedId(flexboxLayoutOrder) } 69 | ) 70 | 71 | private fun getSelectedId(flexBoxLayout: FlexboxLayout): Int = flexBoxLayout 72 | .children 73 | .filterIsInstance() 74 | .first { it.isChecked } 75 | .tag as Int 76 | 77 | private fun configureFlexBoxPills( 78 | flexBoxLayout: FlexboxLayout, 79 | sortPills: List, 80 | selectedSortPill: Pill 81 | ) = sortPills.forEach { pill -> 82 | flexBoxLayout.addView( 83 | createPillRadioButton( 84 | flexBoxLayout, 85 | pill, 86 | pill == selectedSortPill 87 | ) 88 | ) 89 | } 90 | 91 | private fun createPillRadioButton( 92 | flexBoxLayout: FlexboxLayout, 93 | pill: Pill, 94 | isChecked: Boolean 95 | ): AppCompatRadioButton = AppCompatRadioButton(context).apply { 96 | this.isChecked = isChecked 97 | setBackgroundResource(R.drawable.background_pill) 98 | setPadding(8.dpToPx(context)) 99 | tag = pill.id 100 | typeface = ResourcesCompat.getFont(context, R.font.eina_03_regular) 101 | text = pill.getTitle(context) 102 | setTextColor(ContextCompat.getColorStateList(context, R.color.pill_text_color)) 103 | gravity = Gravity.CENTER 104 | buttonDrawable = null 105 | minimumHeight = 0 106 | minimumWidth = 0 107 | 108 | setOnCheckedChangeListener { button, isChecked -> 109 | setFlexBoxPillCheckStates(flexBoxLayout, button.tag, isChecked) 110 | } 111 | } 112 | 113 | private fun setFlexBoxPillCheckStates( 114 | flexBoxLayout: FlexboxLayout, 115 | buttonTag: Any, 116 | isChecked: Boolean 117 | ) { 118 | if (!isChecked) { 119 | return 120 | } 121 | 122 | flexBoxLayout 123 | .children 124 | .filterIsInstance() 125 | .filterNot { it.tag == buttonTag } 126 | .forEach { it.isChecked = false } 127 | } 128 | 129 | data class Model( 130 | val sortPills: List, 131 | val orderPills: List, 132 | val selectedSortPill: Pill, 133 | val selectedOrderPill: Pill 134 | ) 135 | 136 | data class Pill( 137 | val id: Int, 138 | val getTitle: (context: Context) -> String 139 | ) 140 | 141 | 142 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/query/constants/IndexerQueryResultSortBy.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.query.constants 2 | 3 | import android.content.Context 4 | import com.masterwok.shrimplesearch.R 5 | 6 | enum class IndexerQueryResultSortBy(val id: Int) { 7 | Name(0), 8 | Peers(1), 9 | Seeders(2), 10 | Size(3), 11 | PublishedOn(4); 12 | 13 | fun getDisplayValue(context: Context): String = when (this) { 14 | Name -> context.getString(R.string.component_sort_query_results_name) 15 | Peers -> context.getString(R.string.component_sort_query_results_peers) 16 | Seeders -> context.getString(R.string.component_sort_query_results_seeders) 17 | Size -> context.getString(R.string.component_sort_query_results_size) 18 | PublishedOn -> context.getString(R.string.component_sort_query_results_published_on) 19 | } 20 | 21 | companion object { 22 | fun getByValue(value: Int) = values().first { it.id == value } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/query/constants/OrderBy.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.query.constants 2 | 3 | import android.content.Context 4 | import com.masterwok.shrimplesearch.R 5 | 6 | enum class OrderBy (val id: Int){ 7 | Ascending(0), 8 | Descending(1); 9 | 10 | fun getDisplayValue(context: Context): String = when(this) { 11 | Ascending -> context.getString(R.string.order_by_asecnding) 12 | Descending -> context.getString(R.string.order_by_descending) 13 | } 14 | 15 | 16 | companion object { 17 | fun getByValue(value: Int) = values().first { it.id == value } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/query/constants/QueryResultSortBy.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.query.constants 2 | 3 | import android.content.Context 4 | import com.masterwok.shrimplesearch.R 5 | 6 | enum class QueryResultSortBy(val id: Int) { 7 | Name(0), 8 | MagnetCount(1), 9 | LinkCount(2), 10 | AggregateCount(3); 11 | 12 | fun getDisplayValue(context: Context): String = when (this) { 13 | Name -> context.getString(R.string.query_result_sort_by_name) 14 | MagnetCount -> context.getString(R.string.query_result_sort_by_magnet_count) 15 | LinkCount -> context.getString(R.string.query_result_sort_by_link_count) 16 | AggregateCount -> context.getString(R.string.query_result_sort_by_aggregate_count) 17 | } 18 | 19 | companion object { 20 | fun getByValue(value: Int) = values().first { it.id == value } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/query/di/QueryModule.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.query.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.masterwok.shrimplesearch.common.AGGREGATE_INDEXER_ID 5 | import com.masterwok.shrimplesearch.di.annotations.ViewModelKey 6 | import com.masterwok.shrimplesearch.features.query.viewmodels.QueryViewModel 7 | import dagger.Binds 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.multibindings.IntoMap 11 | import javax.inject.Named 12 | 13 | 14 | @Module(includes = [QueryModule.Declarations::class]) 15 | class QueryModule { 16 | 17 | @Suppress("RedundantModalityModifier", "unused") 18 | @Module 19 | interface Declarations { 20 | @Binds 21 | @IntoMap 22 | @ViewModelKey(QueryViewModel::class) 23 | abstract fun bindViewModel(viewModel: QueryViewModel): ViewModel 24 | } 25 | 26 | @Provides 27 | @Named("aggregate_indexer_id") 28 | fun provideAggregateIndexerId(): String = AGGREGATE_INDEXER_ID 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/query/di/QueryScope.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.query.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention 7 | annotation class QueryScope 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/query/di/QuerySubcomponent.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.query.di 2 | 3 | import com.masterwok.shrimplesearch.features.query.fragments.IndexerQueryResultsFragment 4 | import com.masterwok.shrimplesearch.features.query.fragments.QueryFragment 5 | import dagger.Subcomponent 6 | 7 | @QueryScope 8 | @Subcomponent( 9 | modules = [ 10 | QueryModule::class 11 | ] 12 | ) 13 | interface QuerySubcomponent { 14 | 15 | @Subcomponent.Factory 16 | interface Factory { 17 | fun create(): QuerySubcomponent 18 | } 19 | 20 | fun inject(fragment: QueryFragment) 21 | fun inject(fragment: IndexerQueryResultsFragment) 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/settings/di/SettingsModule.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.settings.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.masterwok.shrimplesearch.common.AGGREGATE_INDEXER_ID 5 | import com.masterwok.shrimplesearch.common.SHARED_PREFERENCES_NAME 6 | import com.masterwok.shrimplesearch.di.annotations.ViewModelKey 7 | import com.masterwok.shrimplesearch.features.settings.viewmodels.SettingsViewModel 8 | import dagger.Binds 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.multibindings.IntoMap 12 | import javax.inject.Named 13 | 14 | 15 | @Module(includes = [SettingsModule.Declarations::class]) 16 | class SettingsModule { 17 | 18 | @Suppress("RedundantModalityModifier", "unused") 19 | @Module 20 | interface Declarations { 21 | @SettingsScope 22 | @Binds 23 | @IntoMap 24 | @ViewModelKey(SettingsViewModel::class) 25 | abstract fun bindViewModel(viewModel: SettingsViewModel): ViewModel 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/settings/di/SettingsScope.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.settings.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention 7 | annotation class SettingsScope 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/settings/di/SettingsSubcomponent.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.settings.di 2 | 3 | import com.masterwok.shrimplesearch.features.settings.fragments.SettingsFragment 4 | import dagger.Subcomponent 5 | 6 | @SettingsScope 7 | @Subcomponent( 8 | modules = [ 9 | SettingsModule::class 10 | ] 11 | )interface SettingsSubcomponent { 12 | 13 | @Subcomponent.Factory 14 | interface Factory { 15 | fun create(): SettingsSubcomponent 16 | } 17 | 18 | fun inject(fragment: SettingsFragment) 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/settings/fragments/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.settings.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 androidx.fragment.app.Fragment 9 | import androidx.fragment.app.viewModels 10 | import androidx.lifecycle.ViewModelProvider 11 | import com.masterwok.shrimplesearch.R 12 | import com.masterwok.shrimplesearch.common.constants.Theme 13 | import com.masterwok.shrimplesearch.common.data.models.UserSettings 14 | import com.masterwok.shrimplesearch.common.data.services.contracts.AnalyticService 15 | import com.masterwok.shrimplesearch.di.AppInjector 16 | import com.masterwok.shrimplesearch.features.settings.viewmodels.SettingsViewModel 17 | import kotlinx.android.synthetic.main.fragment_settings.* 18 | import javax.inject.Inject 19 | 20 | class SettingsFragment : Fragment() { 21 | 22 | @Inject 23 | lateinit var analyticService: AnalyticService 24 | 25 | @Inject 26 | lateinit var viewModelFactory: ViewModelProvider.Factory 27 | 28 | private val viewModel: SettingsViewModel by viewModels { viewModelFactory } 29 | 30 | override fun onCreateView( 31 | inflater: LayoutInflater, container: ViewGroup?, 32 | savedInstanceState: Bundle? 33 | ): View = inflater.inflate( 34 | R.layout.fragment_settings, container, false 35 | ) 36 | 37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 38 | super.onViewCreated(view, savedInstanceState) 39 | 40 | configure(viewModel.readUserSettings()) 41 | 42 | subscribeToViewComponents() 43 | } 44 | 45 | override fun onResume() { 46 | super.onResume() 47 | 48 | analyticService.logScreen(SettingsFragment::class.java) 49 | } 50 | 51 | override fun onAttach(context: Context) { 52 | super.onAttach(context) 53 | 54 | AppInjector.settingsComponent.inject(this) 55 | } 56 | 57 | private fun configure(userSettings: UserSettings) { 58 | configureThemeSelection(userSettings.theme) 59 | 60 | switchScrollToTop.isChecked = checkNotNull(userSettings.isScrollToTopNotificationsEnabled) 61 | switchMagnet.isChecked = checkNotNull(userSettings.isOnlyMagnetQueryResultItemsEnabled) 62 | } 63 | 64 | private fun configureThemeSelection(theme: Theme): Unit = when (theme) { 65 | Theme.Light -> radioButtonThemeLight.isChecked = true 66 | Theme.Oled -> radioButtonThemeOled.isChecked = true 67 | } 68 | 69 | private fun subscribeToViewComponents() { 70 | subscribeToThemeRadioGroup() 71 | subscribeToScrollToTopNotificationsSwitch() 72 | } 73 | 74 | private fun subscribeToScrollToTopNotificationsSwitch() { 75 | switchScrollToTop.setOnCheckedChangeListener { _, isChecked -> 76 | viewModel.updateUserSettings( 77 | viewModel.readUserSettings().copy( 78 | isScrollToTopNotificationsEnabled = isChecked 79 | ) 80 | ) 81 | } 82 | switchMagnet.setOnCheckedChangeListener { _, isChecked -> 83 | viewModel.updateUserSettings( 84 | viewModel.readUserSettings().copy( 85 | isOnlyMagnetQueryResultItemsEnabled = isChecked 86 | ) 87 | ) 88 | } 89 | } 90 | 91 | private fun subscribeToThemeRadioGroup() = radioGroupTheme.setOnCheckedChangeListener { _, _ -> 92 | val selectedThemeId: Int 93 | 94 | val userSettings = viewModel 95 | .readUserSettings() 96 | .copy( 97 | theme = when (radioGroupTheme.checkedRadioButtonId) { 98 | R.id.radioButtonThemeLight -> { 99 | selectedThemeId = R.style.AppTheme 100 | Theme.Light 101 | } 102 | R.id.radioButtonThemeOled -> { 103 | selectedThemeId = R.style.AppTheme_Oled 104 | Theme.Oled 105 | } 106 | else -> error("Theme not registered on settings.") 107 | } 108 | ) 109 | 110 | viewModel.updateUserSettings(userSettings.copy()) 111 | 112 | activity?.setTheme(selectedThemeId) 113 | activity?.recreate() 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/settings/viewmodels/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.settings.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.masterwok.shrimplesearch.common.data.models.UserSettings 5 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository 6 | import javax.inject.Inject 7 | 8 | class SettingsViewModel @Inject constructor( 9 | private val userSettingsRepository: UserSettingsRepository 10 | ) : ViewModel() { 11 | 12 | fun readUserSettings(): UserSettings = userSettingsRepository.read() 13 | 14 | fun updateUserSettings(userSettings: UserSettings) = userSettingsRepository.update(userSettings) 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/splash/activities/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.splash.activities 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.view.Window 7 | import android.view.WindowManager 8 | import androidx.activity.viewModels 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.fragment.app.FragmentActivity 11 | import androidx.lifecycle.ViewModelProvider 12 | import androidx.lifecycle.observe 13 | import com.masterwok.shrimplesearch.R 14 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository 15 | import com.masterwok.shrimplesearch.di.AppInjector 16 | import com.masterwok.shrimplesearch.features.splash.models.BootstrapInfo 17 | import com.masterwok.shrimplesearch.features.splash.viewmodels.SplashViewModel 18 | import com.masterwok.shrimplesearch.main.MainActivity 19 | import kotlinx.android.synthetic.main.activity_splash.* 20 | import javax.inject.Inject 21 | import kotlin.math.ceil 22 | 23 | 24 | class SplashActivity : AppCompatActivity() { 25 | 26 | @Inject 27 | lateinit var userSettingsRepository: UserSettingsRepository 28 | 29 | @Inject 30 | lateinit var viewModelFactory: ViewModelProvider.Factory 31 | 32 | private val viewModel: SplashViewModel by viewModels { viewModelFactory } 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | AppInjector 36 | .splashComponent 37 | .inject(this) 38 | 39 | setTheme(userSettingsRepository.getSplashThemeId()) 40 | 41 | super.onCreate(savedInstanceState) 42 | 43 | setContentView(R.layout.activity_splash) 44 | 45 | subscribeToLiveData() 46 | 47 | viewModel.initialize() 48 | } 49 | 50 | private fun subscribeToLiveData() { 51 | viewModel.liveDataBoostrapInfo.observe(this, this::configure) 52 | viewModel.liveDataBootStrapCompleted.observe(this) { 53 | startMainActivity() 54 | } 55 | } 56 | 57 | private fun configure(bootstrapInfo: BootstrapInfo) { 58 | progressBar.apply { 59 | progress = bootstrapInfo.initializedCount 60 | max = bootstrapInfo.totalIndexerCount 61 | } 62 | 63 | textViewIndexerProgressCount.text = getString( 64 | R.string.splash_progress, 65 | ceil((bootstrapInfo.initializedCount.toDouble() / bootstrapInfo.totalIndexerCount) * 100) 66 | ) 67 | } 68 | 69 | private fun startMainActivity() { 70 | startActivity(MainActivity.createIntent(this).apply { 71 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 72 | }) 73 | 74 | overridePendingTransition(0, 0) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/splash/di/SplashModule.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.splash.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.masterwok.shrimplesearch.di.annotations.ViewModelKey 5 | import com.masterwok.shrimplesearch.features.splash.viewmodels.SplashViewModel 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.multibindings.IntoMap 9 | 10 | 11 | @Module 12 | abstract class SplashModule { 13 | 14 | @SplashScope 15 | @Binds 16 | @IntoMap 17 | @ViewModelKey(SplashViewModel::class) 18 | abstract fun bindViewModel(viewModel: SplashViewModel): ViewModel 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/splash/di/SplashScope.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.splash.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention 7 | annotation class SplashScope 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/splash/di/SplashSubcomponent.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.splash.di 2 | 3 | import com.masterwok.shrimplesearch.features.splash.activities.SplashActivity 4 | import dagger.Subcomponent 5 | 6 | @SplashScope 7 | @Subcomponent( 8 | modules = [ 9 | SplashModule::class 10 | ] 11 | ) 12 | interface SplashSubcomponent { 13 | @Subcomponent.Factory 14 | interface Factory { 15 | fun create(): SplashSubcomponent 16 | } 17 | 18 | fun inject(activity: SplashActivity) 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/splash/models/BootstrapInfo.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.splash.models 2 | 3 | data class BootstrapInfo( 4 | val initializedCount: Int, 5 | val totalIndexerCount: Int 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/features/splash/viewmodels/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.features.splash.viewmodels 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.JackettService 8 | import com.masterwok.shrimplesearch.features.splash.models.BootstrapInfo 9 | import com.masterwok.xamarininterface.enums.QueryState 10 | import com.masterwok.xamarininterface.models.IndexerQueryResult 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | 15 | class SplashViewModel @Inject constructor( 16 | private val jackettService: JackettService 17 | ) : ViewModel(), JackettService.Listener { 18 | 19 | private val _liveDataBootStrapInfo = MutableLiveData() 20 | private val _liveDataBootStrapCompleted = MutableLiveData() 21 | 22 | val liveDataBoostrapInfo: LiveData = _liveDataBootStrapInfo 23 | val liveDataBootStrapCompleted: LiveData = _liveDataBootStrapCompleted 24 | 25 | init { 26 | jackettService.addListener(this) 27 | } 28 | 29 | fun initialize() = viewModelScope.launch { 30 | _liveDataBootStrapInfo.postValue( 31 | BootstrapInfo( 32 | initializedCount = 0, 33 | totalIndexerCount = jackettService.getIndexerCount() 34 | ) 35 | ) 36 | 37 | jackettService.initialize() 38 | } 39 | 40 | override fun onCleared() { 41 | jackettService.removeListener(this) 42 | 43 | super.onCleared() 44 | } 45 | 46 | override fun onIndexersInitialized() { 47 | viewModelScope.launch { 48 | _liveDataBootStrapCompleted.value = Unit 49 | } 50 | } 51 | 52 | override fun onIndexerInitialized() { 53 | viewModelScope.launch { 54 | val newCount = checkNotNull(_liveDataBootStrapInfo.value?.initializedCount) + 1 55 | 56 | _liveDataBootStrapInfo.value = BootstrapInfo( 57 | totalIndexerCount = checkNotNull(_liveDataBootStrapInfo.value?.totalIndexerCount), 58 | initializedCount = newCount 59 | ) 60 | } 61 | } 62 | 63 | override fun onResultsUpdated() = Unit 64 | 65 | override fun onQueryStateChange(queryState: QueryState) = Unit 66 | 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.main 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.activity.viewModels 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.fragment.app.viewModels 9 | import androidx.lifecycle.ViewModelProvider 10 | import androidx.navigation.fragment.NavHostFragment 11 | import androidx.navigation.ui.AppBarConfiguration 12 | import androidx.navigation.ui.setupWithNavController 13 | import com.afollestad.materialdialogs.MaterialDialog 14 | import com.afollestad.materialdialogs.customview.customView 15 | import com.afollestad.materialdialogs.customview.getCustomView 16 | import com.masterwok.shrimplesearch.R 17 | import com.masterwok.shrimplesearch.common.constants.AnalyticEvent 18 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository 19 | import com.masterwok.shrimplesearch.common.data.services.contracts.AnalyticService 20 | import com.masterwok.shrimplesearch.di.AppInjector 21 | import com.masterwok.shrimplesearch.features.query.viewmodels.QueryViewModel 22 | import kotlinx.android.synthetic.main.activity_main.* 23 | import kotlinx.android.synthetic.main.include_toolbar_maneki.* 24 | import kotlinx.android.synthetic.main.view_dialog_exit.view.* 25 | import javax.inject.Inject 26 | 27 | class MainActivity : AppCompatActivity() { 28 | 29 | @Inject 30 | lateinit var analyticService: AnalyticService 31 | 32 | @Inject 33 | lateinit var viewModelFactory: ViewModelProvider.Factory 34 | 35 | private val viewModel: MainActivityViewModel by viewModels { viewModelFactory } 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | AppInjector 39 | .mainComponent 40 | .inject(this) 41 | 42 | setTheme(viewModel.themeId) 43 | 44 | super.onCreate(savedInstanceState) 45 | 46 | setContentView(R.layout.activity_main) 47 | 48 | initSupportActionBar() 49 | initNavigation() 50 | } 51 | 52 | private fun initSupportActionBar() { 53 | setSupportActionBar(toolbar) 54 | 55 | supportActionBar?.apply { 56 | setDisplayShowTitleEnabled(false) 57 | setDisplayShowHomeEnabled(false) 58 | } 59 | } 60 | 61 | private val navController by lazy { 62 | val navHostFragment = supportFragmentManager 63 | .findFragmentById(R.id.fragmentNavHost) as NavHostFragment 64 | 65 | navHostFragment.navController 66 | } 67 | 68 | private fun initNavigation() { 69 | val appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout) 70 | 71 | toolbar.setupWithNavController(navController, appBarConfiguration) 72 | 73 | navigationView.setupWithNavController(navController) 74 | 75 | navController.addOnDestinationChangedListener { _, destination, _ -> 76 | when (destination.id) { 77 | R.id.aboutFragment -> analyticService.logEvent(AnalyticEvent.MenuItemAboutTapped) 78 | } 79 | } 80 | } 81 | 82 | override fun onBackPressed() = when (navController.graph.startDestination) { 83 | navController.currentDestination?.id -> { 84 | if (viewModel.isExitDialogEnabled) { 85 | presentQuitAppDialog() 86 | } else { 87 | exitApplication() 88 | } 89 | } 90 | else -> super.onBackPressed() 91 | } 92 | 93 | private fun exitApplication() { 94 | viewModel.cancelQuery() 95 | finishAndRemoveTask() 96 | } 97 | 98 | private fun presentQuitAppDialog() { 99 | MaterialDialog(this).show { 100 | customView(R.layout.view_dialog_exit) 101 | positiveButton(res = R.string.dialog_exit) { 102 | if (getCustomView().checkBoxDontAskAgain.isChecked) { 103 | viewModel.disableExitDialog() 104 | } 105 | 106 | exitApplication() 107 | } 108 | negativeButton(res = R.string.dialog_cancel) 109 | } 110 | } 111 | 112 | companion object { 113 | fun createIntent(context: Context) = Intent(context, MainActivity::class.java) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/main/MainActivityViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.main 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.JackettService 6 | import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository 7 | import kotlinx.coroutines.launch 8 | import javax.inject.Inject 9 | 10 | class MainActivityViewModel @Inject constructor( 11 | private val userSettingsRepository: UserSettingsRepository, 12 | private val jackettService: JackettService 13 | ) : ViewModel() { 14 | 15 | val themeId get() = userSettingsRepository.getThemeId() 16 | 17 | val isExitDialogEnabled 18 | get() = userSettingsRepository 19 | .read() 20 | .isExitDialogEnabled 21 | 22 | fun disableExitDialog() = userSettingsRepository.update( 23 | userSettingsRepository 24 | .read() 25 | .copy(isExitDialogEnabled = false) 26 | ) 27 | 28 | fun cancelQuery() = viewModelScope.launch { 29 | jackettService.cancelQuery() 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/main/di/MainModule.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.main.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.masterwok.shrimplesearch.di.annotations.ViewModelKey 5 | import com.masterwok.shrimplesearch.main.MainActivityViewModel 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.multibindings.IntoMap 9 | 10 | 11 | @Module(includes = [MainModule.Declarations::class]) 12 | class MainModule { 13 | 14 | @Suppress("RedundantModalityModifier", "unused") 15 | @Module 16 | interface Declarations { 17 | @Binds 18 | @IntoMap 19 | @ViewModelKey(MainActivityViewModel::class) 20 | abstract fun bindViewModel(viewModel: MainActivityViewModel): ViewModel 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/main/di/MainScope.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.main.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention 7 | annotation class MainScope 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/masterwok/shrimplesearch/main/di/MainSubcomponent.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch.main.di 2 | 3 | import com.masterwok.shrimplesearch.main.MainActivity 4 | import dagger.Subcomponent 5 | 6 | @MainScope 7 | @Subcomponent(modules = [MainModule::class]) 8 | interface MainSubcomponent { 9 | @Subcomponent.Factory 10 | interface Factory { 11 | fun create(): MainSubcomponent 12 | } 13 | 14 | fun inject(mainActivity: MainActivity) 15 | } -------------------------------------------------------------------------------- /app/src/main/res/color/button_text_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/color/pill_text_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/radio_button_tint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_button_selected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_button_unselected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_edit_text_rounded.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_indexer_query_result_rounded.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_pill.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_pill_selected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_pill_unselected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_query_auto_complete_popup.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_stat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cursor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/divider_flexbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/divider_recycler_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_downward_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_upward_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_auto_complete_clear.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_open_in_new_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_share_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chevron_right_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_content_copy_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_insert_drive_file_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_link_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_magnet_black.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 14 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sort_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/font/eina_03_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/font/eina_03_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/eina_03_semi_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/font/eina_03_semi_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 18 | 19 | 20 | 21 | 31 | 32 | 33 | 34 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 20 | 21 | 34 | 35 | 48 | 49 | 65 | 66 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/res/layout/component_sort_by.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 34 | 35 | 46 | 47 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | 38 | 39 | 54 | 55 | 56 | 72 | 73 | 86 | 87 | 102 | 103 | 119 | 120 | 136 | 137 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_indexer_query_results.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 28 | 29 | 34 | 35 | 36 | 37 | 44 | 45 | 53 | 54 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_query.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 29 | 30 | 36 | 37 | 38 | 39 | 46 | 47 | 55 | 56 | 65 | 66 | 67 | 72 | 73 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 31 | 32 | 38 | 39 | 45 | 46 | 47 | 48 | 59 | 60 | 66 | 67 | 75 | 76 | 77 | 87 | 88 | 99 | 100 | 106 | 107 | 115 | 116 | 117 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /app/src/main/res/layout/include_toolbar_maneki.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/include_toolbar_query.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_dialog_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_material_dialog_icon_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 22 | 23 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_query_result_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 31 | 32 | 45 | 46 | 51 | 52 | 60 | 61 | 62 | 63 | 75 | 76 | 82 | 83 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /app/src/main/res/menu/drawer_layout_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_sort.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /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/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_no_search_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-hdpi/ic_no_search_results.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-hdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_shrimple_smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-hdpi/ic_shrimple_smile.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_no_search_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-mdpi/ic_no_search_results.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-mdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_shrimple_smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-mdpi/ic_shrimple_smile.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_no_search_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xhdpi/ic_no_search_results.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xhdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_shrimple_smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xhdpi/ic_shrimple_smile.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_no_search_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxhdpi/ic_no_search_results.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxhdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_shrimple_smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxhdpi/ic_shrimple_smile.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_no_search_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxxhdpi/ic_no_search_results.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxxhdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_shrimple_smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/app/src/main/res/mipmap-xxxhdpi/ic_shrimple_smile.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 16 | 17 | 22 | 27 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Maneki 3 | 4 | Settings 5 | About 6 | 7 | Initializing space cats… 8 | %.0f%% 9 | MANEKI 10 | 11 | MANEKI 12 | Search … 13 | 14 | 15 | Hello blank fragment 16 | 17 | All 18 | n/a 19 | Published On: 20 | A search icon. 21 | 22 | Use the toolbar above to search for magnets… 23 | Sort 24 | 25 | Sort By 26 | Name 27 | Published On 28 | Size 29 | Seeders 30 | Peers 31 | Order By 32 | Ascending 33 | Descending 34 | Done 35 | Cancel 36 | Indexer Name 37 | Magnet Count 38 | Link Count 39 | Aggregate Count 40 | No search results 41 | No matching results found… 42 | 43 | Maneki is an open-source native Android application powered by Jackett.\n\n 44 | The source code is available on GitHub. Contributions are welcome. 45 | 46 | 47 | If you have a moment, please consider taking some time to leave a review.\n\n 48 | 49 | Your feedback is appreciated! 50 | 51 | About: 52 | View on Github 53 | Version: %s 54 | https://github.com/masterwok/maneki 55 | Settings: 56 | There\'s not much to see here right now, but the following settings can be used to configure the application: 57 | Unable to find torrent client. Please ensure that you have a torrent client installed on your device. 58 | Ok 59 | Houston, we have a problem… 60 | Unable to open GitHub. Please ensure that you have a web browser installed on your device. 61 | Review Maneki 62 | Unable to open the Google Play Store. 63 | Review: 64 | Theme: 65 | Light 66 | OLED 67 | New query results 68 | SCROLL TO TOP 69 | Share Magnet 70 | Open Magnet 71 | Copy Magnet 72 | Share Link 73 | Copy Link 74 | Open Link 75 | Present scroll to top notifications 76 | Enable or disable the scroll to top query results notification when new results are added. 77 | Only magnet query results 78 | When enabled, only magnet query result items will be included in the results. 79 | Do you want to exit? 80 | Don\'t ask again 81 | Exit 82 | Cancel 83 | Query Cancelled 84 | Share Maneki 85 | Share Maneki 86 | Check out this torrent search engine for Android:\n\n%s 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/src/test/java/com/masterwok/shrimplesearch/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.shrimplesearch 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.4.0' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:4.0.1' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | 16 | classpath 'com.google.gms:google-services:4.3.3' 17 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.1' 18 | 19 | classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" 20 | } 21 | } 22 | 23 | allprojects { 24 | repositories { 25 | google() 26 | jcenter() 27 | maven { url 'https://jitpack.io' } 28 | } 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | 35 | project.ext.compileSdkVersion = 30 36 | project.ext.minSdkVersion = 21 37 | project.ext.targetSdkVersion = 30 38 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterwok/maneki/7de240b0fcfe9992b258e5208de15b4551c461f6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Aug 30 12:18:14 EDT 2020 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-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='Maneki' 2 | include ':app', ':xamarin', ':xamarininterface' 3 | -------------------------------------------------------------------------------- /xamarin/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /xamarin/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 | -------------------------------------------------------------------------------- /xamarin/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 10 | 11 | 13 | 14 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /xamarin/src/main/java/com/masterwok/xamarin/JackettHarnessFactory.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarin 2 | 3 | import com.masterwok.xamarininterface.contracts.ICardigannDefinitionRepository 4 | import com.masterwok.xamarininterface.contracts.IJackettHarness 5 | import com.masterwok.xamarininterface.contracts.IJackettHarnessListener 6 | 7 | interface JackettHarnessFactory { 8 | fun createInstance( 9 | cardigannDefinitionRepository: ICardigannDefinitionRepository 10 | ) : IJackettHarness 11 | } -------------------------------------------------------------------------------- /xamarin/src/main/java/com/masterwok/xamarin/JackettHarnessFactoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarin 2 | 3 | import com.masterwok.jackett.JackettHarness 4 | import com.masterwok.xamarininterface.contracts.ICardigannDefinitionRepository 5 | import com.masterwok.xamarininterface.contracts.IJackettHarness 6 | import com.masterwok.xamarininterface.contracts.IJackettHarnessListener 7 | 8 | object JackettHarnessFactoryImpl : JackettHarnessFactory { 9 | 10 | override fun createInstance( 11 | cardigannDefinitionRepository: ICardigannDefinitionRepository 12 | ): IJackettHarness = JackettHarness( 13 | cardigannDefinitionRepository 14 | ) 15 | 16 | } -------------------------------------------------------------------------------- /xamarin/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | xamarin 3 | 4 | -------------------------------------------------------------------------------- /xamarininterface/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /xamarininterface/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion project.compileSdkVersion 6 | defaultConfig { 7 | minSdkVersion project.minSdkVersion 8 | targetSdkVersion project.targetSdkVersion 9 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 10 | } 11 | buildTypes { 12 | release { 13 | minifyEnabled false 14 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 15 | } 16 | } 17 | } 18 | 19 | dependencies { 20 | implementation fileTree(dir: 'libs', include: ['*.jar']) 21 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 22 | } 23 | 24 | repositories { 25 | mavenCentral() 26 | } 27 | -------------------------------------------------------------------------------- /xamarininterface/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 | -------------------------------------------------------------------------------- /xamarininterface/src/androidTest/java/com/masterwok/xamarininterface/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest 19 | { 20 | @Test 21 | public void useAppContext() throws Exception 22 | { 23 | // Context of the app under test. 24 | Context appContext = InstrumentationRegistry.getTargetContext(); 25 | 26 | assertEquals("com.masterwok.xamarininterface.test", appContext.getPackageName()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /xamarininterface/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /xamarininterface/src/main/java/com/masterwok/xamarininterface/contracts/ICardigannDefinitionRepository.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface.contracts 2 | 3 | interface ICardigannDefinitionRepository { 4 | 5 | fun getDefinitions(): List 6 | 7 | fun getIndexerCount(): Int 8 | 9 | } 10 | -------------------------------------------------------------------------------- /xamarininterface/src/main/java/com/masterwok/xamarininterface/contracts/IJackettHarness.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface.contracts 2 | 3 | import com.masterwok.xamarininterface.enums.QueryState 4 | import com.masterwok.xamarininterface.models.IndexerQueryResult 5 | import com.masterwok.xamarininterface.models.Query 6 | 7 | interface IJackettHarness { 8 | 9 | val isInitialized: Boolean 10 | 11 | val queryState: QueryState? 12 | 13 | val queryResults: List 14 | 15 | fun initialize() 16 | 17 | fun setListener(jackettHarnessListener: IJackettHarnessListener) 18 | 19 | fun getIndexerCount(): Int 20 | 21 | fun query(query: Query) 22 | 23 | fun cancelQuery() 24 | 25 | } -------------------------------------------------------------------------------- /xamarininterface/src/main/java/com/masterwok/xamarininterface/contracts/IJackettHarnessListener.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface.contracts 2 | 3 | import com.masterwok.xamarininterface.enums.QueryState 4 | 5 | interface IJackettHarnessListener { 6 | 7 | fun onIndexersInitialized() 8 | 9 | fun onIndexerInitialized() 10 | 11 | fun onResultsUpdated() 12 | 13 | fun onQueryStateChange(queryState: QueryState) 14 | 15 | } -------------------------------------------------------------------------------- /xamarininterface/src/main/java/com/masterwok/xamarininterface/enums/IndexerQueryState.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface.enums 2 | 3 | enum class IndexerQueryState { 4 | Success, 5 | Failure, 6 | Aborted 7 | } -------------------------------------------------------------------------------- /xamarininterface/src/main/java/com/masterwok/xamarininterface/enums/IndexerType.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface.enums 2 | 3 | enum class IndexerType { 4 | Public, 5 | Private, 6 | Aggregate 7 | } -------------------------------------------------------------------------------- /xamarininterface/src/main/java/com/masterwok/xamarininterface/enums/QueryState.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface.enums 2 | 3 | enum class QueryState { 4 | Pending, 5 | Completed, 6 | Aborted 7 | } -------------------------------------------------------------------------------- /xamarininterface/src/main/java/com/masterwok/xamarininterface/models/Indexer.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface.models 2 | 3 | import com.masterwok.xamarininterface.enums.IndexerType 4 | 5 | data class Indexer( 6 | val id: String, 7 | val type: IndexerType, 8 | val displayName: String, 9 | val displayDescription: String? 10 | ) -------------------------------------------------------------------------------- /xamarininterface/src/main/java/com/masterwok/xamarininterface/models/IndexerQueryResult.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface.models 2 | 3 | import com.masterwok.xamarininterface.enums.IndexerQueryState 4 | 5 | data class IndexerQueryResult( 6 | val indexer: Indexer, 7 | val items: List, 8 | val queryState: IndexerQueryState, 9 | val failureReason: String?, 10 | val magnetCount: Int, 11 | val linkCount: Int 12 | ) -------------------------------------------------------------------------------- /xamarininterface/src/main/java/com/masterwok/xamarininterface/models/Query.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface.models 2 | 3 | data class Query( 4 | val queryString: String 5 | ) -------------------------------------------------------------------------------- /xamarininterface/src/main/java/com/masterwok/xamarininterface/models/QueryResultItem.kt: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface.models 2 | 3 | import android.net.Uri 4 | import java.util.* 5 | 6 | data class QueryResultItem( 7 | val title: String, 8 | val description: String?, 9 | val linkInfo: LinkInfo, 10 | val socialInfo: SocialInfo, 11 | val statInfo: StatInfo 12 | ) { 13 | data class LinkInfo( 14 | val magnetUri: Uri?, 15 | val infoHash: String?, 16 | val link: Uri?, 17 | val details: Uri?, 18 | val posterUri: Uri? 19 | ) 20 | 21 | data class SocialInfo( 22 | val rageId: Long?, 23 | val tvdbId: Long?, 24 | val imdb: Long?, 25 | val tmdb: Long? 26 | ) 27 | 28 | data class StatInfo( 29 | val publishedOn: Date?, 30 | val seeders: Long?, 31 | val peers: Long?, 32 | val size: Long?, 33 | val files: Long?, 34 | val grabs: Long?, 35 | val minimumRatio: Double?, 36 | val minimumSeedTime: Long?, 37 | val downloadVolumeFactor: Double?, 38 | val uploadVolumeFactor: Double? 39 | ) 40 | } 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /xamarininterface/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | xamarininterface 3 | 4 | -------------------------------------------------------------------------------- /xamarininterface/src/test/java/com/masterwok/xamarininterface/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.masterwok.xamarininterface; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest 13 | { 14 | @Test 15 | public void addition_isCorrect() throws Exception 16 | { 17 | assertEquals(4, 2 + 2); 18 | } 19 | } --------------------------------------------------------------------------------