├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── android_glide_lint.xml
├── app
├── .gitignore
├── build.gradle
├── google-services.json
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── josycom
│ │ └── mayorjay
│ │ └── flowoverstack
│ │ ├── ExampleInstrumentedTest.kt
│ │ └── ScreenNavigationTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── josycom
│ │ │ └── mayorjay
│ │ │ └── flowoverstack
│ │ │ ├── MyApplication.kt
│ │ │ ├── data
│ │ │ ├── mapper
│ │ │ │ └── Mappers.kt
│ │ │ ├── model
│ │ │ │ ├── Answer.kt
│ │ │ │ ├── Owner.kt
│ │ │ │ ├── Question.kt
│ │ │ │ └── Tag.kt
│ │ │ ├── remote
│ │ │ │ ├── datasource
│ │ │ │ │ ├── AnswerPagingSource.kt
│ │ │ │ │ ├── QuestionPagingSource.kt
│ │ │ │ │ └── TagPagingSource.kt
│ │ │ │ ├── model
│ │ │ │ │ ├── AnswerRemote.kt
│ │ │ │ │ ├── AnswerResponse.kt
│ │ │ │ │ ├── OwnerRemote.kt
│ │ │ │ │ ├── QuestionRemote.kt
│ │ │ │ │ ├── QuestionsResponse.kt
│ │ │ │ │ ├── SearchResponse.kt
│ │ │ │ │ ├── TagRemote.kt
│ │ │ │ │ └── TagsResponse.kt
│ │ │ │ └── service
│ │ │ │ │ └── ApiService.kt
│ │ │ └── repository
│ │ │ │ ├── AnswerRepository.kt
│ │ │ │ ├── PreferenceRepository.kt
│ │ │ │ ├── PreferenceRepositoryImpl.kt
│ │ │ │ ├── QuestionRepository.kt
│ │ │ │ ├── SearchRepository.kt
│ │ │ │ └── TagRepository.kt
│ │ │ ├── di
│ │ │ ├── ApiModule.kt
│ │ │ ├── DataStoreModule.kt
│ │ │ └── RepositoryModule.kt
│ │ │ ├── util
│ │ │ ├── AppConstants.kt
│ │ │ ├── AppLogger.kt
│ │ │ ├── AppUtils.kt
│ │ │ └── ThreadExecutor.kt
│ │ │ ├── view
│ │ │ ├── answer
│ │ │ │ ├── AnswerActivity.kt
│ │ │ │ ├── AnswerAdapter.kt
│ │ │ │ └── WebViewActivity.kt
│ │ │ ├── home
│ │ │ │ ├── PagingLoadStateAdapter.kt
│ │ │ │ ├── QuestionActivity.kt
│ │ │ │ ├── QuestionAdapter.kt
│ │ │ │ └── QuestionsFragment.kt
│ │ │ ├── init
│ │ │ │ └── SplashActivity.kt
│ │ │ ├── ocr
│ │ │ │ └── OcrActivity.kt
│ │ │ ├── search
│ │ │ │ ├── SearchActivity.kt
│ │ │ │ └── SearchAdapter.kt
│ │ │ └── tag
│ │ │ │ ├── TagsAdapter.kt
│ │ │ │ └── TagsDialogFragment.kt
│ │ │ └── viewmodel
│ │ │ ├── AnswerViewModel.kt
│ │ │ ├── QuestionActivityViewModel.kt
│ │ │ ├── QuestionViewModel.kt
│ │ │ ├── SearchViewModel.kt
│ │ │ └── TagsDialogViewModel.kt
│ └── res
│ │ ├── anim
│ │ ├── fab_close.xml
│ │ ├── fab_open.xml
│ │ ├── fade_in_anim.xml
│ │ ├── fade_out_anim.xml
│ │ ├── slide_in_left.xml
│ │ ├── slide_in_right.xml
│ │ ├── slide_out_left.xml
│ │ └── slide_out_right.xml
│ │ ├── animator
│ │ └── search_button_state_list_anim.xml
│ │ ├── drawable-anydpi
│ │ ├── ic_action_scroll_up.xml
│ │ └── ic_info.xml
│ │ ├── drawable-hdpi
│ │ ├── ic_action_scroll_up.png
│ │ └── ic_info.png
│ │ ├── drawable-mdpi
│ │ ├── ic_action_scroll_up.png
│ │ └── ic_info.png
│ │ ├── drawable-xhdpi
│ │ ├── ic_action_scroll_up.png
│ │ ├── ic_info.png
│ │ └── loading.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_action_scroll_up.png
│ │ └── ic_info.png
│ │ ├── drawable
│ │ ├── app_icon.png
│ │ ├── app_iconn.png
│ │ ├── button_background.xml
│ │ ├── ic_baseline_verified_answer.xml
│ │ ├── ic_camera.xml
│ │ ├── ic_clear.xml
│ │ ├── ic_close.xml
│ │ ├── ic_date.xml
│ │ ├── ic_email.xml
│ │ ├── ic_keyboard.xml
│ │ ├── ic_refresh.xml
│ │ ├── ic_search.xml
│ │ ├── ic_share.xml
│ │ ├── ic_share_green.xml
│ │ ├── ic_tag.xml
│ │ ├── ic_twitter.xml
│ │ ├── ic_view.xml
│ │ ├── ic_vote.xml
│ │ ├── splash_screen.xml
│ │ └── tag_bg.xml
│ │ ├── font
│ │ ├── magra.xml
│ │ ├── montserrat.xml
│ │ ├── montserrat_bold.xml
│ │ ├── montserrat_light.otf
│ │ ├── poppins_bold.ttf
│ │ ├── poppins_light.ttf
│ │ └── poppins_semibold.ttf
│ │ ├── layout
│ │ ├── activity_answer.xml
│ │ ├── activity_ocr.xml
│ │ ├── activity_question.xml
│ │ ├── activity_search.xml
│ │ ├── activity_web_view.xml
│ │ ├── answer_item.xml
│ │ ├── content_main.xml
│ │ ├── fragment_questions.xml
│ │ ├── layout_info_dialog.xml
│ │ ├── paging_load_state_footer_view_item.xml
│ │ ├── question_item.xml
│ │ ├── tag_item.xml
│ │ └── tags_dialog_fragment.xml
│ │ ├── menu
│ │ ├── menu_main.xml
│ │ └── menu_webview.xml
│ │ ├── mipmap-hdpi
│ │ └── app_icon.png
│ │ ├── mipmap-mdpi
│ │ └── app_icon.png
│ │ ├── mipmap-xhdpi
│ │ └── app_icon.png
│ │ ├── mipmap-xxhdpi
│ │ └── app_icon.png
│ │ ├── mipmap-xxxhdpi
│ │ └── app_icon.png
│ │ ├── values-sw600dp
│ │ └── attrs.xml
│ │ ├── values-xlarge
│ │ └── attrs.xml
│ │ ├── values
│ │ ├── attrs.xml
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── font_certs.xml
│ │ ├── plurals.xml
│ │ ├── preloaded_fonts.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── provider_path.xml
│ └── test
│ └── java
│ └── com
│ └── josycom
│ └── mayorjay
│ └── flowoverstack
│ ├── ExampleUnitTest.kt
│ ├── testdata
│ └── FakePreferenceRepository.kt
│ ├── util
│ └── AppUtilsTest.kt
│ └── viewmodel
│ └── QuestionActivityViewModelTest.kt
├── app_icon.png
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .DS_Store
5 | /build
6 | /captures
7 | .externalNativeBuild
8 | .cxx
9 | /.idea
10 | /fos-keystore
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | This document contains information and guidelines about contributing to the [FlowOver Stack](https://github.com/mayorJAY/FlowOverStack) project. Please read it before you start participating.
4 |
5 | ## Contributing Code
6 |
7 | To start contributing to the codebase, first fork the repository. Branches are made off of `development` with each feature or issue per branch. Once done, take a look at open [Issues](https://github.com/mayorJAY/FlowOverStack/issues), pick anyone you're comfortable with, code it out and submit a Pull Request. I will review the code and respond if changes should be made or just merge if it looks great!
8 |
9 | ## Submitting an Issue
10 |
11 | Please feel free to open an issue if you find something that make you unhappy with the application. It can as well be a feature request - you think there is a particular feature that should be on the application, just pen it down [here](https://github.com/mayorJAY/FlowOverStack/issues).
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Joseph Olugbohunmi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # StackOverflow - Community Version
6 |
7 | An Android StackOverflow client application implemented using the MVVM architecture, Retrofit2, LiveData, Flow, ViewModel, Repository pattern, View Binding. Users can get to view Questions which have been asked on Stack Overflow; picking a particular Question makes the user view it in detail as well as the Answers provided. These Questions can be filtered by any of these four categories; Active, Recent, Hot or Voted. Questions that have an accepted Answer are easily identified.
8 | Users can also search for a particular problem they are having by typing in a search query or by capturing an image and performing an OCR on it. Questions are curated based on the search query and presented to the user; again, the user can pick a particular Question to view the provided Answers. Users also have the options to keep a tab on a Tag, filter out questions by Tags, search for any Tag of interest, share questions and answers with themselves or other developers.
9 | # Tech Stack
10 |
11 | * [Retrofit](https://square.github.io/retrofit/) which is a type-safe REST client for Android which makes it easier to consume RESTful web services
12 | * [Paging Library](https://developer.android.com/topic/libraries/architecture/paging) which helps to load and display small chunks of data at a time
13 | * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) used to store and manage UI-related data in a lifecycle conscious way
14 | * [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) which is an observable data holder class used to handle data in a lifecycle-aware manner
15 | * [Glide](https://github.com/bumptech/glide) which is an image loading and caching library for Android
16 | * [Material Design](https://material.io/develop/android/docs/getting-started/) which is an adaptable system that guides in maintaining principles and best practices of contemporary UI
17 | * [View Binding](https://developer.android.com/topic/libraries/view-binding) used to easily write code that interacts with views by referencing them directly
18 | * [Espresso](https://developer.android.com/training/testing/espresso) used to write concise, beautiful, and reliable Android UI tests
19 | * [MarkdownView](https://github.com/mukeshsolanki/MarkdownView-Android) which is a simple library that helps to display Markdown text or files on Android as a HTML page just like GitHub
20 | * [ML Kit OCR](https://developers.google.com/ml-kit/vision/text-recognition) which is a library that recognizes text in any captured image
21 | * [Android Image Cropper](https://github.com/ArthurHub/Android-Image-Cropper) which is an image cropping library for Android, optimized for camera and gallery.
22 | * [Dagger](https://developer.android.com/training/dependency-injection/dagger-android) for Dependency Injection
23 | * [Kotlin Coroutines](https://developer.android.com/kotlin/coroutines) for executing network calls asynchronously
24 | * [Kotlin flow](https://developer.android.com/kotlin/flow) for emitting live updates from a network call sequentially
25 | * [Preferences DataStore](https://developer.android.com/topic/libraries/architecture/datastore) for storing and retrieving key-value pairs of primitive data types
26 | # Installation
27 |
28 | This App requires a minimum API level of 26. Clone the repository. You will need an API key from [Stack Exchange API](https://api.stackexchange.com/) to receive a higher request quota. Locate the StringConstants.java file and edit the following line to add your API key:
29 |
30 | ````
31 | API_KEY = "YOUR_API_KEY"
32 | ````
33 |
34 | # Contribution
35 | All contributions are welcome. See the [CONTRIBUTING](https://github.com/mayorJAY/FlowOverStack/blob/feat/ocr/CONTRIBUTING.md) file for guidelines on contributing
36 |
37 |
38 |
39 | ## LICENSE
40 | ```
41 | MIT License
42 | Copyright (c) 2021 Joseph Olugbohunmi
43 |
44 | Permission is hereby granted, free of charge, to any person obtaining a copy
45 | of this software and associated documentation files (the "Software"), to deal
46 | in the Software without restriction, including without limitation the rights
47 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
48 | copies of the Software, and to permit persons to whom the Software is
49 | furnished to do so, subject to the following conditions:
50 |
51 | The above copyright notice and this permission notice shall be included in all
52 | copies or substantial portions of the Software.
53 |
54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
55 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
56 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
57 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
58 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
59 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
60 | SOFTWARE.
61 | ```
62 |
--------------------------------------------------------------------------------
/android_glide_lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /debug
3 | /release
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'com.google.gms.google-services'
4 | apply plugin: 'kotlin-kapt'
5 | apply plugin: 'com.google.dagger.hilt.android'
6 |
7 | android {
8 | namespace 'com.josycom.mayorjay.flowoverstack'
9 | compileSdk 34
10 |
11 | defaultConfig {
12 | applicationId "com.josycom.mayorjay.flowoverstack"
13 | minSdk 26
14 | targetSdk 34
15 | versionCode 200
16 | versionName "2.00"
17 | multiDexEnabled true
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | }
21 |
22 | signingConfigs {
23 | Properties keyProps = new Properties()
24 | keyProps.load(new FileInputStream(project.rootProject.file('local.properties')))
25 | debug {
26 | storeFile file(keyProps.getProperty("STORE_FILE"))
27 | storePassword keyProps.getProperty("STORE_PASSWORD", "")
28 | keyAlias keyProps.getProperty("KEY_ALIAS", "")
29 | keyPassword keyProps.getProperty("KEY_PASSWORD", "")
30 | }
31 | release {
32 | storeFile file(keyProps.getProperty("STORE_FILE"))
33 | storePassword keyProps.getProperty("STORE_PASSWORD", "")
34 | keyAlias keyProps.getProperty("KEY_ALIAS", "")
35 | keyPassword keyProps.getProperty("KEY_PASSWORD", "")
36 | }
37 | }
38 |
39 | buildTypes {
40 | debug {
41 | signingConfig signingConfigs.debug
42 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
43 | }
44 | release {
45 | signingConfig signingConfigs.release
46 | minifyEnabled false
47 | shrinkResources false
48 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
49 | }
50 | }
51 |
52 | buildFeatures {
53 | viewBinding true
54 | buildConfig true
55 | }
56 |
57 | compileOptions {
58 | sourceCompatibility = 17
59 | targetCompatibility = 17
60 | }
61 |
62 | kotlinOptions {
63 | jvmTarget = JavaVersion.VERSION_17
64 | freeCompilerArgs += [
65 | "-Xjvm-default=all",
66 | ]
67 | }
68 |
69 | kapt {
70 | correctErrorTypes true
71 | }
72 |
73 | lint {
74 | // https://github.com/bumptech/glide/issues/4940
75 | lintConfig = file("$rootDir/android_glide_lint.xml")
76 | }
77 |
78 | }
79 |
80 | dependencies {
81 | implementation fileTree(dir: 'libs', include: ['*.jar'])
82 |
83 | implementation 'androidx.appcompat:appcompat:1.3.1'
84 | implementation 'com.google.android.material:material:1.4.0'
85 | implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
86 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
87 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
88 | implementation 'androidx.recyclerview:recyclerview:1.2.1'
89 | implementation 'android.arch.lifecycle:extensions:1.1.1'
90 | implementation 'org.jsoup:jsoup:1.13.1'
91 | implementation 'androidx.fragment:fragment-ktx:1.5.3'
92 |
93 | implementation 'com.intuit.sdp:sdp-android:1.0.6'
94 | implementation 'com.intuit.ssp:ssp-android:1.0.6'
95 |
96 | // Timber
97 | implementation 'com.jakewharton.timber:timber:5.0.1'
98 |
99 | // Retrofit
100 | implementation 'com.squareup.retrofit2:retrofit:2.9.0'
101 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
102 | implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
103 |
104 | // Glide
105 | implementation 'com.github.bumptech.glide:glide:4.12.0'
106 |
107 | // Logging
108 | implementation 'com.squareup.okhttp3:logging-interceptor:4.7.2'
109 | implementation 'com.squareup.okhttp3:okhttp:4.7.2'
110 |
111 | // Paging library
112 | implementation 'androidx.paging:paging-runtime:3.1.0'
113 |
114 | // ViewModel
115 | implementation 'android.arch.lifecycle:viewmodel:1.1.1'
116 |
117 | // LiveData
118 | implementation "androidx.lifecycle:lifecycle-livedata:2.4.0"
119 |
120 | // SwipeRefreshLayout
121 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
122 |
123 | // Firebase analytics
124 | implementation 'com.google.firebase:firebase-analytics:20.0.0'
125 |
126 | // Firebase crashlytics
127 | apply plugin: 'com.google.firebase.crashlytics'
128 | implementation 'com.google.firebase:firebase-crashlytics:18.2.4'
129 |
130 | // MarkdownView
131 | implementation 'com.github.mukeshsolanki:MarkdownView-Android:1.0.8'
132 |
133 | // Image cropper
134 | implementation 'com.vanniktech:android-image-cropper:4.3.3'
135 |
136 | // ML Kit
137 | implementation 'com.google.android.gms:play-services-mlkit-text-recognition:17.0.0'
138 |
139 | // Hilt
140 | implementation "com.google.dagger:hilt-android:$hilt_version"
141 | kapt "com.google.dagger:hilt-compiler:$hilt_version"
142 |
143 | // Play core
144 | implementation 'com.google.android.play:asset-delivery:2.2.2'
145 | implementation 'com.google.android.play:feature-delivery:2.1.0'
146 | implementation 'com.google.android.play:app-update:2.1.0'
147 |
148 | // Kotlin
149 | implementation 'androidx.core:core-ktx:1.6.0'
150 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
151 |
152 | // Kotlin Coroutines
153 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
154 |
155 | // Preferences DataStore
156 | implementation 'androidx.datastore:datastore-preferences:1.0.0'
157 |
158 | // Unit tests
159 | testImplementation 'junit:junit:4.13.2'
160 | testImplementation 'androidx.test:core:1.4.0'
161 | testImplementation 'org.robolectric:robolectric:4.3.1'
162 | testImplementation "org.mockito:mockito-core:3.12.4"
163 | testImplementation 'androidx.arch.core:core-testing:2.1.0'
164 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
165 |
166 | // Instrumentation tests
167 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
168 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
169 | androidTestImplementation 'org.hamcrest:hamcrest-library:1.3'
170 | androidTestImplementation 'com.android.support:support-annotations:28.0.0'
171 | androidTestImplementation 'androidx.test:runner:1.4.0'
172 | androidTestImplementation 'androidx.test:rules:1.4.0'
173 | }
174 |
--------------------------------------------------------------------------------
/app/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "138698715536",
4 | "firebase_url": "https://flowoverstack-484af.firebaseio.com",
5 | "project_id": "flowoverstack-484af",
6 | "storage_bucket": "flowoverstack-484af.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:138698715536:android:4c7287ffe6cbac6ad64315",
12 | "android_client_info": {
13 | "package_name": "com.josycom.mayorjay.flowoverstack"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "138698715536-phpumr32ftc1af20cmfjj0tcag7idsj3.apps.googleusercontent.com",
19 | "client_type": 3
20 | }
21 | ],
22 | "api_key": [
23 | {
24 | "current_key": "AIzaSyDgJojmGJflNkPj1rcklwkUD-8jkM74Q6c"
25 | }
26 | ],
27 | "services": {
28 | "appinvite_service": {
29 | "other_platform_oauth_client": [
30 | {
31 | "client_id": "138698715536-phpumr32ftc1af20cmfjj0tcag7idsj3.apps.googleusercontent.com",
32 | "client_type": 3
33 | }
34 | ]
35 | }
36 | }
37 | }
38 | ],
39 | "configuration_version": "1"
40 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/josycom/mayorjay/flowoverstack/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * @see [Testing documentation](http://d.android.com/tools/testing)
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
20 | Assert.assertEquals("com.josycom.mayorjay.flowoverstack", appContext.packageName)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/josycom/mayorjay/flowoverstack/ScreenNavigationTest.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack
2 |
3 | import androidx.test.core.app.ActivityScenario
4 | import androidx.test.core.app.ApplicationProvider
5 | import androidx.test.espresso.Espresso
6 | import androidx.test.espresso.action.ViewActions
7 | import androidx.test.espresso.assertion.ViewAssertions
8 | import androidx.test.espresso.matcher.ViewMatchers
9 | import androidx.test.ext.junit.rules.ActivityScenarioRule
10 | import androidx.test.ext.junit.runners.AndroidJUnit4
11 | import com.josycom.mayorjay.flowoverstack.view.home.QuestionActivity
12 | import org.junit.After
13 | import org.junit.Before
14 | import org.junit.Rule
15 | import org.junit.Test
16 | import org.junit.runner.RunWith
17 |
18 | @RunWith(AndroidJUnit4::class)
19 | class ScreenNavigationTest {
20 |
21 | @get:Rule
22 | val scenarioRule = ActivityScenarioRule(QuestionActivity::class.java)
23 | private var scenario: ActivityScenario? = null
24 |
25 | @Before
26 | fun setup() {
27 | scenario = scenarioRule.scenario
28 | }
29 |
30 | @After
31 | fun tearDown() {
32 | scenario?.close()
33 | }
34 |
35 | @Test
36 | fun test_clickMenuOption_displayQuestionsFilteredByCreation() {
37 | Espresso.openActionBarOverflowOrOptionsMenu(ApplicationProvider.getApplicationContext())
38 | Espresso.onView(ViewMatchers.withText(R.string.action_filter_by_recency)).perform(ViewActions.click())
39 | Espresso.onView(ViewMatchers.withText(R.string.recent_questions)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
40 | }
41 |
42 | @Test
43 | fun test_clickMenuOption_displayQuestionsFilteredByHot() {
44 | Espresso.openActionBarOverflowOrOptionsMenu(ApplicationProvider.getApplicationContext())
45 | Espresso.onView(ViewMatchers.withText(R.string.action_filter_by_hot)).perform(ViewActions.click())
46 | Espresso.onView(ViewMatchers.withText(R.string.hot_questions)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
47 | }
48 |
49 | @Test
50 | fun test_clickMenuOption_displayQuestionsFilteredByVotes() {
51 | Espresso.openActionBarOverflowOrOptionsMenu(ApplicationProvider.getApplicationContext())
52 | Espresso.onView(ViewMatchers.withText(R.string.action_filter_by_vote)).perform(ViewActions.click())
53 | Espresso.onView(ViewMatchers.withText(R.string.voted_questions)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
54 | }
55 |
56 | @Test
57 | fun test_clickMenuOption_displayPopularTagsFragment() {
58 | Espresso.openActionBarOverflowOrOptionsMenu(ApplicationProvider.getApplicationContext())
59 | Espresso.onView(ViewMatchers.withText(R.string.filter_by_popular_tags)).perform(ViewActions.click())
60 | Espresso.onView(ViewMatchers.withText(R.string.select_a_tag_to_view_the_related_questions)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
61 | }
62 |
63 | @Test
64 | fun test_clickMenuOption_displaySearchTagsFragment() {
65 | Espresso.openActionBarOverflowOrOptionsMenu(ApplicationProvider.getApplicationContext())
66 | Espresso.onView(ViewMatchers.withText(R.string.filter_by_tag_name)).perform(ViewActions.click())
67 | Espresso.onView(ViewMatchers.withId(R.id.search_text_input_layout)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
68 | }
69 |
70 | @Test
71 | fun test_clickSearchFab_launchSearchActivity() {
72 | Espresso.onView(ViewMatchers.withId(R.id.search_fab)).perform(ViewActions.click())
73 | Espresso.onView(ViewMatchers.withId(R.id.type_to_search)).perform(ViewActions.click())
74 | Espresso.onView(ViewMatchers.withId(R.id.search_text_input_layout)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
75 | }
76 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
35 |
38 |
39 |
43 |
46 |
47 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
62 |
63 |
64 |
65 |
66 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/MyApplication.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack
2 |
3 | import android.app.Application
4 | import com.josycom.mayorjay.flowoverstack.util.AppLogger
5 | import dagger.hilt.android.HiltAndroidApp
6 |
7 | @HiltAndroidApp
8 | class MyApplication : Application() {
9 |
10 | override fun onCreate() {
11 | super.onCreate()
12 | AppLogger.init()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/mapper/Mappers.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.mapper
2 |
3 | import com.josycom.mayorjay.flowoverstack.data.model.Answer
4 | import com.josycom.mayorjay.flowoverstack.data.model.Owner
5 | import com.josycom.mayorjay.flowoverstack.data.model.Question
6 | import com.josycom.mayorjay.flowoverstack.data.model.Tag
7 | import com.josycom.mayorjay.flowoverstack.data.remote.model.AnswerRemote
8 | import com.josycom.mayorjay.flowoverstack.data.remote.model.OwnerRemote
9 | import com.josycom.mayorjay.flowoverstack.data.remote.model.QuestionRemote
10 | import com.josycom.mayorjay.flowoverstack.data.remote.model.TagRemote
11 |
12 | fun QuestionRemote.toQuestion(): Question {
13 | return Question(
14 | tags = this.tags ?: emptyList(),
15 | body = this.body ?: "",
16 | owner = this.owner?.toOwner() ?: Owner(),
17 | isAnswered = this.isAnswered ?: false,
18 | viewCount = this.viewCount ?: 0,
19 | acceptedAnswerId = this.acceptedAnswerId ?: 0,
20 | answerCount = this.answerCount ?: 0,
21 | score = this.score ?: 0,
22 | lastActivityDate = this.lastActivityDate ?: 0,
23 | creationDate = this.creationDate ?: 0,
24 | lastEditDate = this.lastEditDate ?: 0,
25 | questionId = this.questionId ?: 0,
26 | link = this.link ?: "",
27 | title = this.title ?: ""
28 | )
29 | }
30 |
31 | fun OwnerRemote.toOwner(): Owner {
32 | return Owner(
33 | reputation = this.reputation ?: 0,
34 | userId = this.userId ?: 0,
35 | userType = this.userType ?: "",
36 | acceptRate = this.acceptRate ?: 0,
37 | profileImage = this.profileImage ?: "",
38 | displayName = this.displayName ?: "",
39 | link = this.link ?: ""
40 | )
41 | }
42 |
43 | fun AnswerRemote.toAnswer(): Answer {
44 | return Answer(
45 | owner = this.owner?.toOwner() ?: Owner(),
46 | isAccepted = this.isAccepted ?: false,
47 | score = this.score ?: 0,
48 | lastActivityDate = this.lastActivityDate ?: 0,
49 | creationDate = this.creationDate ?: 0,
50 | lastEditDate = this.lastEditDate ?: 0,
51 | answerId = this.answerId ?: 0,
52 | questionId = this.questionId ?: 0,
53 | body = this.body ?: "",
54 | )
55 | }
56 |
57 | fun TagRemote.toTag(): Tag {
58 | return Tag(
59 | name = this.name ?: "",
60 | count = this.count ?: 0L,
61 | hasSynonyms = this.hasSynonyms ?: false,
62 | isModeratorOnly = this.isModeratorOnly ?: false,
63 | isRequired = this.isRequired ?: false
64 | )
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/model/Answer.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.model
2 |
3 | import java.io.Serializable
4 |
5 | data class Answer(
6 | val owner: Owner = Owner(),
7 | val isAccepted: Boolean = false,
8 | val score: Int = 0,
9 | val lastActivityDate: Int = 0,
10 | val lastEditDate: Int = 0,
11 | val creationDate: Int = 0,
12 | val answerId: Int = 0,
13 | val questionId: Int = 0,
14 | val body: String = ""
15 | ) : Serializable
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/model/Owner.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.model
2 |
3 | import java.io.Serializable
4 |
5 | data class Owner(
6 | val reputation: Int = 0,
7 | val userId: Int = 0,
8 | val userType: String = "",
9 | val acceptRate: Int = 0,
10 | val profileImage: String = "",
11 | val displayName: String = "",
12 | val link: String = ""
13 | ) : Serializable
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/model/Question.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.model
2 |
3 | import java.io.Serializable
4 |
5 | data class Question(
6 | val tags: List = emptyList(),
7 | val body: String = "",
8 | val owner: Owner = Owner(),
9 | val isAnswered: Boolean = false,
10 | val viewCount: Int = 0,
11 | val acceptedAnswerId: Int = 0,
12 | val answerCount: Int = 0,
13 | val score: Int = 0,
14 | val lastActivityDate: Int = 0,
15 | val creationDate: Int = 0,
16 | val lastEditDate: Int = 0,
17 | val questionId: Int = 0,
18 | val link: String = "",
19 | val title: String = ""
20 | ) : Serializable
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/model/Tag.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.model
2 |
3 | import java.io.Serializable
4 |
5 | class Tag(
6 | val name: String = "",
7 | val count: Long = 0L,
8 | val hasSynonyms: Boolean = false,
9 | val isModeratorOnly: Boolean = false,
10 | val isRequired: Boolean = false
11 | ) : Serializable
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/datasource/AnswerPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.datasource
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import com.josycom.mayorjay.flowoverstack.data.mapper.toAnswer
6 | import com.josycom.mayorjay.flowoverstack.data.model.Answer
7 | import com.josycom.mayorjay.flowoverstack.data.remote.service.ApiService
8 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
9 | import retrofit2.HttpException
10 | import java.io.IOException
11 |
12 | class AnswerPagingSource(
13 | private val questionId: Int,
14 | private val order: String,
15 | private val sortCondition: String,
16 | private val site: String,
17 | private val filter: String,
18 | private val siteKey: String,
19 | private val apiService: ApiService
20 | ) : PagingSource() {
21 |
22 | override fun getRefreshKey(state: PagingState): Int? {
23 | return state.anchorPosition?.let { anchorPosition ->
24 | state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
25 | ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
26 | }
27 | }
28 |
29 | override suspend fun load(params: LoadParams): LoadResult {
30 | val position = params.key ?: AppConstants.FIRST_PAGE
31 | return try {
32 | val response = apiService.getAnswersToQuestion(
33 | questionId,
34 | order,
35 | sortCondition,
36 | site,
37 | filter,
38 | siteKey
39 | )
40 | val responseItems: List = response.items.map { it.toAnswer() }
41 | val nextKey =
42 | if (responseItems.isEmpty() || responseItems.size <= AppConstants.PAGE_SIZE) {
43 | null
44 | } else {
45 | position + (params.loadSize / AppConstants.PAGE_SIZE)
46 | }
47 | LoadResult.Page(
48 | data = responseItems,
49 | prevKey = if (position == AppConstants.FIRST_PAGE) null else position - 1,
50 | nextKey = nextKey
51 | )
52 | } catch (exception: IOException) {
53 | LoadResult.Error(exception)
54 | } catch (exception: HttpException) {
55 | LoadResult.Error(exception)
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/datasource/QuestionPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.datasource
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import com.josycom.mayorjay.flowoverstack.data.mapper.toQuestion
6 | import com.josycom.mayorjay.flowoverstack.data.model.Question
7 | import com.josycom.mayorjay.flowoverstack.data.remote.service.ApiService
8 | import retrofit2.HttpException
9 | import java.io.IOException
10 |
11 | class QuestionPagingSource(
12 | private val page: Int,
13 | private val pageSize: Int,
14 | private val order: String,
15 | private val sortCondition: String,
16 | private val site: String,
17 | private val tagged: String,
18 | private val filter: String,
19 | private val siteKey: String,
20 | private val apiService: ApiService
21 | ) : PagingSource() {
22 |
23 | override fun getRefreshKey(state: PagingState): Int? {
24 | return state.anchorPosition?.let { anchorPosition ->
25 | state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
26 | ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
27 | }
28 | }
29 |
30 | override suspend fun load(params: LoadParams): LoadResult {
31 | val position = params.key ?: page
32 | return try {
33 | val response = apiService.getQuestionsForAll(
34 | position,
35 | pageSize,
36 | order,
37 | sortCondition,
38 | site,
39 | tagged,
40 | filter,
41 | siteKey
42 | )
43 | val responseItems: List = response.items.map { it.toQuestion() }
44 | val nextKey = if (responseItems.isEmpty()) {
45 | null
46 | } else {
47 | position + (params.loadSize / pageSize)
48 | }
49 | LoadResult.Page(
50 | data = responseItems,
51 | prevKey = if (position == page) null else position - 1,
52 | nextKey = nextKey
53 | )
54 | } catch (exception: IOException) {
55 | LoadResult.Error(exception)
56 | } catch (exception: HttpException) {
57 | LoadResult.Error(exception)
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/datasource/TagPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.datasource
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import com.josycom.mayorjay.flowoverstack.data.mapper.toTag
6 | import com.josycom.mayorjay.flowoverstack.data.model.Tag
7 | import com.josycom.mayorjay.flowoverstack.data.remote.service.ApiService
8 | import retrofit2.HttpException
9 | import java.io.IOException
10 |
11 | class TagPagingSource(private val page: Int, private val pageSize: Int, private val inName: String, private val siteKey: String, private val apiService: ApiService) : PagingSource() {
12 |
13 | override fun getRefreshKey(state: PagingState): Int? {
14 | return state.anchorPosition?.let { anchorPosition ->
15 | state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
16 | ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
17 | }
18 | }
19 |
20 | override suspend fun load(params: LoadParams): LoadResult {
21 | val position = params.key ?: page
22 | return try {
23 | val response = apiService.getAllPopularTags(position, pageSize, inName, siteKey)
24 | val responseItems: List = response.items.map { it.toTag() }
25 | val nextKey = if (responseItems.isEmpty()) {
26 | null
27 | } else {
28 | position + (params.loadSize / pageSize)
29 | }
30 | LoadResult.Page(
31 | data = responseItems,
32 | prevKey = if (position == page) null else position - 1,
33 | nextKey = nextKey
34 | )
35 | } catch (exception: IOException) {
36 | LoadResult.Error(exception)
37 | } catch (exception: HttpException) {
38 | LoadResult.Error(exception)
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/model/AnswerRemote.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class AnswerRemote(
6 | @SerializedName("owner")
7 | val owner: OwnerRemote? = null,
8 |
9 | @SerializedName("is_accepted")
10 | val isAccepted: Boolean? = null,
11 |
12 | @SerializedName("score")
13 | val score: Int? = null,
14 |
15 | @SerializedName("last_activity_date")
16 | val lastActivityDate: Int? = null,
17 |
18 | @SerializedName("last_edit_date")
19 | val lastEditDate: Int? = null,
20 |
21 | @SerializedName("creation_date")
22 | val creationDate: Int? = null,
23 |
24 | @SerializedName("answer_id")
25 | val answerId: Int? = null,
26 |
27 | @SerializedName("question_id")
28 | val questionId: Int? = null,
29 |
30 | @SerializedName("body")
31 | val body: String? = null
32 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/model/AnswerResponse.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class AnswerResponse(
6 | @SerializedName("has_more")
7 | val hasMore: Boolean? = null,
8 |
9 | @SerializedName("items")
10 | val items: List = emptyList(),
11 |
12 | @SerializedName("quota_max")
13 | val quotaMax: Int? = null,
14 |
15 | @SerializedName("quota_remaining")
16 | val quotaRemaining: Int? = null
17 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/model/OwnerRemote.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class OwnerRemote(
6 | @SerializedName("reputation")
7 | val reputation: Int? = null,
8 |
9 | @SerializedName("user_id")
10 | val userId: Int? = null,
11 |
12 | @SerializedName("user_type")
13 | val userType: String? = null,
14 |
15 | @SerializedName("accept_rate")
16 | val acceptRate: Int? = null,
17 |
18 | @SerializedName("profile_image")
19 | val profileImage: String? = null,
20 |
21 | @SerializedName("display_name")
22 | val displayName: String? = null,
23 |
24 | @SerializedName("link")
25 | val link: String? = null
26 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/model/QuestionRemote.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class QuestionRemote(
6 | @SerializedName("tags")
7 | val tags: List? = null,
8 |
9 | @SerializedName("body")
10 | val body: String? = null,
11 |
12 | @SerializedName("owner")
13 | val owner: OwnerRemote? = null,
14 |
15 | @SerializedName("is_answered")
16 | val isAnswered: Boolean? = null,
17 |
18 | @SerializedName("view_count")
19 | val viewCount: Int? = null,
20 |
21 | @SerializedName("accepted_answer_id")
22 | val acceptedAnswerId: Int? = null,
23 |
24 | @SerializedName("answer_count")
25 | val answerCount: Int? = null,
26 |
27 | @SerializedName("score")
28 | val score: Int? = null,
29 |
30 | @SerializedName("last_activity_date")
31 | val lastActivityDate: Int? = null,
32 |
33 | @SerializedName("creation_date")
34 | val creationDate: Int? = null,
35 |
36 | @SerializedName("last_edit_date")
37 | val lastEditDate: Int? = null,
38 |
39 | @SerializedName("question_id")
40 | val questionId: Int? = null,
41 |
42 | @SerializedName("link")
43 | val link: String? = null,
44 |
45 | @SerializedName("title")
46 | val title: String? = null
47 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/model/QuestionsResponse.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class QuestionsResponse(
6 | @SerializedName("has_more")
7 | val hasMore: Boolean? = null,
8 |
9 | @SerializedName("items")
10 | val items: List = emptyList(),
11 |
12 | @SerializedName("quota_max")
13 | val quotaMax: Int? = null,
14 |
15 | @SerializedName("quota_remaining")
16 | val quotaRemaining: Int? = null
17 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/model/SearchResponse.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.model
2 |
3 | import com.josycom.mayorjay.flowoverstack.data.model.Question
4 |
5 | data class SearchResponse(
6 | val networkState: String = "",
7 | val questions: List? = null
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/model/TagRemote.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class TagRemote(
6 | @SerializedName("name")
7 | val name: String? = null,
8 |
9 | @SerializedName("count")
10 | val count: Long? = null,
11 |
12 | @SerializedName("has_synonyms")
13 | val hasSynonyms: Boolean? = null,
14 |
15 | @SerializedName("is_moderator_only")
16 | val isModeratorOnly: Boolean? = null,
17 |
18 | @SerializedName("is_required")
19 | val isRequired: Boolean? = null
20 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/model/TagsResponse.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class TagsResponse(
6 | @SerializedName("items")
7 | val items: List = emptyList(),
8 |
9 | @SerializedName("has_more")
10 | val hasMore: Boolean? = null,
11 |
12 | @SerializedName("quota_max")
13 | val quotaMax: Int? = null,
14 |
15 | @SerializedName("quota_remaining")
16 | val quotaRemaining: Int? = null
17 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/remote/service/ApiService.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.remote.service
2 |
3 | import com.josycom.mayorjay.flowoverstack.data.remote.model.AnswerResponse
4 | import com.josycom.mayorjay.flowoverstack.data.remote.model.QuestionsResponse
5 | import com.josycom.mayorjay.flowoverstack.data.remote.model.TagsResponse
6 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
7 | import retrofit2.Call
8 | import retrofit2.http.GET
9 | import retrofit2.http.Path
10 | import retrofit2.http.Query
11 |
12 | interface ApiService {
13 |
14 | @GET(AppConstants.QUESTIONS_END_POINT)
15 | suspend fun getQuestionsForAll(
16 | @Query("page") page: Int,
17 | @Query("pagesize") pageSize: Int,
18 | @Query("order") order: String?,
19 | @Query("sort") sortCondition: String?,
20 | @Query("site") site: String?,
21 | @Query("tagged") tagged: String?,
22 | @Query(value = "filter", encoded = true) filter: String?,
23 | @Query("key", encoded = true) siteKey: String?): QuestionsResponse
24 |
25 | @GET(AppConstants.ANSWERS_END_POINT)
26 | suspend fun getAnswersToQuestion(
27 | @Path("question_id") id: Int,
28 | @Query("order") order: String?,
29 | @Query("sort") sortCondition: String?,
30 | @Query("site") site: String?,
31 | @Query(value = "filter", encoded = true) filter: String?,
32 | @Query("key") siteKey: String?): AnswerResponse
33 |
34 | @GET(AppConstants.SEARCH_END_POINT)
35 | fun getQuestionsWithTextInTitle(
36 | @Query("intitle") inTitle: String?,
37 | @Query("page") page: Int,
38 | @Query("pagesize") pageSize: Int,
39 | @Query("key") siteKey: String? = AppConstants.API_KEY
40 | ): Call
41 |
42 | @GET(AppConstants.TAGS_END_POINT)
43 | suspend fun getAllPopularTags(
44 | @Query("page") page: Int,
45 | @Query("pagesize") pageSize: Int,
46 | @Query("inname") inName: String?,
47 | @Query("key") siteKey: String?): TagsResponse
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/repository/AnswerRepository.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.repository
2 |
3 | import androidx.paging.Pager
4 | import androidx.paging.PagingConfig
5 | import androidx.paging.PagingData
6 | import com.josycom.mayorjay.flowoverstack.data.model.Answer
7 | import com.josycom.mayorjay.flowoverstack.data.remote.datasource.AnswerPagingSource
8 | import com.josycom.mayorjay.flowoverstack.data.remote.service.ApiService
9 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
10 | import kotlinx.coroutines.flow.Flow
11 | import javax.inject.Inject
12 | import javax.inject.Singleton
13 |
14 | @Singleton
15 | class AnswerRepository @Inject constructor(private val apiService: ApiService) {
16 |
17 | var answerDataFlow: Flow>? = null
18 |
19 | fun init(questionId: Int, order: String, sortCondition: String, site: String, filter: String, siteKey: String) {
20 | answerDataFlow = Pager(PagingConfig(AppConstants.PAGE_SIZE, enablePlaceholders = false),
21 | pagingSourceFactory = {
22 | AnswerPagingSource(
23 | questionId,
24 | order,
25 | sortCondition,
26 | site,
27 | filter,
28 | siteKey,
29 | apiService
30 | )
31 | }).flow
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/repository/PreferenceRepository.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.repository
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface PreferenceRepository {
6 |
7 | fun getIntPreferenceFlow(key: String): Flow
8 | suspend fun setIntPreference(key: String, value: Int)
9 | suspend fun deleteAllPreferences()
10 | suspend fun contains(key: String): Boolean
11 | suspend fun isEmpty(): Boolean
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/repository/PreferenceRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.repository
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import androidx.datastore.preferences.core.edit
6 | import androidx.datastore.preferences.core.emptyPreferences
7 | import androidx.datastore.preferences.core.intPreferencesKey
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.catch
10 | import kotlinx.coroutines.flow.map
11 | import timber.log.Timber
12 | import javax.inject.Inject
13 |
14 | class PreferenceRepositoryImpl @Inject constructor(private val dataStore: DataStore) :
15 | PreferenceRepository {
16 |
17 | override fun getIntPreferenceFlow(key: String): Flow {
18 | val prefKey: Preferences.Key = intPreferencesKey(key)
19 | return dataStore.data
20 | .catch { exception ->
21 | Timber.e(exception)
22 | emit(emptyPreferences())
23 | }
24 | .map { preferences ->
25 | preferences[prefKey] ?: 0
26 | }
27 | }
28 |
29 | override suspend fun setIntPreference(
30 | key: String,
31 | value: Int
32 | ) {
33 | val prefKey: Preferences.Key = intPreferencesKey(key)
34 | dataStore.edit { preferences ->
35 | preferences[prefKey] = value
36 | }
37 | }
38 |
39 | override suspend fun deleteAllPreferences() {
40 | dataStore.edit { preferences ->
41 | preferences.clear()
42 | }
43 | }
44 |
45 | override suspend fun contains(key: String): Boolean {
46 | return dataStore.edit { }.contains(intPreferencesKey(key))
47 | }
48 |
49 | override suspend fun isEmpty(): Boolean {
50 | return dataStore.edit { }.asMap().isEmpty()
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/repository/QuestionRepository.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.repository
2 |
3 | import androidx.paging.Pager
4 | import androidx.paging.PagingConfig
5 | import androidx.paging.PagingData
6 | import com.josycom.mayorjay.flowoverstack.data.model.Question
7 | import com.josycom.mayorjay.flowoverstack.data.remote.datasource.QuestionPagingSource
8 | import com.josycom.mayorjay.flowoverstack.data.remote.service.ApiService
9 | import kotlinx.coroutines.flow.Flow
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | class QuestionRepository @Inject constructor(private val apiService: ApiService) {
15 |
16 | var questionDataFlow: Flow>? = null
17 |
18 | fun init(page: Int, pageSize: Int, order: String, sortCondition: String, site: String, tagged: String, filter: String, siteKey: String) {
19 | questionDataFlow = Pager(PagingConfig(pageSize, enablePlaceholders = false),
20 | pagingSourceFactory = { QuestionPagingSource(page, pageSize, order, sortCondition, site, tagged, filter, siteKey, apiService) }).flow
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/repository/SearchRepository.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.repository
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import com.josycom.mayorjay.flowoverstack.data.mapper.toQuestion
5 | import com.josycom.mayorjay.flowoverstack.data.remote.model.QuestionsResponse
6 | import com.josycom.mayorjay.flowoverstack.data.remote.model.SearchResponse
7 | import com.josycom.mayorjay.flowoverstack.data.remote.service.ApiService
8 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
9 | import com.josycom.mayorjay.flowoverstack.util.ThreadExecutor
10 | import retrofit2.Call
11 | import retrofit2.Callback
12 | import retrofit2.Response
13 | import timber.log.Timber
14 | import javax.inject.Inject
15 | import javax.inject.Singleton
16 |
17 | @Singleton
18 | class SearchRepository @Inject constructor(private val apiService: ApiService) {
19 |
20 | val searchResponse = MutableLiveData()
21 |
22 | private fun getQuestionsWithTextInTitle(inTitle: String?, page: Int, pageSize: Int) {
23 | searchResponse.postValue(SearchResponse(AppConstants.LOADING, null))
24 | val call = apiService.getQuestionsWithTextInTitle(inTitle, page, pageSize)
25 | call.enqueue(object : Callback {
26 | override fun onResponse(call: Call, response: Response) {
27 | val questionsResponse = response.body()
28 | if (questionsResponse != null) {
29 | if (questionsResponse.items.isNotEmpty()) {
30 | searchResponse.setValue(
31 | SearchResponse(
32 | AppConstants.LOADED,
33 | questionsResponse.items.map { it.toQuestion() })
34 | )
35 | } else {
36 | searchResponse.setValue(SearchResponse(AppConstants.NO_MATCHING_RESULT, null))
37 | }
38 | }
39 | }
40 |
41 | override fun onFailure(call: Call, t: Throwable) {
42 | Timber.e(t)
43 | searchResponse.value = SearchResponse(AppConstants.FAILED, null)
44 | }
45 | })
46 | }
47 |
48 | fun performSearch(inTitle: String?, page: Int, pageSize: Int) {
49 | ThreadExecutor.mExecutor.execute { getQuestionsWithTextInTitle(inTitle, page, pageSize) }
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/data/repository/TagRepository.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.data.repository
2 |
3 | import androidx.paging.Pager
4 | import androidx.paging.PagingConfig
5 | import androidx.paging.PagingData
6 | import com.josycom.mayorjay.flowoverstack.data.model.Tag
7 | import com.josycom.mayorjay.flowoverstack.data.remote.datasource.TagPagingSource
8 | import com.josycom.mayorjay.flowoverstack.data.remote.service.ApiService
9 | import kotlinx.coroutines.flow.Flow
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | class TagRepository @Inject constructor(private val apiService: ApiService) {
15 |
16 | var tagDataFlow: Flow>? = null
17 |
18 | fun init(page: Int, pageSize: Int, inName: String, siteKey: String) {
19 | tagDataFlow = Pager(PagingConfig(pageSize, enablePlaceholders = false), pagingSourceFactory = { TagPagingSource(page, pageSize, inName, siteKey, apiService) }).flow
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/di/ApiModule.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.di
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.GsonBuilder
5 | import com.josycom.mayorjay.flowoverstack.BuildConfig
6 | import com.josycom.mayorjay.flowoverstack.data.remote.service.ApiService
7 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.hilt.InstallIn
11 | import dagger.hilt.components.SingletonComponent
12 | import okhttp3.OkHttpClient
13 | import okhttp3.logging.HttpLoggingInterceptor
14 | import retrofit2.Retrofit
15 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
16 | import retrofit2.converter.gson.GsonConverterFactory
17 | import javax.inject.Singleton
18 |
19 | @InstallIn(SingletonComponent::class)
20 | @Module
21 | object ApiModule {
22 |
23 | @Provides
24 | @Singleton
25 | fun provideGson(): Gson = GsonBuilder().setLenient().create()
26 |
27 | @Provides
28 | @Singleton
29 | fun provideInterceptor(): HttpLoggingInterceptor =
30 | HttpLoggingInterceptor()
31 | .setLevel(if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE)
32 |
33 | @Provides
34 | @Singleton
35 | fun getOkHttpClient(interceptor: HttpLoggingInterceptor): OkHttpClient.Builder =
36 | OkHttpClient.Builder().addInterceptor(interceptor)
37 |
38 | @Provides
39 | @Singleton
40 | fun provideRetrofit(gson: Gson, okHttpClient: OkHttpClient.Builder): Retrofit =
41 | Retrofit.Builder()
42 | .baseUrl(AppConstants.BASE_URL)
43 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
44 | .addConverterFactory(GsonConverterFactory.create(gson))
45 | .client(okHttpClient.build())
46 | .build()
47 |
48 | @Provides
49 | @Singleton
50 | fun getApiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java)
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/di/DataStoreModule.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.di
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory
6 | import androidx.datastore.preferences.core.Preferences
7 | import androidx.datastore.preferences.preferencesDataStoreFile
8 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
9 | import dagger.Module
10 | import dagger.Provides
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.android.qualifiers.ApplicationContext
13 | import dagger.hilt.components.SingletonComponent
14 | import javax.inject.Singleton
15 |
16 | @InstallIn(SingletonComponent::class)
17 | @Module
18 | object DataStoreModule {
19 |
20 | @Provides
21 | @Singleton
22 | fun provideDataStore(@ApplicationContext context: Context): DataStore =
23 | PreferenceDataStoreFactory.create(
24 | produceFile = {
25 | context.preferencesDataStoreFile(AppConstants.PREFERENCES_FILE_NAME)
26 | }
27 | )
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.di
2 |
3 | import com.josycom.mayorjay.flowoverstack.data.repository.PreferenceRepository
4 | import com.josycom.mayorjay.flowoverstack.data.repository.PreferenceRepositoryImpl
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 | @InstallIn(SingletonComponent::class)
11 | @Module
12 | abstract class RepositoryModule {
13 |
14 | @Binds
15 | abstract fun bindRepository(repositoryImpl: PreferenceRepositoryImpl): PreferenceRepository
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/util/AppConstants.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.util
2 |
3 | object AppConstants {
4 | const val BASE_URL = "https://api.stackexchange.com"
5 | const val QUESTIONS_END_POINT = "/2.3/questions"
6 | const val ANSWERS_END_POINT = "/2.3/questions/{question_id}/answers"
7 | const val SEARCH_END_POINT =
8 | "/2.3/search?order=desc&sort=activity&site=stackoverflow&filter=!9Z(-wwYGT"
9 | const val TAGS_END_POINT = "/2.3/tags?order=desc&sort=popular&site=stackoverflow"
10 | const val PLAY_STORE_URL =
11 | "https://play.google.com/store/apps/details?id=com.josycom.mayorjay.flowoverstack"
12 | const val TWITTER_URL = "https://twitter.com/mayorjay1"
13 | const val EMAIL_ADDRESS = "joseolu4gsm@yahoo.com"
14 | const val SITE = "stackoverflow"
15 | const val FIRST_PAGE = 1
16 | const val PAGE_SIZE = 50
17 | const val SEARCH_PAGE_SIZE = 100
18 | const val SPLASH_TIME = 1000L
19 | const val ORDER_DESCENDING = "desc"
20 | const val QUESTION_FILTER = "!9Z(-wwYGT"
21 | const val ANSWER_FILTER = "!9Z(-wzu0T"
22 | const val API_KEY = "YOUR_API_KEY"
23 | const val SORT_BY_ACTIVITY = "activity"
24 | const val SORT_BY_VOTES = "votes"
25 | const val SORT_BY_CREATION = "creation"
26 | const val SORT_BY_HOT = "hot"
27 | const val EXTRA_QUESTION_KEY = "key.QUESTION_EXTRA"
28 | const val WEBVIEW_EXTRA_OBJECT = "key.EXTRA_OBJC"
29 | const val LOADING = "loading"
30 | const val LOADED = "loaded"
31 | const val FAILED = "failed"
32 | const val NO_MATCHING_RESULT = "No matching result"
33 | const val FRAGMENT_STATE = "fragment_state"
34 | const val PREFERENCES_FILE_NAME = "fos_pref"
35 | const val APP_OPEN_COUNT_PREF_KEY = "key.APP_OPEN_COUNT"
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/util/AppLogger.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.util
2 |
3 | import android.util.Log
4 | import com.google.firebase.crashlytics.FirebaseCrashlytics
5 | import com.josycom.mayorjay.flowoverstack.BuildConfig
6 | import timber.log.Timber
7 |
8 | object AppLogger {
9 |
10 | fun init() {
11 | Timber.plant(if (BuildConfig.DEBUG) Timber.DebugTree() else ReleaseTree())
12 | }
13 |
14 | class ReleaseTree : Timber.Tree() {
15 |
16 | companion object {
17 | const val PRIORITY = "Priority"
18 | const val TAG = "Tag"
19 | }
20 |
21 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
22 | if (priority == Log.VERBOSE || priority == Log.DEBUG || priority == Log.INFO) {
23 | return
24 | }
25 |
26 | FirebaseCrashlytics.getInstance().apply {
27 | this.setCustomKey(PRIORITY, priority)
28 | tag?.let { this.setCustomKey(TAG, it) }
29 | this.log(message)
30 | t?.let { this.recordException(it) }
31 | }.sendUnsentReports()
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/util/AppUtils.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.util
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.widget.Toast
7 | import androidx.appcompat.app.AppCompatActivity
8 | import com.josycom.mayorjay.flowoverstack.R
9 | import com.josycom.mayorjay.flowoverstack.view.answer.WebViewActivity
10 | import timber.log.Timber
11 | import java.text.SimpleDateFormat
12 | import java.util.Date
13 | import java.util.Locale
14 |
15 | /**
16 | * FlowOverStack
17 | * Created by Mbuodile Obiosio on Jun 05, 2020
18 | * https://twitter.com/cazewonder
19 | * Modified by MayorJay
20 | */
21 | object AppUtils {
22 |
23 | private const val OPEN_IN_APP_BROWSER = true
24 |
25 | fun directLinkToBrowser(activity: AppCompatActivity, url: String?) {
26 | try {
27 | if (OPEN_IN_APP_BROWSER) {
28 | WebViewActivity.navigate(activity, url)
29 | } else {
30 | launchViewIntent(activity, url)
31 | }
32 | } catch (e: Exception) {
33 | Timber.e(e)
34 | showToast(activity, activity.getString(R.string.cannot_open_url))
35 | }
36 | }
37 |
38 | fun showToast(context: Context, text: String) {
39 | Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
40 | }
41 |
42 | fun toNormalDate(seconds: Long): String? {
43 | val date = Date(seconds * 1000L)
44 | val simpleDateFormat = SimpleDateFormat("MMM dd yyyy", Locale.getDefault())
45 | return simpleDateFormat.format(date)
46 | }
47 |
48 | fun getFormattedTags(tagList: List): String = tagList.joinToString(", ")
49 |
50 | fun shareContent(content: String, context: Context) {
51 | val intent = Intent(Intent.ACTION_SEND)
52 | intent.type = "text/plain"
53 | intent.putExtra(Intent.EXTRA_TEXT, content)
54 | context.startActivity(Intent.createChooser(intent, "Share Via"))
55 | }
56 |
57 | fun launchViewIntent(context: Context, url: String?) {
58 | context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
59 | }
60 |
61 | fun launchEmailIntent(context: Context, address: String) {
62 | val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:$address"))
63 | context.startActivity(Intent.createChooser(intent, "Send With"))
64 | }
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/util/ThreadExecutor.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.util
2 |
3 | import java.util.concurrent.ExecutorService
4 | import java.util.concurrent.Executors
5 |
6 | object ThreadExecutor {
7 | var mExecutor: ExecutorService = Executors.newFixedThreadPool(5)
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/view/answer/AnswerActivity.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.view.answer
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.MenuItem
6 | import android.view.View
7 | import androidx.activity.viewModels
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.core.view.isInvisible
10 | import androidx.core.view.isVisible
11 | import androidx.lifecycle.Lifecycle
12 | import androidx.lifecycle.lifecycleScope
13 | import androidx.lifecycle.repeatOnLifecycle
14 | import androidx.paging.LoadState
15 | import androidx.recyclerview.widget.DefaultItemAnimator
16 | import androidx.recyclerview.widget.LinearLayoutManager
17 | import com.bumptech.glide.Glide
18 | import com.josycom.mayorjay.flowoverstack.R
19 | import com.josycom.mayorjay.flowoverstack.data.model.Answer
20 | import com.josycom.mayorjay.flowoverstack.data.model.Question
21 | import com.josycom.mayorjay.flowoverstack.databinding.ActivityAnswerBinding
22 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
23 | import com.josycom.mayorjay.flowoverstack.util.AppUtils
24 | import com.josycom.mayorjay.flowoverstack.view.home.PagingLoadStateAdapter
25 | import com.josycom.mayorjay.flowoverstack.viewmodel.AnswerViewModel
26 | import dagger.hilt.android.AndroidEntryPoint
27 | import kotlinx.coroutines.flow.collectLatest
28 | import kotlinx.coroutines.launch
29 | import org.jsoup.Jsoup
30 |
31 | @AndroidEntryPoint
32 | class AnswerActivity : AppCompatActivity() {
33 |
34 | private lateinit var binding: ActivityAnswerBinding
35 | private val answerViewModel: AnswerViewModel by viewModels()
36 | private var ownerQuestionLink: String? = null
37 | private var questionId = 0
38 | private var questionLink: String? = null
39 |
40 | override fun onCreate(savedInstanceState: Bundle?) {
41 | super.onCreate(savedInstanceState)
42 | binding = ActivityAnswerBinding.inflate(layoutInflater)
43 | setContentView(binding.root)
44 |
45 | setupViewContents(intent)
46 | answerViewModel.init(questionId,
47 | AppConstants.ORDER_DESCENDING,
48 | AppConstants.SORT_BY_ACTIVITY,
49 | AppConstants.SITE,
50 | AppConstants.ANSWER_FILTER,
51 | AppConstants.API_KEY)
52 |
53 | fetchAndDisplayAnswers()
54 | setupListeners()
55 | }
56 |
57 | private fun setupViewContents(intent: Intent?) {
58 | var avatarAddress: String? = ""
59 | intent?.let {
60 | val question = it.getSerializableExtra(AppConstants.EXTRA_QUESTION_KEY) as Question
61 | binding.apply {
62 | tvQuestionDetail.text = Jsoup.parse(question.title).text()
63 | markDownView.setMarkDownText(question.body)
64 | tvDateQuestionDetail.text = AppUtils.toNormalDate(question.creationDate.toLong())
65 | tvNameQuestionDetail.text = question.owner.displayName
66 | }
67 | val voteCount = question.score
68 | if (voteCount <= 0) {
69 | binding.tvVotesCountItem.text = voteCount.toString()
70 | } else {
71 | binding.tvVotesCountItem.text = getString(R.string.plus_score, voteCount)
72 | }
73 | questionId = question.questionId
74 | avatarAddress = question.owner.profileImage
75 | ownerQuestionLink = question.owner.link
76 | questionLink = question.link
77 | }
78 | Glide.with(this)
79 | .load(avatarAddress)
80 | .placeholder(R.drawable.loading)
81 | .into(binding.ivAvatarQuestionDetail)
82 | }
83 |
84 | private fun setupListeners() {
85 | binding.apply {
86 | avatarCard.setOnClickListener {
87 | AppUtils.directLinkToBrowser(this@AnswerActivity, ownerQuestionLink)
88 | }
89 | tvNameQuestionDetail.setOnClickListener {
90 | AppUtils.directLinkToBrowser(this@AnswerActivity, ownerQuestionLink)
91 | }
92 | ivShare.setOnClickListener {
93 | val content = getString(
94 | R.string.share_content,
95 | getString(R.string.question),
96 | questionLink ?: "",
97 | AppConstants.PLAY_STORE_URL
98 | )
99 | AppUtils.shareContent(content, this@AnswerActivity)
100 | }
101 | }
102 | }
103 |
104 | private fun fetchAndDisplayAnswers() {
105 | val answerAdapter = AnswerAdapter()
106 | binding.rvAnswers.apply {
107 | layoutManager = LinearLayoutManager(this@AnswerActivity)
108 | itemAnimator = DefaultItemAnimator()
109 | adapter = answerAdapter.withLoadStateFooter(PagingLoadStateAdapter { answerAdapter.retry() })
110 | }
111 |
112 | lifecycleScope.launch {
113 | answerAdapter.loadStateFlow.collect {
114 | binding.pbFetchData.isVisible = it.source.refresh is LoadState.Loading
115 | binding.tvError.isVisible = it.source.refresh is LoadState.Error
116 | binding.btRetry.isVisible = it.source.refresh is LoadState.Error
117 | if (it.source.refresh is LoadState.NotLoading && answerAdapter.itemCount <= 0) {
118 | binding.tvNoAnswerQuestionDetail.isVisible = true
119 | } else {
120 | binding.tvNoAnswerQuestionDetail.isInvisible = true
121 | }
122 | }
123 | }
124 |
125 | lifecycleScope.launch {
126 | repeatOnLifecycle(Lifecycle.State.STARTED) {
127 | answerViewModel.answerDataFlow?.collectLatest {
128 | answerAdapter.submitData(it)
129 | }
130 | }
131 | }
132 | binding.btRetry.setOnClickListener { answerAdapter.retry() }
133 | answerAdapter.setOnClickListener(shareClickListener)
134 | }
135 |
136 | private val shareClickListener = View.OnClickListener { v ->
137 | val currentAnswer = v.tag as? Answer
138 | currentAnswer?.let {
139 | val content = getString(
140 | R.string.share_content,
141 | getString(R.string.answer),
142 | "https://stackoverflow.com/a/${it.answerId}",
143 | AppConstants.PLAY_STORE_URL
144 | )
145 | AppUtils.shareContent(content, this)
146 | }
147 | }
148 |
149 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
150 | if (item.itemId == android.R.id.home) {
151 | onBackPressed()
152 | return true
153 | }
154 | return super.onOptionsItemSelected(item)
155 | }
156 |
157 | override fun onBackPressed() {
158 | if (binding.markDownView.canGoBack()) {
159 | binding.markDownView.goBack()
160 | } else {
161 | super.onBackPressed()
162 | finish()
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/view/answer/AnswerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.view.answer
2 |
3 | import android.annotation.SuppressLint
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.paging.PagingDataAdapter
8 | import androidx.recyclerview.widget.DiffUtil
9 | import androidx.recyclerview.widget.RecyclerView
10 | import com.josycom.mayorjay.flowoverstack.data.model.Answer
11 | import com.josycom.mayorjay.flowoverstack.databinding.AnswerItemBinding
12 | import com.josycom.mayorjay.flowoverstack.util.AppUtils
13 | import com.josycom.mayorjay.flowoverstack.view.answer.AnswerAdapter.AnswerViewHolder
14 | import org.jsoup.Jsoup
15 |
16 | class AnswerAdapter : PagingDataAdapter(DIFF_CALLBACK) {
17 |
18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnswerViewHolder {
19 | val answerItemBinding = AnswerItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
20 | return AnswerViewHolder(answerItemBinding)
21 | }
22 |
23 | override fun onBindViewHolder(holder: AnswerViewHolder, position: Int) {
24 | getItem(position)?.let {
25 | holder.bind(it)
26 | }
27 | }
28 |
29 | fun setOnClickListener(shareOnClickListener: View.OnClickListener?) {
30 | shareClickListener = shareOnClickListener
31 | }
32 |
33 | companion object {
34 | private var shareClickListener: View.OnClickListener? = null
35 | private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() {
36 | override fun areItemsTheSame(oldItem: Answer, newItem: Answer): Boolean = oldItem.answerId == newItem.answerId
37 |
38 | @SuppressLint("DiffUtilEquals")
39 | override fun areContentsTheSame(oldItem: Answer, newItem: Answer): Boolean = oldItem == newItem
40 | }
41 | }
42 |
43 | class AnswerViewHolder(private val binding: AnswerItemBinding) : RecyclerView.ViewHolder(binding.root) {
44 |
45 | init {
46 | binding.ivShare.setOnClickListener(shareClickListener)
47 | }
48 |
49 | fun bind(answer: Answer) {
50 | binding.ivShare.tag = answer
51 | binding.tvVotesItem.text = answer.score.toString()
52 | binding.tvAnswerNameItem.text = answer.owner.displayName
53 | binding.tvAnswerDateItem.text = AppUtils.toNormalDate(answer.creationDate.toLong())
54 | binding.tvAnswerBodyItem.text = Jsoup.parse(answer.body).text()
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/view/home/PagingLoadStateAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.view.home
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.core.view.isVisible
6 | import androidx.paging.LoadState
7 | import androidx.paging.LoadStateAdapter
8 | import androidx.recyclerview.widget.RecyclerView
9 | import com.josycom.mayorjay.flowoverstack.databinding.PagingLoadStateFooterViewItemBinding
10 |
11 | class PagingLoadStateAdapter(private val retry: () -> Unit) : LoadStateAdapter() {
12 |
13 | override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): PagingLoadStateViewHolder {
14 | val view = PagingLoadStateFooterViewItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
15 | return PagingLoadStateViewHolder(view, retry)
16 | }
17 |
18 | override fun onBindViewHolder(holder: PagingLoadStateViewHolder, loadState: LoadState) {
19 | holder.bind(loadState)
20 | }
21 |
22 | class PagingLoadStateViewHolder(private val binding: PagingLoadStateFooterViewItemBinding, private val retry: () -> Unit) : RecyclerView.ViewHolder(binding.root) {
23 |
24 | init {
25 | binding.btRetry.setOnClickListener { retry.invoke() }
26 | }
27 |
28 | fun bind(loadState: LoadState) {
29 | binding.pbFooter.isVisible = loadState is LoadState.Loading
30 | binding.btRetry.isVisible = loadState is LoadState.Error
31 | binding.tvErrorMsg.isVisible = loadState is LoadState.Error
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/view/home/QuestionAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.view.home
2 |
3 | import android.annotation.SuppressLint
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.core.view.isVisible
8 | import androidx.paging.PagingDataAdapter
9 | import androidx.recyclerview.widget.DiffUtil
10 | import androidx.recyclerview.widget.RecyclerView
11 | import com.bumptech.glide.Glide
12 | import com.josycom.mayorjay.flowoverstack.R
13 | import com.josycom.mayorjay.flowoverstack.data.model.Question
14 | import com.josycom.mayorjay.flowoverstack.databinding.QuestionItemBinding
15 | import com.josycom.mayorjay.flowoverstack.util.AppUtils
16 | import com.josycom.mayorjay.flowoverstack.view.home.QuestionAdapter.QuestionViewHolder
17 | import org.jsoup.Jsoup
18 |
19 | class QuestionAdapter : PagingDataAdapter(DIFF_CALLBACK) {
20 |
21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionViewHolder {
22 | val questionItemBinding = QuestionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
23 | return QuestionViewHolder(questionItemBinding)
24 | }
25 |
26 | override fun onBindViewHolder(holder: QuestionViewHolder, position: Int) {
27 | getItem(position)?.let {
28 | holder.bind(it)
29 | }
30 | }
31 |
32 | fun setOnClickListeners(onClickListener: View.OnClickListener?, shareOnClickListener: View.OnClickListener?) {
33 | viewHolderClickListener = onClickListener
34 | shareClickListener = shareOnClickListener
35 | }
36 |
37 | companion object {
38 | private var viewHolderClickListener: View.OnClickListener? = null
39 | private var shareClickListener: View.OnClickListener? = null
40 | private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() {
41 | override fun areItemsTheSame(oldItem: Question, newItem: Question): Boolean = oldItem.questionId == newItem.questionId
42 |
43 | @SuppressLint("DiffUtilEquals")
44 | override fun areContentsTheSame(oldItem: Question, newItem: Question): Boolean = oldItem == newItem
45 | }
46 | }
47 |
48 | class QuestionViewHolder(private val binding: QuestionItemBinding) : RecyclerView.ViewHolder(binding.root) {
49 |
50 | init {
51 | binding.root.setOnClickListener(viewHolderClickListener)
52 | binding.ivShare.setOnClickListener(shareClickListener)
53 | }
54 |
55 | fun bind(question: Question) {
56 | binding.root.tag = question
57 | binding.ivShare.tag = question
58 | Glide.with(binding.root.context)
59 | .load(question.owner.profileImage)
60 | .placeholder(R.drawable.loading)
61 | .into(binding.ivAvatarItem)
62 | binding.tvQuestionItem.text = Jsoup.parse(question.title).text()
63 | binding.tvViewsCountItem.text = question.viewCount.toString()
64 | binding.tvDateItem.text = AppUtils.toNormalDate(question.creationDate.toLong())
65 | binding.answered.isVisible = question.isAnswered
66 | val answers = question.answerCount
67 | val answerCount = binding.root.context.resources.getQuantityString(
68 | R.plurals.answers,
69 | answers,
70 | answers
71 | )
72 | binding.tvAnswersCountItem.text = answerCount
73 | if (question.score <= 0) {
74 | binding.tvVotesCountItem.text = question.score.toString()
75 | } else {
76 | binding.tvVotesCountItem.text =
77 | binding.root.context.getString(R.string.plus_score, question.score)
78 | }
79 | binding.tvTagsListItem.text = AppUtils.getFormattedTags(question.tags)
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/view/home/QuestionsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.view.home
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.core.view.isInvisible
10 | import androidx.core.view.isVisible
11 | import androidx.fragment.app.Fragment
12 | import androidx.fragment.app.viewModels
13 | import androidx.lifecycle.Lifecycle
14 | import androidx.lifecycle.lifecycleScope
15 | import androidx.lifecycle.repeatOnLifecycle
16 | import androidx.paging.LoadState
17 | import androidx.recyclerview.widget.DefaultItemAnimator
18 | import androidx.recyclerview.widget.LinearLayoutManager
19 | import androidx.recyclerview.widget.RecyclerView
20 | import com.josycom.mayorjay.flowoverstack.R
21 | import com.josycom.mayorjay.flowoverstack.data.model.Question
22 | import com.josycom.mayorjay.flowoverstack.databinding.FragmentQuestionsBinding
23 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
24 | import com.josycom.mayorjay.flowoverstack.util.AppUtils
25 | import com.josycom.mayorjay.flowoverstack.view.answer.AnswerActivity
26 | import com.josycom.mayorjay.flowoverstack.viewmodel.QuestionViewModel
27 | import dagger.hilt.android.AndroidEntryPoint
28 | import kotlinx.coroutines.flow.collectLatest
29 | import kotlinx.coroutines.launch
30 |
31 | /**
32 | * This [Fragment] houses all Questions
33 | */
34 | @AndroidEntryPoint
35 | class QuestionsFragment : Fragment() {
36 |
37 | private lateinit var binding: FragmentQuestionsBinding
38 | private val viewModel: QuestionViewModel by viewModels()
39 |
40 | override fun onCreateView(
41 | inflater: LayoutInflater,
42 | container: ViewGroup?,
43 | savedInstanceState: Bundle?
44 | ): View {
45 | binding = FragmentQuestionsBinding.inflate(layoutInflater)
46 | return binding.root
47 | }
48 |
49 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
50 | super.onViewCreated(view, savedInstanceState)
51 |
52 | viewModel.init(
53 | AppConstants.FIRST_PAGE,
54 | AppConstants.PAGE_SIZE,
55 | AppConstants.ORDER_DESCENDING,
56 | sortCondition,
57 | AppConstants.SITE,
58 | tagName,
59 | AppConstants.QUESTION_FILTER,
60 | AppConstants.API_KEY
61 | )
62 |
63 | initViews()
64 | fetchAndDisplayQuestions()
65 | }
66 |
67 | private fun initViews() {
68 | (activity as AppCompatActivity).supportActionBar?.title = title
69 | binding.activityScrollUpFab.isInvisible = true
70 | binding.activityRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
71 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
72 | super.onScrolled(recyclerView, dx, dy)
73 | if (dy > 0) {
74 | binding.activityScrollUpFab.isVisible = true
75 | } else {
76 | binding.activityScrollUpFab.isInvisible = true
77 | }
78 | }
79 | })
80 | binding.activityScrollUpFab.setOnClickListener {
81 | binding.activityRecyclerView.scrollToPosition(0)
82 | }
83 | }
84 |
85 | private fun fetchAndDisplayQuestions() {
86 | val questionAdapter = QuestionAdapter()
87 | binding.activityRecyclerView.apply {
88 | layoutManager = LinearLayoutManager(context)
89 | itemAnimator = DefaultItemAnimator()
90 | adapter = questionAdapter.withLoadStateFooter(PagingLoadStateAdapter { questionAdapter.retry() })
91 | }
92 |
93 | viewLifecycleOwner.lifecycleScope.launch {
94 | questionAdapter.loadStateFlow.collect {
95 | if (it.source.refresh is LoadState.Loading) onLoading()
96 | if (it.source.refresh is LoadState.Error) onError()
97 | if (it.source.refresh !is LoadState.Loading && it.source.refresh !is LoadState.Error) onLoaded()
98 | }
99 | }
100 |
101 | viewLifecycleOwner.lifecycleScope.launch {
102 | repeatOnLifecycle(Lifecycle.State.STARTED) {
103 | viewModel.questionDataFlow?.collectLatest {
104 | questionAdapter.submitData(it)
105 | }
106 | }
107 | }
108 |
109 | questionAdapter.setOnClickListeners(viewHolderClickListener, shareClickListener)
110 | binding.btRetry.setOnClickListener { questionAdapter.retry() }
111 | }
112 |
113 | private val viewHolderClickListener = View.OnClickListener { v ->
114 | Intent(requireContext(), AnswerActivity::class.java).apply {
115 | val currentQuestion = v?.tag as? Question
116 | currentQuestion?.let { putExtra(AppConstants.EXTRA_QUESTION_KEY, it) }
117 | startActivity(this)
118 | }
119 | }
120 |
121 | private val shareClickListener = View.OnClickListener { v ->
122 | val currentQuestion = v.tag as? Question
123 | currentQuestion?.let {
124 | val content = getString(
125 | R.string.share_content,
126 | getString(R.string.question),
127 | it.link,
128 | AppConstants.PLAY_STORE_URL
129 | )
130 | AppUtils.shareContent(content, requireContext())
131 | }
132 | }
133 |
134 | private fun onLoaded() = binding.apply {
135 | activityPbFetchData.isInvisible = true
136 | activityRecyclerView.isVisible = true
137 | activityTvError.isInvisible = true
138 | btRetry.isInvisible = true
139 | }
140 |
141 | private fun onError() = binding.apply {
142 | activityPbFetchData.isInvisible = true
143 | activityRecyclerView.isInvisible = true
144 | activityTvError.isVisible = true
145 | btRetry.isVisible = true
146 | }
147 |
148 | private fun onLoading() = binding.apply {
149 | activityPbFetchData.isVisible = true
150 | activityRecyclerView.isVisible = true
151 | activityTvError.isInvisible = true
152 | btRetry.isInvisible = true
153 | }
154 |
155 | companion object {
156 | var title = ""
157 | var sortCondition = ""
158 | var tagName = ""
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/view/init/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.view.init
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.lifecycle.lifecycleScope
8 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
9 | import com.josycom.mayorjay.flowoverstack.view.home.QuestionActivity
10 | import dagger.hilt.android.AndroidEntryPoint
11 | import kotlinx.coroutines.Job
12 | import kotlinx.coroutines.delay
13 | import kotlinx.coroutines.launch
14 |
15 | @SuppressLint("CustomSplashScreen")
16 | @AndroidEntryPoint
17 | class SplashActivity : AppCompatActivity() {
18 |
19 | private var job: Job? = null
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 | startJob()
24 | }
25 |
26 | private fun startJob() {
27 | job = lifecycleScope.launch {
28 | delay(AppConstants.SPLASH_TIME)
29 | goToMainActivity()
30 | }
31 | }
32 |
33 | private fun goToMainActivity() {
34 | startActivity(Intent(this, QuestionActivity::class.java))
35 | finish()
36 | }
37 |
38 | override fun onDestroy() {
39 | super.onDestroy()
40 | job?.cancel()
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/view/search/SearchActivity.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.view.search
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.text.TextUtils
6 | import android.view.MenuItem
7 | import android.view.View
8 | import android.view.inputmethod.InputMethodManager
9 | import androidx.activity.viewModels
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.core.view.isInvisible
12 | import androidx.core.view.isVisible
13 | import androidx.core.widget.NestedScrollView
14 | import androidx.recyclerview.widget.DefaultItemAnimator
15 | import androidx.recyclerview.widget.LinearLayoutManager
16 | import androidx.recyclerview.widget.RecyclerView
17 | import com.josycom.mayorjay.flowoverstack.R
18 | import com.josycom.mayorjay.flowoverstack.databinding.ActivitySearchBinding
19 | import com.josycom.mayorjay.flowoverstack.data.model.Question
20 | import com.josycom.mayorjay.flowoverstack.data.remote.model.SearchResponse
21 | import com.josycom.mayorjay.flowoverstack.viewmodel.SearchViewModel
22 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
23 | import com.josycom.mayorjay.flowoverstack.util.AppUtils
24 | import com.josycom.mayorjay.flowoverstack.view.answer.AnswerActivity
25 | import dagger.hilt.android.AndroidEntryPoint
26 |
27 | @AndroidEntryPoint
28 | class SearchActivity : AppCompatActivity() {
29 |
30 | private lateinit var binding: ActivitySearchBinding
31 | private var searchInput: String = ""
32 | private var questions: List? = listOf()
33 | private val searchViewModel: SearchViewModel by viewModels()
34 | override fun onCreate(savedInstanceState: Bundle?) {
35 | super.onCreate(savedInstanceState)
36 | binding = ActivitySearchBinding.inflate(layoutInflater)
37 | setContentView(binding.root)
38 |
39 | binding.apply {
40 | rvSearchResults.layoutManager = LinearLayoutManager(this@SearchActivity)
41 | rvSearchResults.setHasFixedSize(true)
42 | rvSearchResults.itemAnimator = DefaultItemAnimator()
43 | searchScrollUpFab.isInvisible = true
44 | searchNestedScrollview.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, _: Int ->
45 | if (scrollY > 0) {
46 | searchScrollUpFab.isVisible = true
47 | } else {
48 | searchScrollUpFab.isInvisible = true
49 | }
50 | }
51 | searchScrollUpFab.setOnClickListener { searchNestedScrollview.scrollTo(0, 0) }
52 | }
53 |
54 | binding.searchButton.setOnClickListener {
55 | if (TextUtils.isEmpty(binding.searchTextInputEditText.text.toString())) {
56 | binding.searchTextInputEditText.error = getString(R.string.type_a_search_query)
57 | } else {
58 | searchInput = binding.searchTextInputEditText.text.toString().trim()
59 | val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
60 | inputMethodManager.hideSoftInputFromWindow(currentFocus?.windowToken, 0)
61 | setQuery()
62 | binding.ivLookup.isInvisible = true
63 | }
64 | }
65 | val searchAdapter = SearchAdapter()
66 | searchViewModel.responseLiveData.observe(this) { searchResponse: SearchResponse ->
67 | when (searchResponse.networkState) {
68 | AppConstants.LOADING -> onLoading()
69 | AppConstants.LOADED -> {
70 | onLoaded()
71 | questions = searchResponse.questions
72 | searchAdapter.setQuestions(searchResponse.questions)
73 | }
74 | AppConstants.NO_MATCHING_RESULT -> onNoMatchingResult()
75 | AppConstants.FAILED -> onError()
76 | else -> searchAdapter.setQuestions(null)
77 | }
78 | }
79 | binding.rvSearchResults.adapter = searchAdapter
80 | searchAdapter.setOnClickListener(viewHolderClickListener, shareClickListener)
81 | }
82 |
83 | private val viewHolderClickListener = View.OnClickListener { v ->
84 | val viewHolder = v.tag as RecyclerView.ViewHolder
85 | val position = viewHolder.bindingAdapterPosition
86 | Intent(this, AnswerActivity::class.java).apply {
87 | val currentQuestion = questions?.get(position)
88 | currentQuestion?.let { putExtra(AppConstants.EXTRA_QUESTION_KEY, it) }
89 | startActivity(this)
90 | }
91 | }
92 |
93 | private val shareClickListener = View.OnClickListener { v ->
94 | val currentQuestion = v.tag as? Question
95 | currentQuestion?.let {
96 | val content = getString(
97 | R.string.share_content,
98 | getString(R.string.question),
99 | it.link,
100 | AppConstants.PLAY_STORE_URL
101 | )
102 | AppUtils.shareContent(content, this)
103 | }
104 | }
105 |
106 | private fun setQuery() {
107 | searchViewModel.setQuery(searchInput)
108 | }
109 |
110 | private fun onLoading() = binding.apply {
111 | searchPbFetchData.isVisible = true
112 | rvSearchResults.isInvisible = true
113 | searchTvError.isInvisible = true
114 | }
115 |
116 | private fun onLoaded() = binding.apply {
117 | searchPbFetchData.isInvisible = true
118 | rvSearchResults.isVisible = true
119 | searchTvError.isInvisible = true
120 | }
121 |
122 | private fun onNoMatchingResult() = binding.apply {
123 | searchPbFetchData.isInvisible = true
124 | rvSearchResults.isInvisible = true
125 | searchTvError.isVisible = true
126 | searchTvError.setText(R.string.no_matching_result)
127 | }
128 |
129 | private fun onError() = binding.apply {
130 | searchPbFetchData.isInvisible = true
131 | rvSearchResults.isInvisible = true
132 | searchTvError.isVisible = true
133 | searchTvError.setText(R.string.network_error_message)
134 | }
135 |
136 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
137 | if (item.itemId == android.R.id.home) {
138 | onBackPressed()
139 | return true
140 | }
141 | return super.onOptionsItemSelected(item)
142 | }
143 |
144 | override fun onBackPressed() {
145 | finish()
146 | }
147 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/view/search/SearchAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.view.search
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.core.view.isInvisible
7 | import androidx.core.view.isVisible
8 | import androidx.recyclerview.widget.RecyclerView
9 | import com.bumptech.glide.Glide
10 | import com.josycom.mayorjay.flowoverstack.R
11 | import com.josycom.mayorjay.flowoverstack.databinding.QuestionItemBinding
12 | import com.josycom.mayorjay.flowoverstack.data.model.Question
13 | import com.josycom.mayorjay.flowoverstack.view.search.SearchAdapter.SearchViewHolder
14 | import com.josycom.mayorjay.flowoverstack.util.AppUtils
15 | import org.jsoup.Jsoup
16 |
17 | class SearchAdapter : RecyclerView.Adapter() {
18 |
19 | private var questions: List? = null
20 |
21 | companion object {
22 | private var clickListener: View.OnClickListener? = null
23 | private var shareClickListener: View.OnClickListener? = null
24 | }
25 |
26 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
27 | val questionItemBinding = QuestionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
28 | return SearchViewHolder(questionItemBinding)
29 | }
30 |
31 | override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
32 | if (questions != null) {
33 | val currentQuestion = questions?.get(position)
34 | holder.bind(currentQuestion)
35 | }
36 | }
37 |
38 | fun setOnClickListener(onClickListener: View.OnClickListener?, shareOnClickListener: View.OnClickListener?) {
39 | clickListener = onClickListener
40 | shareClickListener = shareOnClickListener
41 | }
42 |
43 | override fun getItemCount(): Int {
44 | return if (questions != null) questions?.size ?: 0 else 0
45 | }
46 |
47 | fun setQuestions(questions: List?) {
48 | this.questions = questions
49 | notifyDataSetChanged()
50 | }
51 |
52 | inner class SearchViewHolder(private val binding: QuestionItemBinding) : RecyclerView.ViewHolder(binding.root) {
53 |
54 | init {
55 | binding.root.tag = this
56 | binding.root.setOnClickListener(clickListener)
57 | binding.ivShare.setOnClickListener(shareClickListener)
58 | }
59 |
60 | fun bind(question: Question?) {
61 | if (question != null) {
62 | binding.ivShare.tag = question
63 | val owner = question.owner
64 | val profileImage = owner.profileImage
65 | val tagList = question.tags
66 | Glide.with(binding.root.context)
67 | .load(profileImage)
68 | .placeholder(R.drawable.loading)
69 | .into(binding.ivAvatarItem)
70 | binding.tvQuestionItem.text = Jsoup.parse(question.title).text()
71 | binding.tvViewsCountItem.text = question.viewCount.toString()
72 | binding.tvDateItem.text = AppUtils.toNormalDate(question.creationDate.toLong())
73 | if (question.isAnswered) {
74 | binding.answered.isVisible = true
75 | } else {
76 | binding.answered.isInvisible = true
77 | }
78 | val answers = question.answerCount
79 | val resources = binding.root.context.resources
80 | val answerCount = resources.getQuantityString(R.plurals.answers, answers, answers)
81 | binding.tvAnswersCountItem.text = answerCount
82 | if (question.score <= 0) {
83 | binding.tvVotesCountItem.text = question.score.toString()
84 | } else {
85 | binding.tvVotesCountItem.text =
86 | binding.root.context.getString(R.string.plus_score, question.score)
87 | }
88 | binding.tvTagsListItem.text = AppUtils.getFormattedTags(tagList)
89 | }
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/view/tag/TagsAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.view.tag
2 |
3 | import android.annotation.SuppressLint
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.paging.PagingDataAdapter
8 | import androidx.recyclerview.widget.DiffUtil
9 | import androidx.recyclerview.widget.RecyclerView
10 | import com.josycom.mayorjay.flowoverstack.R
11 | import com.josycom.mayorjay.flowoverstack.databinding.TagItemBinding
12 | import com.josycom.mayorjay.flowoverstack.data.model.Tag
13 |
14 | class TagsAdapter: PagingDataAdapter(DIFF_CALLBACK) {
15 |
16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TagViewHolder {
17 | val view = TagItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
18 | return TagViewHolder(view)
19 | }
20 |
21 | override fun onBindViewHolder(holder: TagViewHolder, position: Int) {
22 | getItem(position)?.let { holder.bind(it) }
23 | }
24 |
25 | fun setOnClickListener(listener: View.OnClickListener?) {
26 | onClickListener = listener
27 | }
28 |
29 | companion object {
30 | private var onClickListener: View.OnClickListener? = null
31 | private val DIFF_CALLBACK = object: DiffUtil.ItemCallback() {
32 | override fun areItemsTheSame(oldItem: Tag, newItem: Tag): Boolean = oldItem.name == newItem.name
33 |
34 | @SuppressLint("DiffUtilEquals")
35 | override fun areContentsTheSame(oldItem: Tag, newItem: Tag): Boolean = oldItem == newItem
36 | }
37 | }
38 |
39 |
40 | class TagViewHolder(private val binding: TagItemBinding): RecyclerView.ViewHolder(binding.root) {
41 |
42 | init {
43 | binding.root.tag = this
44 | binding.root.setOnClickListener(onClickListener)
45 | }
46 |
47 | fun bind(tag: Tag) {
48 | with(binding) {
49 | tvTagName.text = tag.name
50 | tvTagCount.text = root.context.resources.getQuantityString(
51 | R.plurals.questions,
52 | tag.count.toInt(),
53 | tag.count.toInt()
54 | )
55 | }
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/view/tag/TagsDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.view.tag
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.TextView
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.core.view.isInvisible
10 | import androidx.core.view.isVisible
11 | import androidx.fragment.app.DialogFragment
12 | import androidx.fragment.app.viewModels
13 | import androidx.lifecycle.Lifecycle
14 | import androidx.lifecycle.lifecycleScope
15 | import androidx.lifecycle.repeatOnLifecycle
16 | import androidx.paging.LoadState
17 | import androidx.recyclerview.widget.DefaultItemAnimator
18 | import androidx.recyclerview.widget.LinearLayoutManager
19 | import androidx.recyclerview.widget.RecyclerView
20 | import com.josycom.mayorjay.flowoverstack.R
21 | import com.josycom.mayorjay.flowoverstack.databinding.TagsDialogFragmentBinding
22 | import com.josycom.mayorjay.flowoverstack.view.home.PagingLoadStateAdapter
23 | import com.josycom.mayorjay.flowoverstack.viewmodel.TagsDialogViewModel
24 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
25 | import dagger.hilt.android.AndroidEntryPoint
26 | import kotlinx.coroutines.flow.collectLatest
27 | import kotlinx.coroutines.launch
28 | import org.jsoup.internal.StringUtil
29 | import java.util.Locale
30 |
31 | @AndroidEntryPoint
32 | class TagsDialogFragment : DialogFragment() {
33 |
34 | private lateinit var activity: AppCompatActivity
35 | private val viewModel: TagsDialogViewModel by viewModels()
36 | private lateinit var binding: TagsDialogFragmentBinding
37 | private var tagSelectionCallback: TagSelectionCallback? = null
38 | private var title: String = ""
39 | private var isPopularTagOption: Boolean = false
40 |
41 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
42 | activity = getActivity() as AppCompatActivity
43 | binding = TagsDialogFragmentBinding.inflate(layoutInflater)
44 | return binding.root
45 | }
46 |
47 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
48 | super.onViewCreated(view, savedInstanceState)
49 |
50 | initViews()
51 | if (isPopularTagOption) {
52 | fetchAndDisplayTags("")
53 | }
54 | setupListeners()
55 | }
56 |
57 | private fun initViews() {
58 | binding.tvTitle.text = title
59 | binding.layoutSearch.isInvisible = isPopularTagOption
60 | binding.tvInfo.isInvisible = !isPopularTagOption
61 | }
62 |
63 | private fun setupListeners() {
64 | binding.btSearch.setOnClickListener {
65 | val query = binding.searchTextInputEditText.text.toString().trim()
66 | .lowercase(Locale.getDefault())
67 | if (StringUtil.isBlank(query)) {
68 | binding.searchTextInputLayout.isErrorEnabled = true
69 | binding.searchTextInputLayout.error = getString(R.string.type_a_search_query)
70 | return@setOnClickListener
71 | }
72 | binding.searchTextInputLayout.isErrorEnabled = false
73 | fetchAndDisplayTags(query)
74 | }
75 | }
76 |
77 | private fun fetchAndDisplayTags(inName: String) {
78 | viewModel.fetchTags(inName, AppConstants.FIRST_PAGE, AppConstants.PAGE_SIZE, AppConstants.API_KEY)
79 | binding.ivLookup.isInvisible = true
80 | val popularTagAdapter = TagsAdapter()
81 | binding.rvPopularTags.apply {
82 | layoutManager = LinearLayoutManager(activity)
83 | itemAnimator = DefaultItemAnimator()
84 | adapter = popularTagAdapter.withLoadStateFooter(PagingLoadStateAdapter { popularTagAdapter.retry() })
85 | }
86 |
87 | viewLifecycleOwner.lifecycleScope.launch {
88 | popularTagAdapter.loadStateFlow.collect {
89 | binding.pbPopularTags.isVisible = it.source.refresh is LoadState.Loading
90 | binding.tvError.isVisible = it.source.refresh is LoadState.Error
91 | binding.btRetry.isVisible = it.source.refresh is LoadState.Error
92 | binding.tvInfo.isVisible = it.source.refresh !is LoadState.Loading && it.source.refresh !is LoadState.Error && isPopularTagOption
93 | if (it.source.refresh is LoadState.NotLoading && popularTagAdapter.itemCount <= 0) {
94 | binding.tvError.isVisible = true
95 | binding.tvError.setText(R.string.no_matching_result_rephrase)
96 | } else {
97 | binding.tvError.isInvisible = true
98 | }
99 | }
100 | }
101 |
102 | viewLifecycleOwner.lifecycleScope.launch {
103 | repeatOnLifecycle(Lifecycle.State.STARTED) {
104 | viewModel.tagDataFlow?.collectLatest {
105 | popularTagAdapter.submitData(it)
106 | }
107 | }
108 | }
109 |
110 | popularTagAdapter.setOnClickListener(onClickListener)
111 | binding.btRetry.setOnClickListener { popularTagAdapter.retry() }
112 | }
113 |
114 | private val onClickListener = View.OnClickListener { v ->
115 | val holder = v?.tag as RecyclerView.ViewHolder
116 | val tagName = holder.itemView.findViewById(R.id.tv_tag_name).text
117 | tagSelectionCallback?.onTagSelected(tagName.toString())
118 | dismiss()
119 | }
120 |
121 | override fun onStart() {
122 | super.onStart()
123 | val window = dialog?.window
124 | if (window != null) {
125 | val width = ViewGroup.LayoutParams.MATCH_PARENT
126 | val height = ViewGroup.LayoutParams.MATCH_PARENT
127 | window.setLayout(width, height)
128 | }
129 | }
130 |
131 | fun setTagSelectionListener(tagSelectionCallback: TagSelectionCallback) {
132 | this.tagSelectionCallback = tagSelectionCallback
133 | }
134 |
135 | fun setInitItems(title: String, isPopularTagOption: Boolean) {
136 | this.title = title
137 | this.isPopularTagOption = isPopularTagOption
138 | }
139 |
140 | interface TagSelectionCallback {
141 | fun onTagSelected(tagName: String)
142 | }
143 |
144 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/viewmodel/AnswerViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.viewmodel
2 |
3 | import com.josycom.mayorjay.flowoverstack.data.repository.AnswerRepository
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import androidx.paging.PagingData
7 | import androidx.paging.cachedIn
8 | import com.josycom.mayorjay.flowoverstack.data.model.Answer
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.Flow
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | class AnswerViewModel @Inject constructor(private val answerRepository: AnswerRepository) : ViewModel() {
15 |
16 | var answerDataFlow: Flow>? = null
17 |
18 | fun init(questionId: Int, order: String, sortCondition: String, site: String, filter: String, siteKey: String) {
19 | answerRepository.init(questionId, order, sortCondition, site, filter, siteKey)
20 | answerDataFlow = answerRepository.answerDataFlow?.cachedIn(viewModelScope)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/viewmodel/QuestionActivityViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.viewmodel
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.asLiveData
6 | import androidx.lifecycle.viewModelScope
7 | import com.josycom.mayorjay.flowoverstack.data.repository.PreferenceRepository
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.launch
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | class QuestionActivityViewModel @Inject constructor(private val preferenceRepository: PreferenceRepository) :
15 | ViewModel() {
16 |
17 | var appOpenCountLiveData: LiveData? = null
18 | var appOpenCount = 0
19 |
20 | fun getAppOpenCountPref(key: String): Flow {
21 | return preferenceRepository.getIntPreferenceFlow(key).apply {
22 | appOpenCountLiveData = this.asLiveData()
23 | }
24 | }
25 |
26 | fun saveAppOpenCounts(key: String, value: Int) {
27 | viewModelScope.launch {
28 | preferenceRepository.setIntPreference(key, value)
29 | }
30 | }
31 |
32 | fun deletePreferences() {
33 | viewModelScope.launch {
34 | preferenceRepository.deleteAllPreferences()
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/viewmodel/QuestionViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import androidx.paging.PagingData
6 | import androidx.paging.cachedIn
7 | import com.josycom.mayorjay.flowoverstack.data.model.Question
8 | import com.josycom.mayorjay.flowoverstack.data.repository.QuestionRepository
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.Flow
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | class QuestionViewModel @Inject constructor(private val questionRepository: QuestionRepository) : ViewModel() {
15 |
16 | var questionDataFlow: Flow>? = null
17 |
18 | fun init(page: Int, pageSize: Int, order: String, sortCondition: String, site: String, tagged: String, filter: String, siteKey: String) {
19 | questionRepository.init(page, pageSize, order, sortCondition, site, tagged, filter, siteKey)
20 | questionDataFlow = questionRepository.questionDataFlow?.cachedIn(viewModelScope)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/viewmodel/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.josycom.mayorjay.flowoverstack.data.remote.model.SearchResponse
5 | import com.josycom.mayorjay.flowoverstack.data.repository.SearchRepository
6 | import com.josycom.mayorjay.flowoverstack.util.AppConstants
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import javax.inject.Inject
9 |
10 | @HiltViewModel
11 | class SearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() {
12 |
13 | val responseLiveData = searchRepository.searchResponse
14 |
15 | init {
16 | responseLiveData.value = SearchResponse()
17 | }
18 |
19 | fun setQuery(query: String) {
20 | searchRepository.performSearch(query, AppConstants.FIRST_PAGE, AppConstants.SEARCH_PAGE_SIZE)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/josycom/mayorjay/flowoverstack/viewmodel/TagsDialogViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import androidx.paging.PagingData
6 | import androidx.paging.cachedIn
7 | import com.josycom.mayorjay.flowoverstack.data.repository.TagRepository
8 | import com.josycom.mayorjay.flowoverstack.data.model.Tag
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.Flow
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | class TagsDialogViewModel @Inject constructor(private val tagRepository: TagRepository) : ViewModel() {
15 |
16 | var tagDataFlow: Flow>? = null
17 |
18 | fun fetchTags(inName: String, page: Int, pageSize: Int, siteKey: String) {
19 | tagRepository.init(page, pageSize, inName, siteKey)
20 | tagDataFlow = tagRepository.tagDataFlow?.cachedIn(viewModelScope)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/res/anim/fab_close.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
13 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fab_open.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
13 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_in_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_out_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/animator/search_button_state_list_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | -
9 |
10 |
15 |
20 |
21 |
22 |
23 |
24 | -
27 |
28 |
33 |
38 |
39 |
40 |
41 |
42 | -
45 |
46 |
51 |
56 |
57 |
58 |
59 |
60 | -
61 |
62 |
69 |
74 |
75 |
76 |
77 |
78 | -
79 |
80 |
85 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_action_scroll_up.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_info.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_action_scroll_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable-hdpi/ic_action_scroll_up.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable-hdpi/ic_info.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_action_scroll_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable-mdpi/ic_action_scroll_up.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable-mdpi/ic_info.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_action_scroll_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable-xhdpi/ic_action_scroll_up.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable-xhdpi/ic_info.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable-xhdpi/loading.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_action_scroll_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable-xxhdpi/ic_action_scroll_up.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable-xxhdpi/ic_info.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable/app_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/app_iconn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/drawable/app_iconn.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/button_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_verified_answer.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_camera.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_clear.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_date.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_email.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_keyboard.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_refresh.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_search.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_share.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_share_green.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_tag.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_twitter.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_view.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_vote.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash_screen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 | -
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/tag_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/font/magra.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/font/montserrat.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/font/montserrat_bold.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/font/montserrat_light.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/font/montserrat_light.otf
--------------------------------------------------------------------------------
/app/src/main/res/font/poppins_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/font/poppins_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/poppins_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/font/poppins_light.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/poppins_semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/font/poppins_semibold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_ocr.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
18 |
19 |
25 |
26 |
38 |
39 |
55 |
56 |
66 |
67 |
80 |
81 |
88 |
89 |
90 |
106 |
107 |
112 |
113 |
126 |
127 |
128 |
129 |
130 |
147 |
148 |
163 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_question.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
49 |
50 |
73 |
74 |
90 |
91 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
18 |
19 |
30 |
31 |
37 |
38 |
39 |
50 |
51 |
64 |
65 |
78 |
79 |
85 |
86 |
90 |
91 |
92 |
93 |
94 |
95 |
110 |
111 |
118 |
119 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_web_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
21 |
22 |
35 |
36 |
43 |
44 |
45 |
46 |
47 |
48 |
53 |
54 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/answer_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
31 |
32 |
41 |
42 |
51 |
52 |
61 |
62 |
71 |
72 |
82 |
83 |
94 |
95 |
102 |
103 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_questions.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
31 |
32 |
47 |
48 |
57 |
58 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_info_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
31 |
32 |
44 |
45 |
51 |
52 |
58 |
59 |
65 |
66 |
77 |
78 |
87 |
88 |
99 |
100 |
109 |
110 |
121 |
122 |
131 |
132 |
139 |
140 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/paging_load_state_footer_view_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
24 |
25 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/tag_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
23 |
24 |
36 |
37 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/tags_dialog_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
21 |
22 |
31 |
32 |
41 |
42 |
49 |
50 |
51 |
59 |
60 |
61 |
62 |
76 |
77 |
86 |
87 |
95 |
96 |
106 |
107 |
122 |
123 |
133 |
134 |
144 |
145 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_webview.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/mipmap-hdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/mipmap-mdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/mipmap-xhdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/mipmap-xxhdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app/src/main/res/mipmap-xxxhdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/values-sw600dp/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 | true
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-xlarge/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 | true
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 | false
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #344f27
4 | #5f7b50
5 | #0f2600
6 | #ffffff
7 | #000000
8 | #3B6AA8
9 | #dadada
10 | #cc5f7b50
11 | #66000000
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 | 8dp
4 | 4dp
5 | 50dp
6 | 4dp
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/font_certs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @array/com_google_android_gms_fonts_certs_dev
5 | - @array/com_google_android_gms_fonts_certs_prod
6 |
7 |
8 | -
9 | MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
10 |
11 |
12 |
13 | -
14 | MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/plurals.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - %d answer
5 | - %d answers
6 |
7 |
8 | - %d question
9 | - %d questions
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/preloaded_fonts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @font/magra
5 | - @font/montserrat
6 | - @font/montserrat_bold
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | StackOverflow: Community Version
3 | Something is not right! Please check your network
4 | Search
5 | Filter by Activity
6 | Filter by Recency
7 | Filter by Hot
8 | Filter by Votes
9 | User image
10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua
11 | 11.10.2019
12 | 12
13 | John Doe
14 | Asked:
15 | Answers:
16 | Score
17 | Answered:
18 | view indicator icon
19 | Asked:
20 | Search Stack Overflow
21 | Oops! No Matching Result. Rephrase your search query or Use fewer words
22 | Oops! No Matching Result. Rephrase your search query
23 | scroll up button
24 | By:
25 | No Answers Yet
26 | App logo
27 | Question
28 | Active Questions
29 | Recent Questions
30 | Hot Questions
31 | Voted Questions
32 | vote icon
33 | tag icon
34 | Type a search query
35 | Search
36 | Read More
37 | +%d
38 | answers
39 | Answer
40 | Refresh
41 | Open in Browser
42 | Answered icon
43 | Scan to Search
44 | Type to Search
45 | Home
46 | Scan
47 | Ensure that the recognised text is here
48 | Recognised Text
49 | Recognise
50 | OCR
51 | Filter by Popular Tags
52 | Filter by Tag Name
53 | %s questions
54 | Popular Tags
55 | Select a Tag to view the related Questions
56 | Retry
57 | Search Tag Name
58 | Input tag name
59 | Share image
60 | No text found, scan again
61 | Oops!, that text could not be recognized. Scan again
62 | App Permissions
63 | This application requires access to your camera and storage to function!
64 | No
65 | Ask Me
66 | Permission Denied
67 | Oops, cannot open URL
68 | Restart
69 | An update has just been downloaded
70 | Oops! an error occurred, try again
71 | Hi, check out this %s on SO:\n%s \n\nDownload StackOverflow Community Version and get to browse SO easily:\n%s
72 | Download StackOverflow Community Version and get to browse SO easily:\n%s
73 | App Info
74 | ☝️
75 | Share
76 | Connect
77 | Contact
78 | v%s
79 | App Rating
80 | Hey, it looks like you\'re a frequent user of StackOverflow Community Version! If you have a couple seconds, please rate this app on Google Play Store. Every rating goes a long way towards motivating us to continue improving the app!
81 | Sure, I\'ll Rate
82 | No, I\'m Good
83 |
84 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
38 |
39 |
42 |
43 |
48 |
49 |
56 |
57 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/provider_path.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/test/java/com/josycom/mayorjay/flowoverstack/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * @see [Testing documentation](http://d.android.com/tools/testing)
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | Assert.assertEquals(4, (2 + 2).toLong())
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/josycom/mayorjay/flowoverstack/testdata/FakePreferenceRepository.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.testdata
2 |
3 | import com.josycom.mayorjay.flowoverstack.data.repository.PreferenceRepository
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.flow
6 |
7 | class FakePreferenceRepository : PreferenceRepository {
8 |
9 | private val fakeDataStore: MutableMap = mutableMapOf()
10 |
11 | override fun getIntPreferenceFlow(key: String): Flow {
12 | return flow {
13 | emit(fakeDataStore[key] as Int?)
14 | }
15 | }
16 |
17 | override suspend fun setIntPreference(key: String, value: Int) {
18 | fakeDataStore[key] = value
19 | }
20 |
21 | override suspend fun deleteAllPreferences() {
22 | fakeDataStore.clear()
23 | }
24 |
25 | override suspend fun contains(key: String) = fakeDataStore.contains(key)
26 |
27 | override suspend fun isEmpty() = fakeDataStore.isEmpty()
28 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/josycom/mayorjay/flowoverstack/util/AppUtilsTest.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.util
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 |
6 | class AppUtilsTest {
7 |
8 | @Test
9 | fun `test toNormalDate pass_in_valid_time return a non_null value`() {
10 | val result = AppUtils.toNormalDate(System.currentTimeMillis())
11 | Assert.assertNotNull(result)
12 | }
13 |
14 | @Test
15 | fun `test toNormalDate pass_in_valid_time return a valid format`() {
16 | val result = AppUtils.toNormalDate(System.currentTimeMillis())
17 | val regex = "^[a-zA-Z]{3}\\s[0-9]{2}\\s[0-9]{4,}$".toRegex()
18 | val match = regex.matches(result!!)
19 | Assert.assertTrue(match)
20 | }
21 |
22 | @Test
23 | fun `test getFormattedTags pass_in_empty_list return empty String`() {
24 | val result = AppUtils.getFormattedTags(listOf())
25 | Assert.assertEquals("", result)
26 | }
27 |
28 | @Test
29 | fun `test getFormattedTags pass_in_valid_list return list joined as a single String`() {
30 | val list = listOf("Java", "Kotlin", "Android", "JetPack")
31 | val result = AppUtils.getFormattedTags(list)
32 | Assert.assertEquals("Java, Kotlin, Android, JetPack", result)
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/josycom/mayorjay/flowoverstack/viewmodel/QuestionActivityViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.josycom.mayorjay.flowoverstack.viewmodel
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import com.josycom.mayorjay.flowoverstack.testdata.FakePreferenceRepository
5 | import junit.framework.TestCase
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import kotlinx.coroutines.flow.first
9 | import kotlinx.coroutines.runBlocking
10 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
11 | import kotlinx.coroutines.test.resetMain
12 | import kotlinx.coroutines.test.setMain
13 | import org.junit.Rule
14 |
15 | @OptIn(ExperimentalCoroutinesApi::class)
16 | class QuestionActivityViewModelTest : TestCase() {
17 |
18 | @get:Rule
19 | val instantTaskExecutorRule = InstantTaskExecutorRule()
20 | private val dispatcher = UnconfinedTestDispatcher()
21 | private lateinit var sut: QuestionActivityViewModel
22 | private val repository = FakePreferenceRepository()
23 |
24 | override fun setUp() {
25 | Dispatchers.setMain(dispatcher)
26 | sut = QuestionActivityViewModel(repository)
27 | }
28 |
29 | override fun tearDown() {
30 | super.tearDown()
31 | Dispatchers.resetMain()
32 | }
33 |
34 | fun `test getAppOpenCountPref __ pass_any_key_not_previously_saved __ null should be returned`() =
35 | runBlocking {
36 | val result = sut.getAppOpenCountPref("int").first()
37 | assertNull(result)
38 | }
39 |
40 | fun `test saveAppOpenCounts __ pass_any_key __ data source should not be empty`() =
41 | runBlocking {
42 | sut.saveAppOpenCounts("key", 20)
43 | assertFalse(repository.isEmpty())
44 | }
45 |
46 | fun `test saveAppOpenCounts __ data source should contain the key saved`() = runBlocking {
47 | sut.saveAppOpenCounts("key", 20)
48 | assertTrue(repository.contains("key"))
49 | }
50 |
51 | fun `test saveAppOpenCounts when retrieved the value should be the correct value saved`() =
52 | runBlocking {
53 | sut.saveAppOpenCounts("key", 20)
54 | val result = sut.getAppOpenCountPref("key").first()
55 | assertEquals(20, result)
56 | }
57 |
58 | fun `test getAppOpenCountPref when retrieved the value should be the correct value saved`() =
59 | runBlocking {
60 | sut.saveAppOpenCounts("abc", 5)
61 | val result = sut.getAppOpenCountPref("abc").first()
62 | assertEquals(5, result)
63 | }
64 |
65 | fun `test deletePreferences the data store should be empty`() = runBlocking {
66 | sut.saveAppOpenCounts("xyz", 1)
67 | sut.saveAppOpenCounts("tea", 2)
68 | sut.deletePreferences()
69 | assertTrue(repository.isEmpty())
70 | }
71 | }
--------------------------------------------------------------------------------
/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/app_icon.png
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 |
5 | ext.kotlin_version = "1.9.24"
6 | ext.hilt_version = '2.52'
7 |
8 | repositories {
9 | google()
10 | mavenCentral()
11 | maven { url "https://jitpack.io" }
12 | }
13 | dependencies {
14 | classpath 'com.android.tools.build:gradle:8.7.1'
15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
16 | classpath 'org.robolectric:robolectric-gradle-plugin:1.0.1'
17 | classpath 'com.google.gms:google-services:4.3.10'
18 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.0'
19 | classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
20 | // NOTE: Do not place your application dependencies here; they belong
21 | // in the individual module build.gradle files
22 | }
23 | }
24 |
25 | allprojects {
26 | repositories {
27 | google()
28 | mavenCentral()
29 | maven { url "https://jitpack.io" }
30 | }
31 | }
32 |
33 | tasks.register('clean', Delete) {
34 | delete rootProject.layout.buildDirectory
35 | }
36 |
--------------------------------------------------------------------------------
/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 |
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayorjay/StackOverflow-Community-Version/eb154d661077579ccf00bbf329d2bf6ae358d7e7/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jun 14 12:23:56 WAT 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-8.9-bin.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='FlowOverStack'
2 | include ':app'
3 |
--------------------------------------------------------------------------------