├── .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 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_sort.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------